diff --git a/cjs/shared/parse-from-string.js b/cjs/shared/parse-from-string.js index 84a9fdf3..aac2862b 100644 --- a/cjs/shared/parse-from-string.js +++ b/cjs/shared/parse-from-string.js @@ -1,7 +1,7 @@ 'use strict'; const HTMLParser2 = require('htmlparser2'); -const {ELEMENT_NODE, SVG_NAMESPACE} = require('./constants.js'); +const {ELEMENT_NODE, SVG_NAMESPACE, TEXT_NODE} = require('./constants.js'); const {CUSTOM_ELEMENTS, PREV, END, VALUE} = require('./symbols.js'); const {keys} = require('./object.js'); @@ -95,7 +95,11 @@ const parseFromString = (document, isHTML, markupLanguage) => { if (parsingCData) { append(node, document.createCDATASection(text), active); } else { - append(node, document.createTextNode(text), active); + const prev = node[END][PREV]; + if (prev.nodeType === TEXT_NODE) + prev[VALUE] += text; + else + append(node, document.createTextNode(text), active); } }, diff --git a/esm/shared/parse-from-string.js b/esm/shared/parse-from-string.js index 08c6fe39..14b678ae 100644 --- a/esm/shared/parse-from-string.js +++ b/esm/shared/parse-from-string.js @@ -1,6 +1,6 @@ import * as HTMLParser2 from 'htmlparser2'; -import {ELEMENT_NODE, SVG_NAMESPACE} from './constants.js'; +import {ELEMENT_NODE, SVG_NAMESPACE, TEXT_NODE} from './constants.js'; import {CUSTOM_ELEMENTS, PREV, END, VALUE} from './symbols.js'; import {keys} from './object.js'; @@ -93,7 +93,11 @@ export const parseFromString = (document, isHTML, markupLanguage) => { if (parsingCData) { append(node, document.createCDATASection(text), active); } else { - append(node, document.createTextNode(text), active); + const prev = node[END][PREV]; + if (prev.nodeType === TEXT_NODE) + prev[VALUE] += text; + else + append(node, document.createTextNode(text), active); } }, diff --git a/test/html/document.js b/test/html/document.js index b5e15062..71d40498 100644 --- a/test/html/document.js +++ b/test/html/document.js @@ -98,3 +98,40 @@ const issue187 = (new DOMParser).parseFromString(`
'; let span = issue187.body.firstElementChild; assert(span.ownerDocument.body, issue187.body); + +// @issue 353: the parser must merge consecutive ontext pieces into one Text node. +// htmlparser2 with decodeEntities:true calls ontext once per piece around each +// character reference; adjacent pieces under the same parent must be merged into +// a single Text node (per the HTML parsing algorithm "insert a character"). +{ + const parse = html => (new DOMParser).parseFromString(`${html}`, 'text/html'); + + // entity split: "a & b" -> one Text node "a & b" + let p = parse('a & b
').querySelector('p'); + assert(p.childNodes.length, 1, 'entity split merged into one Text node'); + assert(p.firstChild.data, 'a & b', 'entity split data'); + + // multi-entity: "x < y > z ©" -> one Text node + p = parse('x < y > z ©
').querySelector('p'); + assert(p.childNodes.length, 1, 'multi-entity merged into one Text node'); + assert(p.firstChild.data, 'x < y > z ©', 'multi-entity data'); + + // plain text unchanged (still one node) + p = parse('plain text
').querySelector('p'); + assert(p.childNodes.length, 1, 'plain text stays one node'); + assert(p.firstChild.data, 'plain text', 'plain text data'); + + // text after an element boundary is NOT merged across it + p = parse('a &x & b
').querySelector('p'); + assert(p.childNodes.length, 3, 'text not merged across element boundary'); + assert(p.firstChild.data, 'a &', 'first run before element'); + assert(p.lastChild.data, ' & b', 'last run after element'); + assert(p.querySelector('b').childNodes.length, 1, 'element own text still one node'); + assert(p.querySelector('b').firstChild.data, 'x', 'element own text data'); + + // text after a comment boundary is NOT merged across it + p = parse('a & b
').querySelector('p'); + assert(p.childNodes.length, 3, 'text not merged across comment boundary'); + assert(p.firstChild.data, 'a ', 'first run before comment'); + assert(p.lastChild.data, ' & b', 'last run after comment'); +}