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..c3d2600ea 100644 --- a/src/trix/views/block_view.js +++ b/src/trix/views/block_view.js @@ -42,12 +42,14 @@ 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 +57,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 }) }