Skip to content
Draft
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
7 changes: 6 additions & 1 deletion cjs/dom/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down
7 changes: 4 additions & 3 deletions cjs/interface/attr.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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() {
Expand Down
12 changes: 9 additions & 3 deletions cjs/interface/comment.js
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -18,6 +18,12 @@ class Comment extends CharacterData {
return new Comment(ownerDocument, data);
}

toString() { return `<!--${escape(this[VALUE])}-->`; }
toString() {
const {ownerDocument} = this;
if (ownerDocument[MIME].escapeHtmlEntities) {
return `<!--${escape(this[VALUE])}-->`;
}
return `<!--${(this[VALUE])}-->`;
}
}
exports.Comment = Comment
exports.Comment = Comment
12 changes: 9 additions & 3 deletions cjs/interface/text.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down Expand Up @@ -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
10 changes: 10 additions & 0 deletions cjs/jsx/document.js
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions cjs/shared/mime.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,38 @@ const Mime = {
'text/html': {
docType: '<!DOCTYPE html>',
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: '<?xml version="1.0" encoding="utf-8"?>',
ignoreCase: false,
escapeHtmlEntities: true,
voidElements
},
'text/xml': {
docType: '<?xml version="1.0" encoding="utf-8"?>',
ignoreCase: false,
escapeHtmlEntities: true,
voidElements
},
'application/xml': {
docType: '<?xml version="1.0" encoding="utf-8"?>',
ignoreCase: false,
escapeHtmlEntities: true,
voidElements
},
'application/xhtml+xml': {
docType: '<?xml version="1.0" encoding="utf-8"?>',
ignoreCase: false,
escapeHtmlEntities: true,
voidElements
},
'text/jsx+xml': {
docType: '<?xml version="1.0" encoding="utf-8"?>',
ignoreCase: false,
escapeHtmlEntities: false,
unquotedJsonAttributes: true,
voidElements
}
};
Expand Down
7 changes: 6 additions & 1 deletion esm/dom/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down
7 changes: 4 additions & 3 deletions esm/interface/attr.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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) ? '' : '"'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is /^\{(.[\s\S]?)+\}$/ for? if it's about starting with { and ending with } I believe /^\{[\s\S]*\}$/ is what you are after?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I'll try that; should be more precise than my code there. It was late when I wrote that RegExp :)

return emptyAttributes.has(name) && !value ?
name : `${name}="${value.replace(QUOTE, '&quot;')}"`;
name : `${name}=${doubleQuote}${value.replace(QUOTE, doubleQuote ? '&quot;' : '"')}${doubleQuote}`;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have instead:

const useQuote = ownerDocument[MIME].unquotedJsonAttributes && /^\{(.[\s\S]?)+\}$/.test(value);
const quote = useQuote ? '"' : '';

// ...

`${name}=${quote}${value.replace(QUOTE, useQuote ? '&quot;' : '"')}${quote}`

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is def. more readable. Good point.

}

toJSON() {
Expand Down
12 changes: 9 additions & 3 deletions esm/interface/comment.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -17,5 +17,11 @@ export class Comment extends CharacterData {
return new Comment(ownerDocument, data);
}

toString() { return `<!--${escape(this[VALUE])}-->`; }
}
toString() {
const {ownerDocument} = this;
if (ownerDocument[MIME].escapeHtmlEntities) {
return `<!--${escape(this[VALUE])}-->`;
}
return `<!--${(this[VALUE])}-->`;
}
}
12 changes: 9 additions & 3 deletions esm/interface/text.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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];
}
}
8 changes: 8 additions & 0 deletions esm/jsx/document.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {Document} from '../interface/document.js';

/**
* @implements globalThis.JSXDocument
*/
export class JSXDocument extends Document {
constructor() { super('text/jsx+xml'); }
}
12 changes: 12 additions & 0 deletions esm/shared/mime.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,38 @@ export const Mime = {
'text/html': {
docType: '<!DOCTYPE html>',
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: '<?xml version="1.0" encoding="utf-8"?>',
ignoreCase: false,
escapeHtmlEntities: true,
voidElements
},
'text/xml': {
docType: '<?xml version="1.0" encoding="utf-8"?>',
ignoreCase: false,
escapeHtmlEntities: true,
voidElements
},
'application/xml': {
docType: '<?xml version="1.0" encoding="utf-8"?>',
ignoreCase: false,
escapeHtmlEntities: true,
voidElements
},
'application/xhtml+xml': {
docType: '<?xml version="1.0" encoding="utf-8"?>',
ignoreCase: false,
escapeHtmlEntities: true,
voidElements
},
'text/jsx+xml': {
docType: '<?xml version="1.0" encoding="utf-8"?>',
ignoreCase: false,
escapeHtmlEntities: false,
unquotedJsonAttributes: true,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 things I'd rather change here, or discuss:

  1. I don't like checking emptiness ... all objects here are homogeneous, if we add a single boolean field somewhere I want it to be explicitly boolean field somewhere else too, as it is for escapeHtmlEntities ... so, I'd add unquotedJsonAttributes: flase in other cases
  2. I don't unrestand why it's called unquotedJsonAttributes ... I understand it's virtually JSON, but practically that's an unquoted attribute ... so ... since we want to allow only JSON which results into being unquoted, I think a field name such as jsonAttributes would better reflect the intent/purpose? ... 'cause of course those gotta be unquoted, otherwise it'd be invalid JSON, does it make sense?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. agreed, that makes sense to me
  2. I think that jsonAttributes would describe the purpose what should be achieved, while unquotedJsonAttributes is describing what it is happening. I like both ways of naming. Lets go with jsonAttributes then

voidElements
}
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,4 @@
"url": "https://github.com/WebReflection/linkedom/issues"
},
"homepage": "https://github.com/WebReflection/linkedom#readme"
}
}
2 changes: 2 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand All @@ -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'))
Expand Down
8 changes: 7 additions & 1 deletion test/interface/text.js
Original file line number Diff line number Diff line change
@@ -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('<html><div></div></html>');

Expand Down Expand Up @@ -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('<html><!-- <> --><div foo={bar}><></div><img className="foo" src={getSrc()} />{eles.map(ele => (<div foo={ele.id}></div>))}</html>', 'text/jsx+xml');
assert(
jsxDocument.getRootNode().toString(),
'<html><!-- <> --><div foo={bar}><></div><img className="foo" src={getSrc()} />{eles.map(ele => (<div foo={ele.id} />))}</html>',
'Must resemble JSX original form, no escaping, allow for JSON attributes'
);
49 changes: 49 additions & 0 deletions test/jsx/document.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const assert = require('../assert.js').for('XMLDocument');

const {DOMParser} = global[Symbol.for('linkedom')];

const document = (new DOMParser).parseFromString('<html></html>', 'text/jsx+xml');

assert(document.toString(), '<html />');

assert(document.documentElement.tagName, 'html');
assert(document.documentElement.nodeName, 'html');

document.documentElement.innerHTML = `
<Something>
<Element someAttribute={foo}>{ bar }</Element>
<Element>Text</Element>
</Something>
`.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')
assert(document.querySelector('Element').toString(), '<Element someAttribute={foo}>{ bar }</Element>', 'JSX must render case-sensitive')

assert(document.toString(), `<html><Something>
<Element someAttribute={foo}>{ bar }</Element>
<Element>Text</Element>
</Something></html>`, '1:1')

const documentFullRerender = (new DOMParser).parseFromString(`<html />`, 'text/jsx+xml')

// internally creates a html -> html -> ... structure because
// documentElement cannot be replaced
documentFullRerender.documentElement.innerHTML = `<html>
<head>
<title>{ Some.props.catName }</title>
</head>
<KittenHeader title={"foo"} />
...
</html>`

// accessing html (documentElement) -> html (actual root) -> ... here
assert(documentFullRerender.documentElement.firstChild.toString(), `<html>
<head>
<title>{ Some.props.catName }</title>
</head>
<KittenHeader title={"foo"} />
...
</html>`, 'Internal logic should not override mime-type decision set by the developer')
Loading