From ef08bf8ad64ca389ca2603d2f002fcf40a2733b8 Mon Sep 17 00:00:00 2001 From: Aron Homberg Date: Sun, 14 Nov 2021 11:41:37 +0100 Subject: [PATCH 1/2] feat: implemented JSX support as text/jsx+xml --- cjs/dom/parser.js | 7 +++++- cjs/interface/attr.js | 7 +++--- cjs/interface/comment.js | 12 ++++++++--- cjs/interface/text.js | 12 ++++++++--- cjs/jsx/document.js | 10 +++++++++ cjs/shared/mime.js | 12 +++++++++++ esm/dom/parser.js | 7 +++++- esm/interface/attr.js | 7 +++--- esm/interface/comment.js | 12 ++++++++--- esm/interface/text.js | 12 ++++++++--- esm/jsx/document.js | 8 +++++++ esm/shared/mime.js | 12 +++++++++++ package.json | 2 +- test/index.js | 2 ++ test/interface/text.js | 8 ++++++- test/jsx/document.js | 22 +++++++++++++++++++ types/dom/parser.d.ts | 5 ++++- types/jsx/document.d.ts | 7 ++++++ types/shared/mime.d.ts | 14 ++++++++++++ worker.js | 46 +++++++++++++++++++++++++++++++++++----- 20 files changed, 196 insertions(+), 28 deletions(-) create mode 100644 cjs/jsx/document.js create mode 100644 esm/jsx/document.js create mode 100644 test/jsx/document.js create mode 100644 types/jsx/document.d.ts 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..9a59344e 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}`; } 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..df1a0656 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}`; } 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('
<>
{eles.map(ele => (
))}', 'text/jsx+xml'); +assert( + jsxDocument.getRootNode().toString(), + '
<>
{eles.map(ele => (
))}', + 'Must resemble JSX original form, no escaping, allow for JSON attributes' +); diff --git a/test/jsx/document.js b/test/jsx/document.js new file mode 100644 index 00000000..8d3ffa1e --- /dev/null +++ b/test/jsx/document.js @@ -0,0 +1,22 @@ +const assert = require('../assert.js').for('XMLDocument'); + +const {DOMParser} = global[Symbol.for('linkedom')]; + +const document = (new DOMParser).parseFromString('', 'text/jsx+xml'); + +assert(document.toString(), ''); + +assert(document.documentElement.tagName, 'root'); +assert(document.documentElement.nodeName, 'root'); + +document.documentElement.innerHTML = ` + + { bar } + Text + +`.trim(); + +assert(document.querySelectorAll('Element').length, 2, 'case senstivive 2'); +assert(document.querySelectorAll('element').length, 0, 'case senstivive 0'); + +assert(document.querySelector('Element').attributes.someAttribute.toString(), 'someAttribute={foo}', 'JSX must allow unquoted JSON attributes') diff --git a/types/dom/parser.d.ts b/types/dom/parser.d.ts index 84374205..d7faabb2 100644 --- a/types/dom/parser.d.ts +++ b/types/dom/parser.d.ts @@ -2,7 +2,7 @@ * @implements globalThis.DOMParser */ export class DOMParser implements globalThis.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 @@ -13,12 +13,15 @@ export class DOMParser implements globalThis.DOMParser { "text/html": HTMLDocument; "image/svg+xml": SVGDocument; "text/xml": XMLDocument; + "text/jsx+xml": JSXDocument; }>(markupLanguage: string, mimeType: MIME): { "text/html": HTMLDocument; "image/svg+xml": SVGDocument; "text/xml": XMLDocument; + "text/jsx+xml": JSXDocument; }[MIME]; } import { HTMLDocument } from "../html/document.js"; import { SVGDocument } from "../svg/document.js"; import { XMLDocument } from "../xml/document.js"; +import { JSXDocument } from "../jsx/document.js"; diff --git a/types/jsx/document.d.ts b/types/jsx/document.d.ts new file mode 100644 index 00000000..9362cca7 --- /dev/null +++ b/types/jsx/document.d.ts @@ -0,0 +1,7 @@ +/** + * @implements globalThis.JSXDocument + */ +export class JSXDocument extends Document implements globalThis.JSXDocument { + constructor(); +} +import { Document } from "../interface/document.js"; diff --git a/types/shared/mime.d.ts b/types/shared/mime.d.ts index fa7d537f..2e1857e2 100644 --- a/types/shared/mime.d.ts +++ b/types/shared/mime.d.ts @@ -2,11 +2,13 @@ export const Mime: { 'text/html': { docType: string; ignoreCase: boolean; + escapeHtmlEntities: boolean; voidElements: RegExp; }; 'image/svg+xml': { docType: string; ignoreCase: boolean; + escapeHtmlEntities: boolean; voidElements: { test: () => boolean; }; @@ -14,6 +16,7 @@ export const Mime: { 'text/xml': { docType: string; ignoreCase: boolean; + escapeHtmlEntities: boolean; voidElements: { test: () => boolean; }; @@ -21,6 +24,7 @@ export const Mime: { 'application/xml': { docType: string; ignoreCase: boolean; + escapeHtmlEntities: boolean; voidElements: { test: () => boolean; }; @@ -28,6 +32,16 @@ export const Mime: { 'application/xhtml+xml': { docType: string; ignoreCase: boolean; + escapeHtmlEntities: boolean; + voidElements: { + test: () => boolean; + }; + }; + 'text/jsx+xml': { + docType: string; + ignoreCase: boolean; + escapeHtmlEntities: boolean; + unquotedJsonAttributes: boolean; voidElements: { test: () => boolean; }; diff --git a/worker.js b/worker.js index 07232cc5..874cae67 100644 --- a/worker.js +++ b/worker.js @@ -9440,9 +9440,10 @@ class Attr$1 extends Node$1 { } 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}`; } toJSON() { @@ -9666,7 +9667,13 @@ class Comment$1 extends CharacterData$1 { return new Comment$1(ownerDocument, data); } - toString() { return ``; } + toString() { + const {ownerDocument} = this; + if (ownerDocument[MIME].escapeHtmlEntities) { + return ``; + } + return ``; + } } var lib$2 = {}; @@ -11654,7 +11661,13 @@ class Text$1 extends CharacterData$1 { return new Text$1(ownerDocument, data); } - toString() { return escape(this[VALUE]); } + toString() { + const {ownerDocument} = this; + if (ownerDocument[MIME].escapeHtmlEntities) { + return escape(this[VALUE]); + } + return this[VALUE]; + } } // https://dom.spec.whatwg.org/#interface-parentnode @@ -16182,26 +16195,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 } }; @@ -16762,12 +16787,19 @@ class XMLDocument extends Document$1 { } } +/** + * @implements globalThis.JSXDocument + */ +class JSXDocument extends Document$1 { + constructor() { super('text/jsx+xml'); } +} + /** * @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 @@ -16782,6 +16814,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; From 38f63226e2f431fb4d42a9a1c7f484da67b357e9 Mon Sep 17 00:00:00 2001 From: Aron Homberg Date: Sun, 14 Nov 2021 15:45:54 +0100 Subject: [PATCH 2/2] fix: attribute inline nodeValue quotation; added test --- cjs/interface/attr.js | 2 +- esm/interface/attr.js | 2 +- test/jsx/document.js | 35 +++++++++++++++++++++++++++++++---- worker.js | 2 +- 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/cjs/interface/attr.js b/cjs/interface/attr.js index 9a59344e..11510934 100644 --- a/cjs/interface/attr.js +++ b/cjs/interface/attr.js @@ -44,7 +44,7 @@ class Attr extends Node { const {ownerDocument, name, [VALUE]: value} = this; const doubleQuote = ownerDocument[MIME].unquotedJsonAttributes && /^\{(.[\s\S]?)+\}$/.test(value) ? '' : '"' return emptyAttributes.has(name) && !value ? - name : `${name}=${doubleQuote}${value.replace(QUOTE, '"')}${doubleQuote}`; + name : `${name}=${doubleQuote}${value.replace(QUOTE, doubleQuote ? '"' : '"')}${doubleQuote}`; } toJSON() { diff --git a/esm/interface/attr.js b/esm/interface/attr.js index df1a0656..7ba87b85 100644 --- a/esm/interface/attr.js +++ b/esm/interface/attr.js @@ -43,7 +43,7 @@ export class Attr extends Node { const {ownerDocument, name, [VALUE]: value} = this; const doubleQuote = ownerDocument[MIME].unquotedJsonAttributes && /^\{(.[\s\S]?)+\}$/.test(value) ? '' : '"' return emptyAttributes.has(name) && !value ? - name : `${name}=${doubleQuote}${value.replace(QUOTE, '"')}${doubleQuote}`; + name : `${name}=${doubleQuote}${value.replace(QUOTE, doubleQuote ? '"' : '"')}${doubleQuote}`; } toJSON() { diff --git a/test/jsx/document.js b/test/jsx/document.js index 8d3ffa1e..737a9898 100644 --- a/test/jsx/document.js +++ b/test/jsx/document.js @@ -2,12 +2,12 @@ const assert = require('../assert.js').for('XMLDocument'); const {DOMParser} = global[Symbol.for('linkedom')]; -const document = (new DOMParser).parseFromString('', 'text/jsx+xml'); +const document = (new DOMParser).parseFromString('', 'text/jsx+xml'); -assert(document.toString(), ''); +assert(document.toString(), ''); -assert(document.documentElement.tagName, 'root'); -assert(document.documentElement.nodeName, 'root'); +assert(document.documentElement.tagName, 'html'); +assert(document.documentElement.nodeName, 'html'); document.documentElement.innerHTML = ` @@ -20,3 +20,30 @@ assert(document.querySelectorAll('Element').length, 2, 'case senstivive 2'); assert(document.querySelectorAll('element').length, 0, 'case senstivive 0'); assert(document.querySelector('Element').attributes.someAttribute.toString(), 'someAttribute={foo}', 'JSX must allow unquoted JSON attributes') +assert(document.querySelector('Element').toString(), '{ bar }', 'JSX must render case-sensitive') + +assert(document.toString(), ` + { bar } + Text +`, '1:1') + +const documentFullRerender = (new DOMParser).parseFromString(``, 'text/jsx+xml') + +// internally creates a html -> html -> ... structure because +// documentElement cannot be replaced +documentFullRerender.documentElement.innerHTML = ` + + { Some.props.catName } + + + ... +` + +// accessing html (documentElement) -> html (actual root) -> ... here +assert(documentFullRerender.documentElement.firstChild.toString(), ` + + { Some.props.catName } + + + ... +`, 'Internal logic should not override mime-type decision set by the developer') \ No newline at end of file diff --git a/worker.js b/worker.js index 874cae67..f7fcbd0d 100644 --- a/worker.js +++ b/worker.js @@ -9443,7 +9443,7 @@ class Attr$1 extends Node$1 { const {ownerDocument, name, [VALUE]: value} = this; const doubleQuote = ownerDocument[MIME].unquotedJsonAttributes && /^\{(.[\s\S]?)+\}$/.test(value) ? '' : '"'; return emptyAttributes.has(name) && !value ? - name : `${name}=${doubleQuote}${value.replace(QUOTE, '"')}${doubleQuote}`; + name : `${name}=${doubleQuote}${value.replace(QUOTE, doubleQuote ? '"' : '"')}${doubleQuote}`; } toJSON() {