From d6314506287fd63cbc9f1b5aaee7c395956d134e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Fern=C3=A1ndez-Capel?= Date: Mon, 4 Mar 2024 10:51:54 +0000 Subject: [PATCH 1/2] Allow custom HTML attributes in blocks In particular, keeps the assigned language attribute assigned to code blocks. This is useful for syntax highlighting libraries that use the language attribute to determine the language of the code block. --- src/inspector/templates/document.js | 4 ++++ src/test/test_helpers/fixtures/fixtures.js | 10 ++++++-- src/test/unit/html_parser_test.js | 2 +- src/trix/config/block_attributes.js | 1 + src/trix/models/block.js | 18 ++++++++++---- src/trix/models/composition.js | 10 ++++++++ src/trix/models/document.js | 6 +++++ src/trix/models/editor.js | 5 ++++ src/trix/models/html_parser.js | 28 +++++++++++++++++----- src/trix/models/html_sanitizer.js | 2 +- src/trix/views/block_view.js | 13 +++++++--- 11 files changed, 81 insertions(+), 18 deletions(-) diff --git a/src/inspector/templates/document.js b/src/inspector/templates/document.js index e74ffcde7..d9ce0b3b1 100644 --- a/src/inspector/templates/document.js +++ b/src/inspector/templates/document.js @@ -13,6 +13,10 @@ window.JST["trix/inspector/templates/document"] = function() { Attributes: ${JSON.stringify(block.attributes)} +
+ HTML Attributes: ${JSON.stringify(block.htmlAttributes)} +
+
Text: ${text.id}, Pieces: ${pieces.length}, Length: ${text.getLength()} diff --git a/src/test/test_helpers/fixtures/fixtures.js b/src/test/test_helpers/fixtures/fixtures.js index dc3f999c2..9224889af 100644 --- a/src/test/test_helpers/fixtures/fixtures.js +++ b/src/test/test_helpers/fixtures/fixtures.js @@ -41,9 +41,9 @@ const { css } = config const createDocument = function (...parts) { const blocks = parts.map((part) => { - const [ string, textAttributes, blockAttributes ] = Array.from(part) + const [ string, textAttributes, blockAttributes, htmlAttributes = {} ] = Array.from(part) const text = Text.textForStringWithAttributes(string, textAttributes) - return new Block(text, blockAttributes) + return new Block(text, blockAttributes, htmlAttributes) }) return new Document(blocks) @@ -192,6 +192,12 @@ export const fixtures = { html: `
${blockComment}12\n3
`, }, + "code with custom language": { + document: createDocument([ "puts \"Hello world!\"", {}, [ "code" ], { "language": "ruby" } ]), + html: `
${blockComment}puts "Hello world!"
`, + serializedHTML: "
puts \"Hello world!\"
" + }, + "multiple blocks with block comments in their text": { document: createDocument([ `a${blockComment}b`, {}, [ "quote" ] ], [ `${blockComment}c`, {}, [ "code" ] ]), html: `
${blockComment}a<!--block-->b
${blockComment}<!--block-->c
`, diff --git a/src/test/unit/html_parser_test.js b/src/test/unit/html_parser_test.js index a970ef47d..836186c5b 100644 --- a/src/test/unit/html_parser_test.js +++ b/src/test/unit/html_parser_test.js @@ -17,7 +17,7 @@ const cursorTargetLeft = createCursorTarget("left").outerHTML const cursorTargetRight = createCursorTarget("right").outerHTML testGroup("HTMLParser", () => { - eachFixture((name, { html, serializedHTML, document }) => { + eachFixture((name, { html, document }) => { test(name, () => { const parsedDocument = HTMLParser.parse(html).getDocument() assert.documentHTMLEqual(parsedDocument.copyUsingObjectsFromDocument(document), html) diff --git a/src/trix/config/block_attributes.js b/src/trix/config/block_attributes.js index 2c572699f..75e025b42 100644 --- a/src/trix/config/block_attributes.js +++ b/src/trix/config/block_attributes.js @@ -16,6 +16,7 @@ const attributes = { code: { tagName: "pre", terminal: true, + htmlAttributes: [ "language" ], text: { plaintext: true, }, diff --git a/src/trix/models/block.js b/src/trix/models/block.js index a822605a0..36d498925 100644 --- a/src/trix/models/block.js +++ b/src/trix/models/block.js @@ -5,19 +5,21 @@ import { arraysAreEqual, getBlockConfig, getListAttributeNames, + objectsAreEqual, spliceArray, } from "trix/core/helpers" export default class Block extends TrixObject { static fromJSON(blockJSON) { const text = Text.fromJSON(blockJSON.text) - return new this(text, blockJSON.attributes) + return new this(text, blockJSON.attributes, blockJSON.htmlAttributes) } - constructor(text, attributes) { + constructor(text, attributes, htmlAttributes) { super(...arguments) this.text = applyBlockBreakToText(text || new Text()) this.attributes = attributes || [] + this.htmlAttributes = htmlAttributes || {} } isEmpty() { @@ -27,11 +29,11 @@ export default class Block extends TrixObject { isEqualTo(block) { if (super.isEqualTo(block)) return true - return this.text.isEqualTo(block?.text) && arraysAreEqual(this.attributes, block?.attributes) + return this.text.isEqualTo(block?.text) && arraysAreEqual(this.attributes, block?.attributes) && objectsAreEqual(this.htmlAttributes, block?.htmlAttributes) } copyWithText(text) { - return new Block(text, this.attributes) + return new Block(text, this.attributes, this.htmlAttributes) } copyWithoutText() { @@ -39,7 +41,7 @@ export default class Block extends TrixObject { } copyWithAttributes(attributes) { - return new Block(this.text, attributes) + return new Block(this.text, attributes, this.htmlAttributes) } copyWithoutAttributes() { @@ -60,6 +62,11 @@ export default class Block extends TrixObject { return this.copyWithAttributes(attributes) } + addHTMLAttribute(attribute, value) { + const htmlAttributes = Object.assign({}, this.htmlAttributes, { [attribute]: value }) + return new Block(this.text, this.attributes, htmlAttributes) + } + removeAttribute(attribute) { const { listAttribute } = getBlockConfig(attribute) const attributes = removeLastValue(removeLastValue(this.attributes, attribute), listAttribute) @@ -173,6 +180,7 @@ export default class Block extends TrixObject { return { text: this.text, attributes: this.attributes, + htmlAttributes: this.htmlAttributes, } } diff --git a/src/trix/models/composition.js b/src/trix/models/composition.js index 5454a1f51..0438d3b12 100644 --- a/src/trix/models/composition.js +++ b/src/trix/models/composition.js @@ -341,6 +341,16 @@ export default class Composition extends BasicObject { } } + setHTMLAtributeAtPosition(position, attributeName, value) { + const block = this.document.getBlockAtPosition(position) + const allowedHTMLAttributes = getBlockConfig(block.getLastAttribute())?.htmlAttributes + + if (block && allowedHTMLAttributes?.includes(attributeName)) { + const newDocument = this.document.setHTMLAttributeAtPosition(position, attributeName, value) + this.setDocument(newDocument) + } + } + setTextAttribute(attributeName, value) { const selectedRange = this.getSelectedRange() if (!selectedRange) return diff --git a/src/trix/models/document.js b/src/trix/models/document.js index c8bf6396e..957045423 100644 --- a/src/trix/models/document.js +++ b/src/trix/models/document.js @@ -282,6 +282,12 @@ export default class Document extends TrixObject { return this.removeAttributeAtRange(attribute, range) } + setHTMLAttributeAtPosition(position, name, value) { + const block = this.getBlockAtPosition(position) + const updatedBlock = block.addHTMLAttribute(name, value) + return this.replaceBlock(block, updatedBlock) + } + insertBlockBreakAtRange(range) { let blocks range = normalizeRange(range) diff --git a/src/trix/models/editor.js b/src/trix/models/editor.js index 367940d60..8092b1052 100644 --- a/src/trix/models/editor.js +++ b/src/trix/models/editor.js @@ -137,6 +137,11 @@ export default class Editor { return this.composition.removeCurrentAttribute(name) } + // HTML attributes + setHTMLAtributeAtPosition(position, name, value) { + this.composition.setHTMLAtributeAtPosition(position, name, value) + } + // Nesting level canDecreaseNestingLevel() { diff --git a/src/trix/models/html_parser.js b/src/trix/models/html_parser.js index 5dc065a10..c09b4a1b8 100644 --- a/src/trix/models/html_parser.js +++ b/src/trix/models/html_parser.js @@ -33,9 +33,9 @@ const pieceForAttachment = (attachment, attributes = {}) => { return { attachment, attributes, type } } -const blockForAttributes = (attributes = {}) => { +const blockForAttributes = (attributes = {}, htmlAttributes = {}) => { const text = [] - return { text, attributes } + return { text, attributes, htmlAttributes } } const parseTrixDataAttribute = (element, name) => { @@ -133,8 +133,9 @@ export default class HTMLParser extends BasicObject { return this.appendStringWithAttributes("\n") } else if (element === this.containerElement || this.isBlockElement(element)) { const attributes = this.getBlockAttributes(element) + const htmlAttributes = this.getBlockHTMLAttributes(element) if (!arraysAreEqual(attributes, this.currentBlock?.attributes)) { - this.currentBlock = this.appendBlockForAttributesWithElement(attributes, element) + this.currentBlock = this.appendBlockForAttributesWithElement(attributes, element, htmlAttributes) this.currentBlockElement = element } } @@ -147,9 +148,10 @@ export default class HTMLParser extends BasicObject { if (elementIsBlockElement && !this.isBlockElement(element.firstChild)) { if (!this.isInsignificantTextNode(element.firstChild) || !this.isBlockElement(element.firstElementChild)) { const attributes = this.getBlockAttributes(element) + const htmlAttributes = this.getBlockHTMLAttributes(element) if (element.firstChild) { if (!(currentBlockContainsElement && arraysAreEqual(attributes, this.currentBlock.attributes))) { - this.currentBlock = this.appendBlockForAttributesWithElement(attributes, element) + this.currentBlock = this.appendBlockForAttributesWithElement(attributes, element, htmlAttributes) this.currentBlockElement = element } else { return this.appendStringWithAttributes("\n") @@ -233,9 +235,9 @@ export default class HTMLParser extends BasicObject { // Document construction - appendBlockForAttributesWithElement(attributes, element) { + appendBlockForAttributesWithElement(attributes, element, htmlAttributes = {}) { this.blockElements.push(element) - const block = blockForAttributes(attributes) + const block = blockForAttributes(attributes, htmlAttributes) this.blocks.push(block) return block } @@ -350,6 +352,20 @@ export default class HTMLParser extends BasicObject { return attributes.reverse() } + getBlockHTMLAttributes(element) { + const attributes = {} + const blockConfig = Object.values(config.blockAttributes).find(settings => settings.tagName === tagName(element)) + const allowedAttributes = blockConfig?.htmlAttributes || [] + + allowedAttributes.forEach((attribute) => { + if (element.hasAttribute(attribute)) { + attributes[attribute] = element.getAttribute(attribute) + } + }) + + return attributes + } + findBlockElementAncestors(element) { const ancestors = [] while (element && element !== this.containerElement) { diff --git a/src/trix/models/html_sanitizer.js b/src/trix/models/html_sanitizer.js index 6e342e51a..0782bd7b9 100644 --- a/src/trix/models/html_sanitizer.js +++ b/src/trix/models/html_sanitizer.js @@ -2,7 +2,7 @@ import BasicObject from "trix/core/basic_object" import { nodeIsAttachmentElement, removeNode, tagName, walkTree } from "trix/core/helpers" -const DEFAULT_ALLOWED_ATTRIBUTES = "style href src width height class".split(" ") +const DEFAULT_ALLOWED_ATTRIBUTES = "style href src width height language class".split(" ") const DEFAULT_FORBIDDEN_PROTOCOLS = "javascript:".split(" ") const DEFAULT_FORBIDDEN_ELEMENTS = "script iframe form".split(" ") diff --git a/src/trix/views/block_view.js b/src/trix/views/block_view.js index 158d9f6b9..fe3fbae63 100644 --- a/src/trix/views/block_view.js +++ b/src/trix/views/block_view.js @@ -42,12 +42,13 @@ export default class BlockView extends ObjectView { } createContainerElement(depth) { - let attributes, className + const attributes = {} + let className const attributeName = this.attributes[depth] - const { tagName } = getBlockConfig(attributeName) + const { tagName, htmlAttributes } = getBlockConfig(attributeName) if (depth === 0 && this.block.isRTL()) { - attributes = { dir: "rtl" } + Object.assign(attributes, { dir: "rtl" }) } if (attributeName === "attachmentGallery") { @@ -55,6 +56,12 @@ export default class BlockView extends ObjectView { className = `${css.attachmentGallery} ${css.attachmentGallery}--${size}` } + Object.entries(this.block.htmlAttributes).forEach(([ name, value ]) => { + if (htmlAttributes.includes(name)) { + attributes[name] = value + } + }) + return makeElement({ tagName, className, attributes }) } From 54a8cd0c3f72832fec48a77427b58a3ea8167041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alberto=20Fern=C3=A1ndez-Capel?= Date: Tue, 12 Mar 2024 14:14:00 +0000 Subject: [PATCH 2/2] Default to empty HTML attributes --- src/trix/views/block_view.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/trix/views/block_view.js b/src/trix/views/block_view.js index fe3fbae63..c3d2600ea 100644 --- a/src/trix/views/block_view.js +++ b/src/trix/views/block_view.js @@ -46,7 +46,8 @@ export default class BlockView extends ObjectView { let className const attributeName = this.attributes[depth] - const { tagName, htmlAttributes } = getBlockConfig(attributeName) + const { tagName, htmlAttributes = [] } = getBlockConfig(attributeName) + if (depth === 0 && this.block.isRTL()) { Object.assign(attributes, { dir: "rtl" }) }