Skip to content

Commit

Permalink
Merge pull request #1138 from basecamp/custom-html-block-attributes
Browse files Browse the repository at this point in the history
Allow custom HTML attributes in blocks
  • Loading branch information
afcapel committed Mar 27, 2024
2 parents c1ee6c3 + 54a8cd0 commit bf8f52a
Show file tree
Hide file tree
Showing 11 changed files with 82 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(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) {
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
14 changes: 11 additions & 3 deletions src/trix/views/block_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,27 @@ 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 bf8f52a

Please sign in to comment.