diff --git a/cjs/dom/parser.js b/cjs/dom/parser.js index eb86f18a..d01896fc 100644 --- a/cjs/dom/parser.js +++ b/cjs/dom/parser.js @@ -5,13 +5,14 @@ const {parseFromString} = require('../shared/parse-from-string.js'); const {HTMLDocument} = require('../html/document.js'); const {SVGDocument} = require('../svg/document.js'); const {XMLDocument} = require('../xml/document.js'); +const {JSXDocument} = require('../jsx/document.js'); /** * @implements globalThis.DOMParser */ class DOMParser { - /** @typedef {{ "text/html": HTMLDocument, "image/svg+xml": SVGDocument, "text/xml": XMLDocument }} MimeToDoc */ + /** @typedef {{ "text/html": HTMLDocument, "image/svg+xml": SVGDocument, "text/xml": XMLDocument, "text/jsx+xml": JSXDocument }} MimeToDoc */ /** * @template {keyof MimeToDoc} MIME * @param {string} markupLanguage @@ -26,6 +27,10 @@ class DOMParser { } else if (mimeType === 'image/svg+xml') document = new SVGDocument; + else if (mimeType === 'text/jsx+xml') { + document = new JSXDocument; + isHTML = true; + } else document = new XMLDocument; document[DOM_PARSER] = DOMParser; diff --git a/cjs/interface/attr.js b/cjs/interface/attr.js index eabb8028..11510934 100644 --- a/cjs/interface/attr.js +++ b/cjs/interface/attr.js @@ -1,6 +1,6 @@ 'use strict'; const {ATTRIBUTE_NODE} = require('../shared/constants.js'); -const {CHANGED, VALUE} = require('../shared/symbols.js'); +const {CHANGED, VALUE, MIME} = require('../shared/symbols.js'); const {String} = require('../shared/utils.js'); const {attrAsJSON} = require('../shared/jsdon.js'); const {emptyAttributes} = require('../shared/attributes.js'); @@ -41,9 +41,10 @@ class Attr extends Node { } toString() { - const {name, [VALUE]: value} = this; + const {ownerDocument, name, [VALUE]: value} = this; + const doubleQuote = ownerDocument[MIME].unquotedJsonAttributes && /^\{(.[\s\S]?)+\}$/.test(value) ? '' : '"' return emptyAttributes.has(name) && !value ? - name : `${name}="${value.replace(QUOTE, '"')}"`; + name : `${name}=${doubleQuote}${value.replace(QUOTE, doubleQuote ? '"' : '"')}${doubleQuote}`; } toJSON() { diff --git a/cjs/interface/comment.js b/cjs/interface/comment.js index fe8abe95..b5d49359 100644 --- a/cjs/interface/comment.js +++ b/cjs/interface/comment.js @@ -1,6 +1,6 @@ 'use strict'; const {COMMENT_NODE} = require('../shared/constants.js'); -const {VALUE} = require('../shared/symbols.js'); +const {VALUE, MIME} = require('../shared/symbols.js'); const {escape} = require('../shared/text-escaper.js'); const {CharacterData} = require('./character-data.js'); @@ -18,6 +18,12 @@ class Comment extends CharacterData { return new Comment(ownerDocument, data); } - toString() { return ``; } + toString() { + const {ownerDocument} = this; + if (ownerDocument[MIME].escapeHtmlEntities) { + return ``; + } + return ``; + } } -exports.Comment = Comment +exports.Comment = Comment \ No newline at end of file diff --git a/cjs/interface/text.js b/cjs/interface/text.js index 241511a1..e7043b23 100644 --- a/cjs/interface/text.js +++ b/cjs/interface/text.js @@ -1,6 +1,6 @@ 'use strict'; const {TEXT_NODE} = require('../shared/constants.js'); -const {VALUE} = require('../shared/symbols.js'); +const {VALUE, MIME} = require('../shared/symbols.js'); const {escape} = require('../shared/text-escaper.js'); const {CharacterData} = require('./character-data.js'); @@ -39,6 +39,12 @@ class Text extends CharacterData { return new Text(ownerDocument, data); } - toString() { return escape(this[VALUE]); } + toString() { + const {ownerDocument} = this; + if (ownerDocument[MIME].escapeHtmlEntities) { + return escape(this[VALUE]); + } + return this[VALUE]; + } } -exports.Text = Text +exports.Text = Text \ No newline at end of file diff --git a/cjs/jsx/document.js b/cjs/jsx/document.js new file mode 100644 index 00000000..b9964d0e --- /dev/null +++ b/cjs/jsx/document.js @@ -0,0 +1,10 @@ +'use strict'; +const {Document} = require('../interface/document.js'); + +/** + * @implements globalThis.JSXDocument + */ +class JSXDocument extends Document { + constructor() { super('text/jsx+xml'); } +} +exports.JSXDocument = JSXDocument diff --git a/cjs/shared/mime.js b/cjs/shared/mime.js index 1bb849d1..c1a137be 100644 --- a/cjs/shared/mime.js +++ b/cjs/shared/mime.js @@ -7,26 +7,38 @@ const Mime = { 'text/html': { docType: '', ignoreCase: true, + escapeHtmlEntities: true, voidElements: /^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i }, 'image/svg+xml': { docType: '', ignoreCase: false, + escapeHtmlEntities: true, voidElements }, 'text/xml': { docType: '', ignoreCase: false, + escapeHtmlEntities: true, voidElements }, 'application/xml': { docType: '', ignoreCase: false, + escapeHtmlEntities: true, voidElements }, 'application/xhtml+xml': { docType: '', ignoreCase: false, + escapeHtmlEntities: true, + voidElements + }, + 'text/jsx+xml': { + docType: '', + ignoreCase: false, + escapeHtmlEntities: false, + unquotedJsonAttributes: true, voidElements } }; diff --git a/esm/dom/parser.js b/esm/dom/parser.js index cb6fd5e4..65de9d49 100644 --- a/esm/dom/parser.js +++ b/esm/dom/parser.js @@ -4,13 +4,14 @@ import {parseFromString} from '../shared/parse-from-string.js'; import {HTMLDocument} from '../html/document.js'; import {SVGDocument} from '../svg/document.js'; import {XMLDocument} from '../xml/document.js'; +import {JSXDocument} from '../jsx/document.js'; /** * @implements globalThis.DOMParser */ export class DOMParser { - /** @typedef {{ "text/html": HTMLDocument, "image/svg+xml": SVGDocument, "text/xml": XMLDocument }} MimeToDoc */ + /** @typedef {{ "text/html": HTMLDocument, "image/svg+xml": SVGDocument, "text/xml": XMLDocument, "text/jsx+xml": JSXDocument }} MimeToDoc */ /** * @template {keyof MimeToDoc} MIME * @param {string} markupLanguage @@ -25,6 +26,10 @@ export class DOMParser { } else if (mimeType === 'image/svg+xml') document = new SVGDocument; + else if (mimeType === 'text/jsx+xml') { + document = new JSXDocument; + isHTML = true; + } else document = new XMLDocument; document[DOM_PARSER] = DOMParser; diff --git a/esm/interface/attr.js b/esm/interface/attr.js index 4cf1cc7d..7ba87b85 100644 --- a/esm/interface/attr.js +++ b/esm/interface/attr.js @@ -1,5 +1,5 @@ import {ATTRIBUTE_NODE} from '../shared/constants.js'; -import {CHANGED, VALUE} from '../shared/symbols.js'; +import {CHANGED, VALUE, MIME} from '../shared/symbols.js'; import {String} from '../shared/utils.js'; import {attrAsJSON} from '../shared/jsdon.js'; import {emptyAttributes} from '../shared/attributes.js'; @@ -40,9 +40,10 @@ export class Attr extends Node { } toString() { - const {name, [VALUE]: value} = this; + const {ownerDocument, name, [VALUE]: value} = this; + const doubleQuote = ownerDocument[MIME].unquotedJsonAttributes && /^\{(.[\s\S]?)+\}$/.test(value) ? '' : '"' return emptyAttributes.has(name) && !value ? - name : `${name}="${value.replace(QUOTE, '"')}"`; + name : `${name}=${doubleQuote}${value.replace(QUOTE, doubleQuote ? '"' : '"')}${doubleQuote}`; } toJSON() { diff --git a/esm/interface/comment.js b/esm/interface/comment.js index 87092e6e..90b067c7 100644 --- a/esm/interface/comment.js +++ b/esm/interface/comment.js @@ -1,5 +1,5 @@ import {COMMENT_NODE} from '../shared/constants.js'; -import {VALUE} from '../shared/symbols.js'; +import {VALUE, MIME} from '../shared/symbols.js'; import {escape} from '../shared/text-escaper.js'; import {CharacterData} from './character-data.js'; @@ -17,5 +17,11 @@ export class Comment extends CharacterData { return new Comment(ownerDocument, data); } - toString() { return ``; } -} + toString() { + const {ownerDocument} = this; + if (ownerDocument[MIME].escapeHtmlEntities) { + return ``; + } + return ``; + } +} \ No newline at end of file diff --git a/esm/interface/text.js b/esm/interface/text.js index a07a8f3a..6d1119ba 100644 --- a/esm/interface/text.js +++ b/esm/interface/text.js @@ -1,5 +1,5 @@ import {TEXT_NODE} from '../shared/constants.js'; -import {VALUE} from '../shared/symbols.js'; +import {VALUE, MIME} from '../shared/symbols.js'; import {escape} from '../shared/text-escaper.js'; import {CharacterData} from './character-data.js'; @@ -38,5 +38,11 @@ export class Text extends CharacterData { return new Text(ownerDocument, data); } - toString() { return escape(this[VALUE]); } -} + toString() { + const {ownerDocument} = this; + if (ownerDocument[MIME].escapeHtmlEntities) { + return escape(this[VALUE]); + } + return this[VALUE]; + } +} \ No newline at end of file diff --git a/esm/jsx/document.js b/esm/jsx/document.js new file mode 100644 index 00000000..6e11b582 --- /dev/null +++ b/esm/jsx/document.js @@ -0,0 +1,8 @@ +import {Document} from '../interface/document.js'; + +/** + * @implements globalThis.JSXDocument + */ +export class JSXDocument extends Document { + constructor() { super('text/jsx+xml'); } +} diff --git a/esm/shared/mime.js b/esm/shared/mime.js index 4390efe8..4ec36b8a 100644 --- a/esm/shared/mime.js +++ b/esm/shared/mime.js @@ -6,26 +6,38 @@ export const Mime = { 'text/html': { docType: '', ignoreCase: true, + escapeHtmlEntities: true, voidElements: /^(?:area|base|br|col|embed|hr|img|input|keygen|link|menuitem|meta|param|source|track|wbr)$/i }, 'image/svg+xml': { docType: '', ignoreCase: false, + escapeHtmlEntities: true, voidElements }, 'text/xml': { docType: '', ignoreCase: false, + escapeHtmlEntities: true, voidElements }, 'application/xml': { docType: '', ignoreCase: false, + escapeHtmlEntities: true, voidElements }, 'application/xhtml+xml': { docType: '', ignoreCase: false, + escapeHtmlEntities: true, + voidElements + }, + 'text/jsx+xml': { + docType: '', + ignoreCase: false, + escapeHtmlEntities: false, + unquotedJsonAttributes: true, voidElements } }; diff --git a/package.json b/package.json index 9df08cd5..4d424ec2 100644 --- a/package.json +++ b/package.json @@ -69,4 +69,4 @@ "url": "https://github.com/WebReflection/linkedom/issues" }, "homepage": "https://github.com/WebReflection/linkedom#readme" -} +} \ No newline at end of file diff --git a/test/index.js b/test/index.js index 68daaf61..30388c68 100644 --- a/test/index.js +++ b/test/index.js @@ -24,6 +24,7 @@ const test = folder => getFiles(folder).then(files => { console.log(`\x1b[7m\x1b[1m ${'LinkeDOM'.padEnd(74)}\x1b[0m`); test('xml') +.then(() => test('jsx')) .then(() => test('svg')) .then(() => test('html')) .then(() => test('interface')) @@ -33,6 +34,7 @@ test('xml') console.log(`\x1b[7m\x1b[1m ${'LinkeDOM - Cached'.padEnd(74)}\x1b[0m`); global[Symbol.for('linkedom')] = require('../cjs/cached.js'); test('xml') + .then(() => test('jsx')) .then(() => test('svg')) .then(() => test('html')) .then(() => test('interface')) diff --git a/test/interface/text.js b/test/interface/text.js index 8e1891a8..eadf3387 100644 --- a/test/interface/text.js +++ b/test/interface/text.js @@ -1,6 +1,6 @@ const assert = require('../assert.js').for('Text'); -const {parseHTML} = global[Symbol.for('linkedom')]; +const {parseHTML, DOMParser} = global[Symbol.for('linkedom')]; const {document} = parseHTML('
'); @@ -67,3 +67,9 @@ assert(node.childNodes.length, 1, 'normalize() empty text'); assert(text.nodeValue, 'text'); text.nodeValue = ''; assert(text.nodeValue, ''); +const jsxDocument = (new DOMParser).parseFromString('');
+
+assert(document.documentElement.tagName, 'html');
+assert(document.documentElement.nodeName, 'html');
+
+document.documentElement.innerHTML = `
+
`, 'text/jsx+xml') + +// internally creates a html -> html -> ... structure because +// documentElement cannot be replaced +documentFullRerender.documentElement.innerHTML = ` +
+
+
+
+
+
+