diff --git a/e2e/html/directive-bind.html b/e2e/html/directive-bind.html index 98dc7025..09f4734f 100644 --- a/e2e/html/directive-bind.html +++ b/e2e/html/directive-bind.html @@ -33,7 +33,7 @@ Update - + diff --git a/e2e/html/tovdom.html b/e2e/html/tovdom.html new file mode 100644 index 00000000..dfdb7cf2 --- /dev/null +++ b/e2e/html/tovdom.html @@ -0,0 +1,64 @@ + + + + toVdom + + + +
+ +
+ Comments inner node + +
+
+ +
+
+
+ + + +
+
+
+ + + + + + + diff --git a/e2e/html/directive-bind.js b/e2e/js/directive-bind.js similarity index 85% rename from e2e/html/directive-bind.js rename to e2e/js/directive-bind.js index ec68a2c8..e872452b 100644 --- a/e2e/html/directive-bind.js +++ b/e2e/js/directive-bind.js @@ -1,6 +1,5 @@ import { store } from '../../src/runtime/store'; -// State for the store hydration tests. store({ state: { url: '/some-url', diff --git a/e2e/specs/tovdom.spec.ts b/e2e/specs/tovdom.spec.ts new file mode 100644 index 00000000..4322eea2 --- /dev/null +++ b/e2e/specs/tovdom.spec.ts @@ -0,0 +1,38 @@ +import { test, expect } from '../tests'; + +test.describe('toVdom', () => { + test.beforeEach(async ({ goToFile }) => { + await goToFile('tovdom.html'); + }); + + test('it should delete comments', async ({ page }) => { + const el = page.getByTestId('it should delete comments'); + const c = await el.innerHTML(); + expect(c).not.toContain('##1##'); + expect(c).not.toContain('##2##'); + const el2 = page.getByTestId( + 'it should keep this node between comments' + ); + await expect(el2).toBeVisible(); + }); + + test('it should delete processing instructions', async ({ page }) => { + const el = page.getByTestId('it should delete processing instructions'); + const c = await el.innerHTML(); + expect(c).not.toContain('##1##'); + expect(c).not.toContain('##2##'); + const el2 = page.getByTestId( + 'it should keep this node between processing instructions' + ); + await expect(el2).toBeVisible(); + }); + + test('it should replace CDATA with text nodes', async ({ page }) => { + const el = page.getByTestId('it should replace CDATA with text nodes'); + const c = await el.innerHTML(); + expect(c).toContain('##1##'); + expect(c).toContain('##2##'); + const el2 = page.getByTestId('it should keep this node between CDATA'); + await expect(el2).toBeVisible(); + }); +}); diff --git a/src/runtime/index.js b/src/runtime/index.js index 2b7723ce..e32495dc 100644 --- a/src/runtime/index.js +++ b/src/runtime/index.js @@ -5,11 +5,11 @@ export { store } from './store'; export { navigate } from './router'; /** - * Initialize the initial vDOM. + * Initialize the Interactivity API. */ document.addEventListener('DOMContentLoaded', async () => { registerDirectives(); registerComponents(); await init(); - console.log('hydrated!'); + console.log('Interactivity API started'); }); diff --git a/src/runtime/router.js b/src/runtime/router.js index 19a2a4e5..1f74c251 100644 --- a/src/runtime/router.js +++ b/src/runtime/router.js @@ -122,7 +122,7 @@ export const navigate = async (href, { replace = false } = {}) => { if (page) { document.head.replaceChildren(...page.head); render(page.body, rootFragment); - window.history[replace ? "replaceState" : "pushState"]({}, '', href); + window.history[replace ? 'replaceState' : 'pushState']({}, '', href); } else { window.location.assign(href); } @@ -149,7 +149,6 @@ export const init = async () => { document.documentElement, document.body ); - const body = toVdom(document.body); hydrate(body, rootFragment); diff --git a/src/runtime/vdom.js b/src/runtime/vdom.js index f1cd5442..69a13f77 100644 --- a/src/runtime/vdom.js +++ b/src/runtime/vdom.js @@ -8,64 +8,82 @@ const directiveParser = new RegExp(`${p}([^.]+)\.?(.*)$`); export const hydratedIslands = new WeakSet(); // Recursive function that transfoms a DOM tree into vDOM. -export function toVdom(node) { - const props = {}; - const { attributes, childNodes } = node; - const directives = {}; - let hasDirectives = false; - let ignore = false; - let island = false; +export function toVdom(root) { + const treeWalker = document.createTreeWalker( + root, + 205 // ELEMENT + TEXT + COMMENT + CDATA_SECTION + PROCESSING_INSTRUCTION + ); - if (node.nodeType === 3) return node.data; - if (node.nodeType === 4) { - node.replaceWith(new Text(node.nodeValue)); - return node.nodeValue; - } + function walk(node) { + const { attributes, nodeType } = node; + + if (nodeType === 3) return [node.data]; + if (nodeType === 4) { + const next = treeWalker.nextSibling(); + node.replaceWith(new Text(node.nodeValue)); + return [node.nodeValue, next]; + } + if (nodeType === 8 || nodeType === 7) { + const next = treeWalker.nextSibling(); + node.remove(); + return [null, next]; + } - for (let i = 0; i < attributes.length; i++) { - const n = attributes[i].name; - if (n[p.length] && n.slice(0, p.length) === p) { - if (n === ignoreAttr) { - ignore = true; - } else if (n === islandAttr) { - island = true; + const props = {}; + const children = []; + const directives = {}; + let hasDirectives = false; + let ignore = false; + let island = false; + + for (let i = 0; i < attributes.length; i++) { + const n = attributes[i].name; + if (n[p.length] && n.slice(0, p.length) === p) { + if (n === ignoreAttr) { + ignore = true; + } else if (n === islandAttr) { + island = true; + } else { + hasDirectives = true; + let val = attributes[i].value; + try { + val = JSON.parse(val); + } catch (e) {} + const [, prefix, suffix] = directiveParser.exec(n); + directives[prefix] = directives[prefix] || {}; + directives[prefix][suffix || 'default'] = val; + } + } else if (n === 'ref') { + continue; } else { - hasDirectives = true; - let val = attributes[i].value; - try { - val = JSON.parse(val); - } catch (e) {} - const [, prefix, suffix] = directiveParser.exec(n); - directives[prefix] = directives[prefix] || {}; - directives[prefix][suffix || 'default'] = val; + props[n] = attributes[i].value; } - } else if (n === 'ref') { - continue; - } else { - props[n] = attributes[i].value; } - } - if (ignore && !island) - return h(node.localName, { - ...props, - innerHTML: node.innerHTML, - directives: { ignore: true }, - }); - if (island) hydratedIslands.add(node); + if (ignore && !island) + return [ + h(node.localName, { + ...props, + innerHTML: node.innerHTML, + directives: { ignore: true }, + }), + ]; + if (island) hydratedIslands.add(node); - if (hasDirectives) props.directives = directives; + if (hasDirectives) props.directives = directives; - const children = []; - for (let i = 0; i < childNodes.length; i++) { - const child = childNodes[i]; - if (child.nodeType === 8 || child.nodeType === 7) { - child.remove(); - i--; - } else { - children.push(toVdom(child)); + let child = treeWalker.firstChild(); + if (child) { + while (child) { + const [vnode, nextChild] = walk(child); + if (vnode) children.push(vnode); + child = nextChild || treeWalker.nextSibling(); + } + treeWalker.parentNode(); } + + return [h(node.localName, props, children)]; } - return h(node.localName, props, children); + return walk(treeWalker.currentNode); } diff --git a/webpack.config.js b/webpack.config.js index 0747f183..d3b84f2e 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -10,7 +10,7 @@ module.exports = [ runtime: './src/runtime', 'e2e/page-1': './e2e/page-1', 'e2e/page-2': './e2e/page-2', - 'e2e/html/directive-bind': './e2e/html/directive-bind', + 'e2e/directive-bind': './e2e/js/directive-bind', }, output: { filename: '[name].js',