From c7933dcd4fae52f88b293308535fe1d1697ce88c Mon Sep 17 00:00:00 2001 From: visualkhh Date: Tue, 7 Oct 2025 00:45:59 +0900 Subject: [PATCH 1/2] HTMLTemplateElement innerHTML output bugfix (cjs, esm) --browser result-- const f = document.createDocumentFragment(); const d = document.createElement('div') d.innerHTML='aaa'; f.append(d); const t = document.createElement('template'); t.content.append(f); console.log(t.innerHTML); // '
aaa
' console.log(t.innerText); //'' --linkedom-- console.log(t.innerHTML); // '' console.log(t.innerText); //'' After appending the child element to the HTML Template Element's content If you do innerHTML, it is normal to output the contents of the child element, but it is not output in linkedom. bugfix --- cjs/html/template-element.js | 6 ++++++ esm/html/template-element.js | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/cjs/html/template-element.js b/cjs/html/template-element.js index c65245d0..1e6b32b6 100644 --- a/cjs/html/template-element.js +++ b/cjs/html/template-element.js @@ -5,6 +5,8 @@ const {registerHTMLClass} = require('../shared/register-html-class.js'); const {HTMLElement} = require('./element.js'); +import {getInnerHtml} from '../mixin/inner-html.js' + const tagName = 'template'; /** @@ -17,6 +19,10 @@ class HTMLTemplateElement extends HTMLElement { (this[CONTENT] = content)[PRIVATE] = this; } + get innerHTML() { + return getInnerHtml(this.content); + } + get content() { if (this.hasChildNodes() && !this[CONTENT].hasChildNodes()) { for (const node of this.childNodes) diff --git a/esm/html/template-element.js b/esm/html/template-element.js index 78973f8f..d4f8ff46 100644 --- a/esm/html/template-element.js +++ b/esm/html/template-element.js @@ -4,6 +4,8 @@ import {registerHTMLClass} from '../shared/register-html-class.js'; import {HTMLElement} from './element.js'; +import {getInnerHtml} from '../mixin/inner-html.js' + const tagName = 'template'; /** @@ -16,6 +18,10 @@ class HTMLTemplateElement extends HTMLElement { (this[CONTENT] = content)[PRIVATE] = this; } + get innerHTML() { + return getInnerHtml(this.content); + } + get content() { if (this.hasChildNodes() && !this[CONTENT].hasChildNodes()) { for (const node of this.childNodes) From a9d77deac3c83ba424f4dbed47be83f46e85355d Mon Sep 17 00:00:00 2001 From: visualkhh Date: Tue, 7 Oct 2025 23:36:06 +0900 Subject: [PATCH 2/2] Fix template element parsing and innerHTML handling - Fix template children being parsed into element.childNodes instead of template.content during parseHTML() - Add special handling in parse-from-string.js to redirect template children to content DocumentFragment - Implement proper template.innerHTML getter/setter to access content instead of direct children - Add template.toString() method following TextElement pattern to include content in serialization - Ensure template element follows HTML spec where innerHTML always references template.content - Add null safety checks in parsing logic to prevent errors during template processing The template element now correctly: - Parses children into content during HTML parsing - Returns content innerHTML when accessing template.innerHTML - Maintains separation between direct children and content - Serializes with content included in toString() --browser result-- const f = document.createDocumentFragment(); const d = document.createElement('div') d.innerHTML='aaa'; f.append(d); const t = document.createElement('template'); t.content.append(f); console.log(t.innerHTML); // '
aaa
' console.log(t.innerText); //'' --linkedom-- console.log(t.innerHTML); // '' console.log(t.innerText); //'' --- cjs/html/template-element.js | 24 ++++++++++++++++-------- cjs/shared/parse-from-string.js | 9 ++++++++- esm/html/template-element.js | 17 ++++++++++++----- esm/shared/parse-from-string.js | 9 ++++++++- test/html/template-element.js | 22 +++++++++++++++++++++- 5 files changed, 65 insertions(+), 16 deletions(-) diff --git a/cjs/html/template-element.js b/cjs/html/template-element.js index 1e6b32b6..2281038e 100644 --- a/cjs/html/template-element.js +++ b/cjs/html/template-element.js @@ -5,7 +5,10 @@ const {registerHTMLClass} = require('../shared/register-html-class.js'); const {HTMLElement} = require('./element.js'); -import {getInnerHtml} from '../mixin/inner-html.js' +const {getInnerHtml} = require('../mixin/inner-html.js'); +const {setInnerHtml} = require("../mixin/inner-html"); + +const {toString} = require('./element.js').HTMLElement.prototype; const tagName = 'template'; @@ -14,22 +17,27 @@ const tagName = 'template'; */ class HTMLTemplateElement extends HTMLElement { constructor(ownerDocument) { - super(ownerDocument, tagName); - const content = this.ownerDocument.createDocumentFragment(); - (this[CONTENT] = content)[PRIVATE] = this; + super(ownerDocument, tagName); + const content = this.ownerDocument.createDocumentFragment(); + (this[CONTENT] = content)[PRIVATE] = this; } get innerHTML() { return getInnerHtml(this.content); } + set innerHTML(html) { + setInnerHtml(this[CONTENT], html); + } + get content() { - if (this.hasChildNodes() && !this[CONTENT].hasChildNodes()) { - for (const node of this.childNodes) - this[CONTENT].appendChild(node.cloneNode(true)); - } return this[CONTENT]; } + + toString() { + const outerHTML = toString.call(this.cloneNode()); + return outerHTML.replace('>', () => `>${this.innerHTML}`); + } } registerHTMLClass(tagName, HTMLTemplateElement); diff --git a/cjs/shared/parse-from-string.js b/cjs/shared/parse-from-string.js index 84a9fdf3..dcec7d1e 100644 --- a/cjs/shared/parse-from-string.js +++ b/cjs/shared/parse-from-string.js @@ -2,7 +2,7 @@ const HTMLParser2 = require('htmlparser2'); const {ELEMENT_NODE, SVG_NAMESPACE} = require('./constants.js'); -const {CUSTOM_ELEMENTS, PREV, END, VALUE} = require('./symbols.js'); +const {CUSTOM_ELEMENTS, PREV, END, VALUE, CONTENT, PRIVATE} = require('./symbols.js'); const {keys} = require('./object.js'); const {knownBoundaries, knownSiblings} = require('./utils.js'); @@ -19,6 +19,10 @@ const {Parser} = HTMLParser2; let notParsing = true; const append = (self, node, active) => { + if (self && self.localName === 'template' && self[CONTENT]) { + self = self[CONTENT]; + } + if (!self) return node; // null 체크 추가 const end = self[END]; node.parentNode = self; knownBoundaries(end[PREV], node, end); @@ -107,6 +111,9 @@ const parseFromString = (document, isHTML, markupLanguage) => { onclosetag() { if (isHTML && node === ownerSVGElement) ownerSVGElement = null; + if (node && node[PRIVATE]) { + node = node[PRIVATE]; + } node = node.parentNode; } }, { diff --git a/esm/html/template-element.js b/esm/html/template-element.js index d4f8ff46..f0746031 100644 --- a/esm/html/template-element.js +++ b/esm/html/template-element.js @@ -4,7 +4,9 @@ import {registerHTMLClass} from '../shared/register-html-class.js'; import {HTMLElement} from './element.js'; -import {getInnerHtml} from '../mixin/inner-html.js' +import {getInnerHtml, setInnerHtml} from '../mixin/inner-html.js'; + +const {toString} = HTMLElement.prototype; const tagName = 'template'; @@ -22,13 +24,18 @@ class HTMLTemplateElement extends HTMLElement { return getInnerHtml(this.content); } + set innerHTML(html) { + setInnerHtml(this[CONTENT], html); + } + get content() { - if (this.hasChildNodes() && !this[CONTENT].hasChildNodes()) { - for (const node of this.childNodes) - this[CONTENT].appendChild(node.cloneNode(true)); - } return this[CONTENT]; } + + toString() { + const outerHTML = toString.call(this.cloneNode()); + return outerHTML.replace('>', () => `>${this.innerHTML}`); + } } registerHTMLClass(tagName, HTMLTemplateElement); diff --git a/esm/shared/parse-from-string.js b/esm/shared/parse-from-string.js index 08c6fe39..77b43487 100644 --- a/esm/shared/parse-from-string.js +++ b/esm/shared/parse-from-string.js @@ -1,7 +1,7 @@ import * as HTMLParser2 from 'htmlparser2'; import {ELEMENT_NODE, SVG_NAMESPACE} from './constants.js'; -import {CUSTOM_ELEMENTS, PREV, END, VALUE} from './symbols.js'; +import {CUSTOM_ELEMENTS, PREV, END, VALUE, CONTENT, PRIVATE} from './symbols.js'; import {keys} from './object.js'; import {knownBoundaries, knownSiblings} from './utils.js'; @@ -18,6 +18,10 @@ const {Parser} = HTMLParser2; let notParsing = true; const append = (self, node, active) => { + if (self && self.localName === 'template' && self[CONTENT]) { + self = self[CONTENT]; + } + if (!self) return node; // null 체크 추가 const end = self[END]; node.parentNode = self; knownBoundaries(end[PREV], node, end); @@ -105,6 +109,9 @@ export const parseFromString = (document, isHTML, markupLanguage) => { onclosetag() { if (isHTML && node === ownerSVGElement) ownerSVGElement = null; + if (node && node[PRIVATE]) { + node = node[PRIVATE]; + } node = node.parentNode; } }, { diff --git a/test/html/template-element.js b/test/html/template-element.js index 05d5caec..2f37833b 100644 --- a/test/html/template-element.js +++ b/test/html/template-element.js @@ -15,7 +15,7 @@ assert(document.querySelector('template > *'), null); assert(template.content, template.content); template.replaceChildren(); -assert(template.innerHTML, ''); +assert(template.innerHTML, '
foo
bar
'); template.innerHTML = '

ok

'; assert(template.innerHTML, '

ok

'); @@ -47,3 +47,23 @@ const docWithTemplateAttribute = parseHTML(`

not insi assert(docWithTemplateAttribute.querySelector('*').tagName, 'P'); assert(docWithTemplateAttribute.querySelectorAll('*').length, 1); + +template = document.createElement('template'); +let fragment = document.createDocumentFragment(); +let div = document.createElement('div') +div.innerHTML='child element'; +fragment.append(div); +template.content.append(fragment); +assert(template.innerHTML, '

child element
', 'template.innerHTML'); +assert(template.innerText, '', 'template.innerText'); + +template = document.createElement('template'); +template.innerHTML='
child element innerHTML
'; +fragment = document.createDocumentFragment(); +div = document.createElement('div'); +div.innerHTML='new child element'; +assert(Array.from(template.children).length, 0, 'template.appendChild zero'); +template.appendChild(div); +assert(Array.from(template.children).length, 1, 'template.appendChild one'); +assert(template.innerText, 'new child element', 'template.innerText'); +assert(template.innerHTML, '
child element innerHTML
', 'template.innerHTML The template maintains internal HTML even with additional children');