diff --git a/README.md b/README.md index 70540d2..d045c60 100644 --- a/README.md +++ b/README.md @@ -75,16 +75,108 @@ The plugin also integrates with the [htmlfy](https://github.com/j4w8n/htmlfy#rea ## Configuration -Administradors can set these options + +### Admin Settings + +This section describes the configurable admin settings available for the Tiny CodePro plugin, which enable fine-grained control over valid HTML structures, element nesting, and custom elements in the editor. Administrator options -Additionally, the capability `tiny/codepro:viewplugin` controls visibility for specific roles. +For more information, you can read the [Content filtering options](https://www.tiny.cloud/docs/tinymce/latest/content-filtering/) in TinyMCE documentation page. + +--- + +#### 🔧 `tiny_codepro | extendedvalidelements` + +**Default:** +``` +*[*],svg[*],math[*],script[*],style[*] +``` + +**Description:** +Specifies which HTML elements and attributes are considered valid in the editor. Use the format: +`tag[attr1|attr2|...],tag[*]` to allow specific attributes, or `[*]` to allow all. + +**Example:** +``` +*[*],svg[*],math[*],script[*] +``` + +This example allows all attributes on all tags, as well as unrestricted use of ``, ``, and `` +- Elements like `
`, `

`, ``, ``, `` inside `

', - expected: `

${marker}

`, + expected: `

${htmlMarker}

`, pos: 20 }, { name: 'fallback to before ', - expected: `
abc${marker}
`, + expected: `
abc${htmlMarker}
`, pos: 10 }, { name: 'cursor in safe tag but within attribute (should fallback)', doc: '

test

', - expected: `
${marker}

test

`, + expected: `
${htmlMarker}

test

`, pos: 12 }, // More self-closing cases { name: 'cursor at self-closing tag
', doc: '
line
next
', - expected: `
line${marker}
next
`, + expected: `
line${htmlMarker}
next
`, pos: 11, tagName: 'TagName' }, { name: 'cursor at empty container', doc: '
', - expected: `
${marker}
`, + expected: `
${htmlMarker}
`, pos: 5, tagName: 'EndTag' }, { name: 'cursor inside nested disallowed tags (script in title)', doc: '<script><b>skip this</b></script>', - expected: `${marker}<script><b>skip this</b></script>`, + expected: `${htmlMarker}<script><b>skip this</b></script>`, pos: 25, }, { name: 'cursor between sibling paragraphs', doc: '

first

second

', - expected: `

first

${marker}

second

`, + expected: `

first

${htmlMarker}

second

`, pos: 17, tagName: 'EndTag' }, { name: 'document already contains marker (gets removed when doc set) 1', - doc: `
@
`, - expected: `
@
`, + doc: `
@a
`, + expected: `
${htmlMarker}a
`, pos: 5, }, { name: 'document already contains marker (gets removed when doc set) 2', doc: `
@
`, - expected: `@
`, + expected: `
${htmlMarker}
`, pos: 2, } ]; @@ -419,4 +451,149 @@ describe('CodeProEditor - Marker Insertion (atElement)', () => { expectMarker(html, posPresent, expected, tagName); }); }); -}); \ No newline at end of file +}); + + +describe('CodeProEditor - Advanced Marker Insertion (atElement)', () => { + let editor; + const htmlMarker = ` `; + + const createEditor = (doc, pos) => { + const container = document.createElement('div'); + document.body.appendChild(container); + editor = new CodeProEditor(container, { doc, commands: {} }); + editor.setSelection({ anchor: pos }); + return editor; + }; + + const destroyEditor = () => editor?.destroy(); + + afterEach(() => { + destroyEditor(); + }); + + const newTests = [ + // --- Strict tables tests --- + { + name: 'cursor inside ', + doc: '
Cell 1
', + pos: 26, //
Cell| 1
+ expected: `
Cell${htmlMarker} 1
` + }, + { + name: 'cursor between and (should place before closing tag)', + doc: '
Cell 1
', + pos: 29, //
Cell 1<|/td>
+ expected: `
Cell 1${htmlMarker}
` + }, + { + name: 'cursor between and (invalid position, should find nearest valid)', + doc: '
Cell 1
', + pos: 19, // <|td>Cell 1
+ expected: `
${htmlMarker}Cell 1
` + }, + { + name: 'cursor between and (invalid position, should find nearest valid)', + doc: '
Cell 1
', + pos: 35, //
Cell 1
+ expected: `
Cell 1${htmlMarker}
` + }, + + // --- Tests with lists --- + { + name: 'cursor inside
  • ', + doc: '
    • Item 1
    • Item 2
    ', + pos: 10, //
    • It|em 1
    • Item 2
    + expected: `
    • It${htmlMarker}em 1
    • Item 2
    ` + }, + { + name: 'cursor between
  • tags (should place after first item)', + doc: '
    • Item 1
    • Item 2
    ', + pos: 18, //
    • Item 1
    • Item 2
    + expected: `
    • Item 1${htmlMarker}
    • Item 2
    ` + }, + { + name: 'cursor inside invalid
      position (should place before first li)', + doc: '
      • Item 1
      ', + pos: 4, //
        |
      • Item 1
      + expected: `
      • ${htmlMarker}Item 1
      ` + }, + + // --- Tests with selfclosing tags --- + { + name: 'cursor after an tag', + doc: '

      Some text and more.

      ', + pos: 27, //

      Some text and more.

      + expected: `

      Some text ${htmlMarker} and more.

      ` + }, + { + name: 'cursor before an tag', + doc: '

      Name:

      ', + pos: 10, //

      Name: <|input type="text"/>

      + expected: `

      Name: ${htmlMarker}

      ` + }, + + // --- Bad formatted HTML --- + { + name: 'unclosed

      tag', + doc: '

      First paragraph

      Second paragraph', + pos: 20, //

      First paragraphSecond paragraph + expected: `

      First paragraph${htmlMarker}

      Second paragraph

      ` + }, + { + name: 'cursor after unclosed

      tag', + doc: '

      unclosed tag

      ', + pos: 20, //

      unclosed tag|

      + expected: `

      unclosed tag${htmlMarker}

      ` + }, + + // --- Tests including white spaces and line breaks --- + { + name: 'lots of whitespace between elements', + doc: '
      \n

      \n Hello\n

      \n
      ', + pos: 16, // Before "Hello" + expected: `
      \n

      \n ${htmlMarker}Hello\n

      \n
      ` + }, + { + name: 'cursor on a blank line between tags', + doc: '
      \n

      First

      \n \n

      Second

      \n
      ', + pos: 22, //
      \n

      First

      \n | \n

      Second

      \n
      + expected: `
      \n

      First

      \n ${htmlMarker} \n

      Second

      \n
      ` + }, + + // --- Tests with definition lists (dl, dt, dd) --- + { + name: 'cursor inside
      ', + doc: '
      Term
      Definition
      ', + pos: 10, //
      Te|rm
      Definition
      + expected: `
      Te${htmlMarker}rm
      Definition
      ` + }, + { + name: 'cursor between
      and
      (invalid, should place after dt)', + doc: '
      Term
      Definition
      ', + pos: 17, //
      Term
      |
      Definition
      + expected: `
      Term${htmlMarker}
      Definition
      ` + }, + { + name: 'cursor between
      and
      (invalid, should place inside dd)', + doc: '
      Definition
      ', + pos: 15, //
      Definition
      + expected: `
      ${htmlMarker}Definition
      ` + }, + // --- Tests within textarea --- + { + name: 'cursor inside textarea', + doc: '', + pos: 15, // + expected: `${htmlMarker}` + }, + ]; + + newTests.forEach(({ name, doc, pos, expected }) => { + it(`should handle: ${name}`, () => { + const editor = createEditor(doc, pos); + const htmlWithMarker = editor.getValue(CodeProEditor.MarkerType.atElement); + expect(htmlWithMarker).toBe(expected); + }); + }); +}); diff --git a/libs/codemirror/cursorsync.mjs b/libs/codemirror/cursorsync.mjs index d130b4a..963e3e6 100644 --- a/libs/codemirror/cursorsync.mjs +++ b/libs/codemirror/cursorsync.mjs @@ -24,20 +24,6 @@ import { EditorView } from "@codemirror/view"; import { Transaction } from '@codemirror/state'; import { SearchCursor } from '@codemirror/search'; import { syntaxTree } from '@codemirror/language'; -import { getTagNameFromCursor } from './treeutils'; - -const disallowedTags = new Set([ - "script", "style", "textarea", "title", "noscript", - "option", "optgroup", "select", - "svg", "math", "object", "iframe", - "head", "meta", "link", "base", "source", "track", "param", - "img", "input", "br", "hr", "col", "embed", "area", "wbr" -]); - -const disallowedContexts = new Set([ - "ScriptText", "StyleText", "Comment", "CommentText", "CommentBlock", "Attribute", "TagName", - "StartTag", "EndTag", "MismatchedCloseTag", "ObjectElement", "SvgElement" -]); /** * Class responsible for synchronizing cursor-based interactions @@ -45,40 +31,46 @@ const disallowedContexts = new Set([ */ export class CursorSync { /** - * Creates an instance of CursorSync. - * - * @param {EditorView} editorView - The CodeMirror editor view instance. - * @param {string} marker - The marker string to insert at cursor/element positions. - */ - constructor(editorView, marker) { + * Creates an instance of CursorSync. + * + * @param {EditorView} editorView - The CodeMirror editor view instance. + * @param {string} marker - The marker string to insert at cursor/element positions. + */ + constructor(editorView, marker, markerClass) { this.editorView = editorView; this.marker = marker; + this.markerClass = markerClass; } /** * Scrolls the editor view to the position of the marker. * If the marker is not found, scrolls the current view into focus. + * @param {number} [head] - If the head position is passed it will not look for any markers. */ - scrollToCaretPosition() { - const state = this.editorView.state; - const cursor = new SearchCursor(state.doc, this.marker, 0, state.doc.length); + scrollToCaretPosition(head) { const changes = []; - let firstMatch = null; - while (!cursor.next().done) { - const value = cursor.value; - if (!cursor.value) { - continue; - } - if (!firstMatch) { - firstMatch = value; + if (!head) { + // Look for the marker. + const state = this.editorView.state; + const cursor = new SearchCursor(state.doc, this.marker, 0, state.doc.length); + while (!cursor.next().done) { + const value = cursor.value; + if (!cursor.value) { + continue; + } + if (!head) { + // Stores the position of the first marker found + head = value.from; + } + // To deletes all markers + changes.push({ from: value.from, to: value.to, insert: '' }); } - changes.push({ from: value.from, to: value.to, insert: '' }); } - if (firstMatch) { + if (head) { this.editorView.dispatch({ changes, - selection: { anchor: firstMatch.from }, - effects: EditorView.scrollIntoView(firstMatch.from, { y: "center" }), + selection: { anchor: head }, + effects: EditorView.scrollIntoView(head, { y: "center" }), annotations: [Transaction.addToHistory.of(false)] }); } else { @@ -98,123 +90,150 @@ export class CursorSync { */ getValueWithMarkerAtCursor() { const cursor = this.editorView.state.selection.main.head; - this.editorView.dispatch({ - changes: { from: cursor, insert: this.marker }, - annotations: [Transaction.addToHistory.of(false)] - }); - - const html = this.editorView.state.doc.toString(); - if (cursor !== null) { - this.editorView.dispatch({ - changes: { from: cursor, to: cursor + 1, insert: '' }, - annotations: [Transaction.addToHistory.of(false)] - }); - } - return html; + const doc = this.editorView.state.doc.toString(); + return doc.slice(0, cursor) + this.marker + doc.slice(cursor); } /** - * Attempts to insert the marker near a valid HTML element based on the - * current cursor position. Avoids disallowed contexts and falls back to - * safe parent containers if necessary. - * - * @returns {string} The document content with the marker temporarily inserted. - */ - getValueWithMarkerAtElement() { - let state = this.editorView.state; - const head = state.selection.main.head; + * Finds a safe insertion offset using a reliable linear tree traversal. + * + * This method traverses the tree from the beginning up to the cursor position, + * keeping track of the end position of the last "Text" node it encounters. + * + * This approach has proven to be the most robust and reliable, avoiding + * edge navigation issues that previously caused failures. + * + * @param {EditorState} state - The current state of CodeMirror. + * @param {number} head - The cursor position. + * @returns {number} A numeric offset guaranteed to be safe for text marker to be placed. + */ + findSafeInsertionOffset(state, head) { const tree = syntaxTree(state); - let currentNode = tree.resolve(head, -1); - const doc = state.doc; - - const cursor = currentNode.cursor(); - let pos = null; - let firstRun = true; - do { - const nodeName = cursor.name; - if (nodeName === "Text") { - pos = firstRun ? head : cursor.from; - break; - } - if (["EndTag", "SelfClosingTag"].includes(nodeName)) { - pos = cursor.to; - break; - } + // 1. Fast Path: If the cursor is already inside a Text node, the position is perfect. + const currentNode = tree.resolve(head, -1); + if (currentNode.name === 'Text') { + return head; + } - if (["StartTag", "StartCloseTag", "Comment"].includes(nodeName)) { - pos = cursor.from; - break; + // 2. Backward Search: Look for the last text node *before* the cursor. + let lastSeenTextEnd = -1; // Use -1 to indicate "not found" + tree.iterate({ + from: 0, + to: head, + enter: (node) => { + if (node.name === 'Text') { + lastSeenTextEnd = node.to; + } } + }); - if (nodeName === "Element" && !disallowedContexts.has(nodeName)) { - pos = cursor.from; - break; + // If we found a text node behind the cursor, that's our safe spot. + if (lastSeenTextEnd !== -1) { + return lastSeenTextEnd; + } + + // 3. Forward Search: If no text was found behind the cursor, search forward. + // This handles your exact case: Cell 1... + let firstSeenTextStart = -1; + tree.iterate({ + from: head, + enter: (node) => { + if (node.name === 'Text') { + if (firstSeenTextStart === -1) { // We only care about the *first* one we find + firstSeenTextStart = node.from; + } + } } + }); + + if (firstSeenTextStart !== -1) { + return firstSeenTextStart; + } + + // Final fallback: If there is no text anywhere in the document, return 0. + return 0; + } + - if (cursor.nextSibling() && cursor.name === "Element") { - pos = cursor.from; + /** + * Inserta un marcador HTML final utilizando un marcador de texto provisional + * y validando la estructura a través de la API del DOM. + */ + getHtmlWithHybridMarker(html, initialOffset) { + const DISALLOWED_PARENTS = new Set(["script", "style", "textarea", "title", "noscript", + "option", "optgroup", "select", + "svg", "math", "object", "iframe", + "head", "meta", "link", "base", "source", "track", "param", + "img", "input", "br", "hr", "col", "embed", "area", "wbr"]); + + // 1. Insert a text marker at the best position found so far + const htmlWithTextMarker = html.slice(0, initialOffset) + this.marker + html.slice(initialOffset); + + // 2. Parse the DOM + const parser = new DOMParser(); + const doc = parser.parseFromString(htmlWithTextMarker, 'text/html'); + const body = doc.body; + + // 3. Find where the text marker is placed + const walker = doc.createTreeWalker(body, NodeFilter.SHOW_TEXT); + let textNodeWithMarker = null; + while (walker.nextNode()) { + if (walker.currentNode.nodeValue.includes(this.marker)) { + textNodeWithMarker = walker.currentNode; break; } - cursor.prevSibling(); - firstRun = false; - } while (cursor.parent()); + } - if (pos == null) { - return doc.toString(); + if (!textNodeWithMarker) { + return html; // No marker found, so return the initial html. } - const { anyDisallowedFound, safeContainer } = this._getSafeRootedContainer(currentNode, doc); + // Divide the TextNode with the marker (to remove the textMarker) + const [nodeBefore, nodeAfter] = textNodeWithMarker.nodeValue.split(this.marker); + textNodeWithMarker.nodeValue = nodeBefore; + const newNodeAfter = document.createTextNode(nodeAfter); + textNodeWithMarker.parentElement.insertBefore(newNodeAfter, textNodeWithMarker.nextSibling); - if (anyDisallowedFound && safeContainer) { - pos = safeContainer.from; - } else if (anyDisallowedFound) { - return doc.toString(); - } + // 4. Validate the context and find the final insertion point for the SPAN marker. + let insertionPoint = newNodeAfter; + let parent = insertionPoint.parentElement; - this.editorView.dispatch({ - changes: { from: pos, to: pos, insert: this.marker }, - annotations: [Transaction.addToHistory.of(false)] - }); - state = this.editorView.state; + while (parent && parent !== body) { + if (DISALLOWED_PARENTS.has(parent.tagName?.toLowerCase())) { + insertionPoint = parent; // Move the insertion point before the forbidden element. + break; + } + parent = parent.parentElement; + } - const html = state.doc.toString(); + // 5. Create and insert the final HTML marker that TinyMCE will be able to understand. + const finalMarker = doc.createElement('span'); + finalMarker.classList.add(this.markerClass); + finalMarker.innerHTML = ' '; - this.editorView.dispatch({ - changes: { from: pos, to: pos + 1, insert: '' }, - annotations: [Transaction.addToHistory.of(false)] - }); - state = this.editorView.state; + insertionPoint.parentElement.insertBefore(finalMarker, insertionPoint); - return html; + // 6. Serialize back to HTML. + return body.innerHTML; } /** - * Traverses the syntax tree to find a parent node that is disallowed. - * - * @private - * @param {SyntaxNode} node - The starting syntax tree node. - * @param {Text} doc - The current document text. - * @returns {{ anyDisallowedFound: boolean, safeContainer: SyntaxNode | null }} Object with disallowed status and container node. + * Attempts to insert the marker near a valid HTML element based on the + * current cursor position. Avoids disallowed contexts and falls back to + * safe parent containers if necessary. + * + * @returns {string} The document content with the marker temporarily inserted. */ - _getSafeRootedContainer(node, doc) { - const cursor = node.cursor(); - let safeContainer = null; - let anyDisallowedFound = false; - do { - if (cursor.name === "Element") { - const tagName = getTagNameFromCursor(cursor.node, doc); - if (tagName) { - const name = tagName.toLowerCase(); - if (disallowedTags.has(name)) { - safeContainer = cursor.node; - anyDisallowedFound = true; - } - } - } - } while (cursor.parent()); + getValueWithMarkerAtElement() { + const state = this.editorView.state; + const head = state.selection.main.head; + const safeInitialOffset = this.findSafeInsertionOffset(state, head); + const html = state.doc.toString(); + + const finalHtml = this.getHtmlWithHybridMarker(html, safeInitialOffset); - return { anyDisallowedFound, safeContainer }; + return finalHtml; } /** @@ -232,4 +251,3 @@ export class CursorSync { } } - diff --git a/libs/codemirror/package.json b/libs/codemirror/package.json index 0822cd5..e9f3978 100644 --- a/libs/codemirror/package.json +++ b/libs/codemirror/package.json @@ -22,8 +22,8 @@ "@replit/codemirror-css-color-picker": "^6.3.0", "@replit/codemirror-indentation-markers": "^6.5.3", "@replit/codemirror-minimap": "^0.5.2", - "codemirror": "^6.0.1", - "htmlfy": "^0.7.5" + "codemirror": "^6.0.2", + "htmlfy": "^1.0.0" }, "devDependencies": { "@babel/core": "^7.27.3", diff --git a/libs/codemirror/treeutils.mjs b/libs/codemirror/treeutils.mjs deleted file mode 100644 index 78e95b1..0000000 --- a/libs/codemirror/treeutils.mjs +++ /dev/null @@ -1,103 +0,0 @@ -// This file is part of Moodle - http://moodle.org/ -// -// Moodle is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Moodle is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Moodle. If not, see . - -/** - * Tiny CodePro plugin. Thin wrapper around CodeMirror 6 - * - * @module tiny_codepro/plugin - * @copyright 2024 Josep Mulet Pol - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -/** - * Recursively finds the tag name from a syntax tree node. - * @param {Tree} node - * @param {Text} doc - * @returns {string|null} - */ -export function getTagNameFromCursor(node, doc) { - const cursor = node.cursor(); - function findTagName(c) { - if (c.name === "TagName") { - return doc.sliceString(c.from, c.to); - } - if (c.firstChild()) { - do { - const result = findTagName(c); - if (result) return result; - } while (c.nextSibling()); - c.parent(); - } - return null; - } - return findTagName(cursor); -} - -/** - * Debug utility that prints the full syntax tree. - * @param {EditorState} state - */ -export function printFullSyntaxTree(state) { - const doc = state.doc; - const tree = syntaxTree(state); - const cursor = tree.topNode.cursor(); - - function printNode(c, indent = 0) { - const padding = " ".repeat(indent); - const content = doc.sliceString(c.from, c.to).replace(/\n/g, "\\n"); - console.log(`${padding}${c.name} [${c.from}, ${c.to}]: "${content}"`); - - if (c.firstChild()) { - do { - printNode(c, indent + 1); - } while (c.nextSibling()); - c.parent(); - } - } - - printNode(cursor); -} - -/** - * Debug utility that prints the path from a node to the root with siblings. - * @param {Tree} node - * @param {Text} doc - */ -export function printTreePathToRoot(node, doc) { - const path = []; - const cursor = node.cursor(); - - do { - const siblings = []; - const siblingCursor = cursor.node.parent?.cursor(); - if (siblingCursor?.firstChild()) { - do { - const sFrom = siblingCursor.from; - const sTo = siblingCursor.to; - const sText = doc.sliceString(sFrom, sTo).replace(/\n/g, "\\n"); - const isCurrent = siblingCursor.from === cursor.from && siblingCursor.to === cursor.to; - siblings.push(`${isCurrent ? "\uD83D\uDC49 " : " "}${siblingCursor.name} [${sFrom}, ${sTo}]: "${sText}"`); - } while (siblingCursor.nextSibling()); - } - - path.push(siblings); - } while (cursor.parent()); - - console.log("Tree path from node to root with siblings:"); - path.forEach((siblings, level) => { - const indent = " ".repeat(level); - siblings.forEach(line => console.log(indent + line)); - }); -} diff --git a/libs/codemirror/viewmanager.test.js b/libs/codemirror/viewmanager.test.js index 1a01104..c2202dc 100644 --- a/libs/codemirror/viewmanager.test.js +++ b/libs/codemirror/viewmanager.test.js @@ -58,7 +58,8 @@ describe('ViewManager.create', () => { getRng: jest.fn(() => range), setRng: jest.fn(), setCursorLocation: jest.fn(), - collapse: jest.fn() + collapse: jest.fn(), + getNode: jest.fn(() => tinyContainer.querySelector('p')) }; mockTiny = { @@ -68,6 +69,7 @@ describe('ViewManager.create', () => { scrollY: 0, scrollTo: jest.fn() }, + getDoc: jest.fn(() => document), getWin: jest.fn(() => window), focus: jest.fn(), dom: { @@ -82,7 +84,11 @@ describe('ViewManager.create', () => { } return elem; }), - remove: jest.fn(() => {}) + remove: jest.fn(() => {}), + getParent: jest.fn().mockImplementation((currentNode, tags) => { + console.log(currentNode); + return currentNode.closest(tags); + }), }, getContent: jest.fn(() => tinyContainer.innerHTML), setContent: jest.fn((t) => tinyContainer.innerHTML = t), @@ -109,7 +115,8 @@ describe('ViewManager.create', () => { it('should create a CodeProEditor instance and attach it to the DOM', async () => { expect(cm6Container.querySelector('.cm-editor')).toBeNull(); // Before creation - await viewManager.attachCodeEditor(cm6Container); + const docHead = await viewManager.loadDocInfo(); + await viewManager.attachCodeEditor(cm6Container, docHead); expect(viewManager.codeEditor).toBeInstanceOf(CodeProEditor); expect(cm6Container.querySelector('.cm-editor')).not.toBeNull(); // Editor attached // expect that the head of the cmEditor to be in the right place diff --git a/settings.php b/settings.php index ad73cfe..6dac841 100644 --- a/settings.php +++ b/settings.php @@ -88,7 +88,7 @@ 'tiny_codepro/validchildren', new lang_string('validchildren', $pluginname), new lang_string('validchildren_def', $pluginname), - '+button[div|p|span|strong|em],+p[tiny-svg-block],+span[tiny-svg-block]', + '+body[script],+button[div|p|span|strong|em],+p[tiny-svg-block],+span[tiny-svg-block]', PARAM_TEXT )); @@ -96,7 +96,7 @@ 'tiny_codepro/customelements', new lang_string('customelements', $pluginname), new lang_string('customelements_def', $pluginname), - 'script,style,~svg,~tiny-svg-block', + 'script,~svg,~tiny-svg-block', PARAM_TEXT )); } diff --git a/thirdpartylibs.xml b/thirdpartylibs.xml index a4be0bc..541e48a 100644 --- a/thirdpartylibs.xml +++ b/thirdpartylibs.xml @@ -3,14 +3,14 @@ amd/src/cm6pro-lazy.js cm6pro - 6.0.1 + 6.0.2 MIT amd/src/htmlfy-lazy.js htmlfy - 0.6.0 + 1.0.0 MIT https://github.com/j4w8n/htmlfy @@ -18,7 +18,7 @@ libs/codemirror/ codemirror - 6.0.1 + 6.0.2 MIT https://github.com/codemirror/basic-setup diff --git a/upgrade.txt b/upgrade.txt index 4621a9c..408be89 100644 --- a/upgrade.txt +++ b/upgrade.txt @@ -49,9 +49,12 @@ Issue #27: Saving adds additional paragraphs/spans to html content == 2.1.1 == Automatically hide minimap on mobile phones -Safer cursor synchronization logic +Updated cursor synchronization logic. Admin setting for controlling the direction of synchronization +Enable the default code editor shipped by TinyMCE when capability is disabled Include CONTRIBUTING.md file == 2.1.2 == +Safer cursor synchronization logic Fix bug not autosaving in panel mode when form is submitted -New configuration property disableonpagesregex allows to disable the plugin on certain pages \ No newline at end of file +New configuration property disableonpagesregex allows to disable the plugin on certain pages +Updated library versions: htmlfy to 1.0.0 and codemirror to 6.0.2 diff --git a/version.php b/version.php index 53cbf32..2219f4a 100644 --- a/version.php +++ b/version.php @@ -28,4 +28,4 @@ $plugin->release = '2.1.2'; $plugin->requires = 2022112800; $plugin->maturity = MATURITY_STABLE; -$plugin->version = 2025062001; +$plugin->version = 2025080701;