Skip to content
This repository has been archived by the owner on Jul 28, 2023. It is now read-only.

Commit

Permalink
Merge pull request #103 from WordPress/create-tree-walker
Browse files Browse the repository at this point in the history
🎨 Switch toVdom from array iteration to createTreeWalker
  • Loading branch information
luisherranz authored Apr 19, 2023
2 parents aeb51e3 + 065bdb7 commit f1ccde9
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 56 deletions.
2 changes: 1 addition & 1 deletion e2e/html/directive-bind.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
Update
</button>

<script src="../../build/e2e/html/directive-bind.js"></script>
<script src="../../build/e2e/directive-bind.js"></script>
<script src="../../build/runtime.js"></script>
<script src="../../build/vendors.js"></script>
</body>
Expand Down
64 changes: 64 additions & 0 deletions e2e/html/tovdom.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html>
<head>
<title>toVdom</title>
</head>

<body data-wp-island>
<div data-testid="it should delete comments">
<!-- ##1## -->
<div data-testid="it should keep this node between comments">
Comments inner node
<!-- ##2## -->
</div>
</div>

<div data-testid="it should delete processing instructions">
<div id="replace-with-processing-instructions"></div>
</div>

<script>
const processingInstructions = `
<div>
<?xpacket ##1## ?>
<div data-testid="it should keep this node between processing instructions">
Processing instructions inner node
<?xpacket ##2## ?>
</div>
</div>
`;

const processingInstructionsElement = new DOMParser()
.parseFromString(processingInstructions, 'text/xml')
.querySelector('div');
document
.getElementById('replace-with-processing-instructions')
.replaceWith(processingInstructionsElement);
</script>

<div data-testid="it should replace CDATA with text nodes">
<div id="replace-with-cdata"></div>
</div>

<script>
const cdata = `
<div>
<![CDATA[##1##]]>
<div data-testid="it should keep this node between CDATA">
<![CDATA[##2##]]>
</div>
</div>
`;

const cdataElement = new DOMParser()
.parseFromString(cdata, 'text/xml')
.querySelector('div');
document
.getElementById('replace-with-cdata')
.replaceWith(cdataElement);
</script>

<script src="../../build/runtime.js"></script>
<script src="../../build/vendors.js"></script>
</body>
</html>
1 change: 0 additions & 1 deletion e2e/html/directive-bind.js → e2e/js/directive-bind.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { store } from '../../src/runtime/store';

// State for the store hydration tests.
store({
state: {
url: '/some-url',
Expand Down
38 changes: 38 additions & 0 deletions e2e/specs/tovdom.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
4 changes: 2 additions & 2 deletions src/runtime/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
3 changes: 1 addition & 2 deletions src/runtime/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -149,7 +149,6 @@ export const init = async () => {
document.documentElement,
document.body
);

const body = toVdom(document.body);
hydrate(body, rootFragment);

Expand Down
116 changes: 67 additions & 49 deletions src/runtime/vdom.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
2 changes: 1 addition & 1 deletion webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down

0 comments on commit f1ccde9

Please sign in to comment.