Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow custom HTML attributes in blocks #1138

Merged
merged 2 commits into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading