Skip to content

Commit

Permalink
Allow custom HTML attributes in blocks
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
afcapel committed Mar 8, 2024
1 parent 3f22606 commit 04b32e5
Show file tree
Hide file tree
Showing 11 changed files with 81 additions and 18 deletions.
4 changes: 4 additions & 0 deletions src/inspector/templates/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ window.JST["trix/inspector/templates/document"] = function() {
Attributes: ${JSON.stringify(block.attributes)}
</div>
<div class="htmlAttributes">
HTML Attributes: ${JSON.stringify(block.htmlAttributes)}
</div>
<div class="text">
<div class="title">
Text: ${text.id}, Pieces: ${pieces.length}, Length: ${text.getLength()}
Expand Down
10 changes: 8 additions & 2 deletions src/test/test_helpers/fixtures/fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -192,6 +192,12 @@ export const fixtures = {
html: `<pre>${blockComment}12\n3</pre>`,
},

"code with custom language": {
document: createDocument([ "puts \"Hello world!\"", {}, [ "code" ], { "language": "ruby" } ]),
html: `<pre language="ruby">${blockComment}puts "Hello world!"</pre>`,
serializedHTML: "<pre language=\"ruby\">puts \"Hello world!\"</pre>"
},

"multiple blocks with block comments in their text": {
document: createDocument([ `a${blockComment}b`, {}, [ "quote" ] ], [ `${blockComment}c`, {}, [ "code" ] ]),
html: `<blockquote>${blockComment}a&lt;!--block--&gt;b</blockquote><pre>${blockComment}&lt;!--block--&gt;c</pre>`,
Expand Down
2 changes: 1 addition & 1 deletion src/test/unit/html_parser_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/trix/config/block_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const attributes = {
code: {
tagName: "pre",
terminal: true,
htmlAttributes: [ "language" ],
text: {
plaintext: true,
},
Expand Down
18 changes: 13 additions & 5 deletions src/trix/models/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -27,19 +29,19 @@ 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() {
return this.copyWithText(null)
}

copyWithAttributes(attributes) {
return new Block(this.text, attributes)
return new Block(this.text, attributes, this.htmlAttributes)
}

copyWithoutAttributes() {
Expand All @@ -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)
Expand Down Expand Up @@ -173,6 +180,7 @@ export default class Block extends TrixObject {
return {
text: this.text,
attributes: this.attributes,
htmlAttributes: this.htmlAttributes,
}
}

Expand Down
10 changes: 10 additions & 0 deletions src/trix/models/composition.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/trix/models/document.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions src/trix/models/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
28 changes: 22 additions & 6 deletions src/trix/models/html_parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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
}
}
Expand All @@ -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")
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -350,6 +352,20 @@ export default class HTMLParser extends BasicObject {
return attributes.reverse()
}

getBlockHTMLAttributes(element) {
const attributes = {}
const blockConfig = Object.values(config.blockAttributes).find(c => c.tagName === tagName(element))

Check failure on line 357 in src/trix/models/html_parser.js

View workflow job for this annotation

GitHub Actions / Browser tests

Identifier name 'c' is too short (< 2)
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) {
Expand Down
2 changes: 1 addition & 1 deletion src/trix/models/html_sanitizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(" ")

Expand Down
13 changes: 10 additions & 3 deletions src/trix/views/block_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,26 @@ 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") {
const size = this.block.getBlockBreakPosition()
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 })
}

Expand Down

0 comments on commit 04b32e5

Please sign in to comment.