--- a PPN by Garber Painting Akron. With Image Size Reduction included!URL: http://github.com/WebReflection/uhtml/pull/113.patch
m './index.js';
+
+const htmlFor = () => html;
+const svgFor = () => svg;
+
+export { Hole, render, html, svg, htmlFor, svgFor, attr };
diff --git a/esm/utils.js b/esm/utils.js
index 1072670..7ce053e 100644
--- a/esm/utils.js
+++ b/esm/utils.js
@@ -34,3 +34,12 @@ export const gPD = (ref, prop) => {
while(!desc && (ref = getPrototypeOf(ref)));
return desc;
};
+
+
+/**
+ * @param {DocumentFragment} content
+ * @param {number[]} path
+ * @returns {Element}
+ */
+export const find = (content, path) => path.reduceRight(childNodesIndex, content);
+const childNodesIndex = (node, i) => node.childNodes[i];
diff --git a/package.json b/package.json
index b9e63ba..f3bee74 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"scripts": {
"benchmark:w3c": "node test/benchmark/linkedom.js --w3c; node test/benchmark/linkedom-cached.js --w3c; node test/benchmark/dom.js --w3c",
"benchmark:dom": "node test/benchmark/linkedom.js --dom; node test/benchmark/linkedom-cached.js --dom; node test/benchmark/dom.js --dom",
- "build": "npm run rollup:es && node rollup/init.cjs && npm run rollup:init && rm -rf cjs/* && npm run cjs && rm -rf types && npm run ts && npm run test && npm run size",
+ "build": "npm run rollup:es && node rollup/ssr.cjs && node rollup/init.cjs && npm run rollup:init && rm -rf cjs/* && npm run cjs && rm -rf types && npm run ts && npm run test && npm run size",
"cjs": "ascjs --no-default esm cjs",
"rollup:es": "rollup --config rollup/es.config.js",
"rollup:init": "rollup --config rollup/init.config.js",
diff --git a/rollup/es.config.js b/rollup/es.config.js
index e27339b..1522029 100644
--- a/rollup/es.config.js
+++ b/rollup/es.config.js
@@ -18,6 +18,24 @@ export default [
name: 'uhtml',
},
},
+ {
+ plugins: [nodeResolve()],
+ input: './esm/ssr.js',
+ output: {
+ esModule: false,
+ file: './esm/init-ssr.js',
+ format: 'iife',
+ name: 'uhtml',
+ },
+ },
+ {
+ plugins,
+ input: './esm/hydro.js',
+ output: {
+ esModule: true,
+ file: './hydro.js',
+ },
+ },
{
plugins,
input: './esm/index.js',
diff --git a/rollup/ssr.cjs b/rollup/ssr.cjs
new file mode 100644
index 0000000..1256581
--- /dev/null
+++ b/rollup/ssr.cjs
@@ -0,0 +1,33 @@
+const { readFileSync, writeFileSync } = require('fs');
+const { join } = require('path');
+
+const init = join(__dirname, '..', 'esm', 'init-ssr.js');
+const uhtml = readFileSync(init).toString();
+
+const content = [
+ 'const document = content ? new DOMParser().parseFromString(content, ...rest) : new Document;',
+ 'const { constructor: DocumentFragment } = document.createDocumentFragment();'
+];
+
+writeFileSync(init, `
+// ⚠️ WARNING - THIS FILE IS AN ARTIFACT - DO NOT EDIT
+
+import Document from './dom/document.js';
+import DOMParser from './dom/dom-parser.js';
+
+/**
+ * @param {Document} document
+ * @returns {import("./keyed.js")}
+ */
+export default (content, ...rest) => ${
+ // tested via integration
+ uhtml
+ .replace(/const create(HTML|SVG) = create\(parse\((false|true), false\)\)/g, 'const create$1 = create(parse($2, true))')
+ .replace(`svg || ('ownerSVGElement' in element)`, `/* c8 ignore start */ svg || ('ownerSVGElement' in element) /* c8 ignore stop */`)
+ .replace(/diffFragment = \(([\S\s]+?)return /, 'diffFragment = /* c8 ignore start */($1/* c8 ignore stop */return ')
+ .replace(/udomdiff = \(([\S\s]+?)return /, 'udomdiff = /* c8 ignore start */($1/* c8 ignore stop */return ')
+ .replace(/^(\s+)replaceWith\(([^}]+?)\}/m, '$1/* c8 ignore start */\n$1replaceWith($2}\n$1/* c8 ignore stop */')
+ .replace(/^(\s+)(["'])use strict\2;/m, (_, tab, quote) => `${tab}${quote}use strict${quote};\n\n${tab}${content.join(`\n${tab}`)}`)
+ .replace(/^(\s+)(return exports;)/m, '$1exports.document = document;\n$1$2')
+ .replace(/^[^(]+/, '')
+}`);
diff --git a/test/hydro.html b/test/hydro.html
new file mode 100644
index 0000000..7c36415
--- /dev/null
+++ b/test/hydro.html
@@ -0,0 +1,46 @@
+
+
+
+ Hello Hydro
+
+
+
+
+
+
+
+
diff --git a/test/hydro.mjs b/test/hydro.mjs
new file mode 100644
index 0000000..aeae8ea
--- /dev/null
+++ b/test/hydro.mjs
@@ -0,0 +1,57 @@
+import init from '../esm/init-ssr.js';
+
+function App(state) {
+ return html`
+
+
+
+
+ `;
+}
+
+const component = (target, Callback) => {
+ const effect = {
+ target,
+ update(...args) {
+ render(target, Callback.apply(effect, args));
+ }
+ };
+ return Callback.bind(effect);
+};
+
+const state = { title: 'Hello Hydro', count: 0 };
+
+const { document, render, html } = init(`
+
+
+
+
+
+ ${state.title}
+
+
+
+`);
+
+const { body } = document;
+
+const Body = component(body, App);
+
+render(body, Body(state));
+
+console.log(document.toString());
diff --git a/test/parser.mjs b/test/parser.mjs
new file mode 100644
index 0000000..af86d92
--- /dev/null
+++ b/test/parser.mjs
@@ -0,0 +1,11 @@
+import parser from '@webreflection/uparser';
+
+const prefix = 'isµ';
+const re = new RegExp(``, 'g');
+
+const template = t => t;
+
+console.log(
+ parser(template`a${1}b`, prefix, false)
+ .replace(re, '$&')
+);
diff --git a/test/ssr.mjs b/test/ssr.mjs
new file mode 100644
index 0000000..4584d8a
--- /dev/null
+++ b/test/ssr.mjs
@@ -0,0 +1,18 @@
+import init from '../esm/init-ssr.js';
+
+const { document, render, html } = init(`
+
+
+ ${'Hello SSR'}
+
+
+`.trim()
+);
+
+render(document.getElementById('test'), html`
+
+ !!! ${'Hello SSR'} !!!
+
+`);
+
+console.log(document.toString());
From ed8e7f95a8e4bade5a1bf832b2b9d66571563378 Mon Sep 17 00:00:00 2001
From: webreflection
Date: Sun, 21 Jan 2024 15:09:59 +0100
Subject: [PATCH 2/3] up to the point I need to reverse-loop the container to
have clear paths out of the template
---
esm/dom/document.js | 3 +-
esm/dom/symbols.js | 1 +
esm/dom/text.js | 8 ++-
esm/hydro.js | 78 ++++++++++++++++++++----
esm/parser.js | 13 +++-
esm/persistent-fragment.js | 9 ---
package.json | 2 +-
rollup/ssr.cjs | 4 +-
test/hydro.html | 12 ++--
test/hydro.mjs | 2 +
test/parser.mjs | 2 +-
test/ssr.mjs | 2 +-
test/virtual.mjs | 118 +++++++++++++++++++++++++++++++++++++
13 files changed, 220 insertions(+), 34 deletions(-)
create mode 100644 test/virtual.mjs
diff --git a/esm/dom/document.js b/esm/dom/document.js
index 53a1604..a837a83 100644
--- a/esm/dom/document.js
+++ b/esm/dom/document.js
@@ -2,7 +2,7 @@ import { DOCUMENT_NODE } from 'domconstants/constants';
import { setParentNode } from './utils.js';
-import { childNodes, documentElement, nodeName, ownerDocument } from './symbols.js';
+import { childNodes, documentElement, nodeName, ownerDocument, __chunks__ } from './symbols.js';
import Attribute from './attribute.js';
import Comment from './comment.js';
@@ -33,6 +33,7 @@ export default class Document extends Parent {
this[doctype] = null;
this[head] = null;
this[body] = null;
+ this[__chunks__] = false;
if (type === 'html') {
const html = (this[documentElement] = new Element(type, this));
this[childNodes] = [
diff --git a/esm/dom/symbols.js b/esm/dom/symbols.js
index 6e5bd52..2855b1a 100644
--- a/esm/dom/symbols.js
+++ b/esm/dom/symbols.js
@@ -9,3 +9,4 @@ export const parentNode = Symbol('parentNode');
export const attributes = Symbol('attributes');
export const name = Symbol('name');
export const value = Symbol('value');
+export const __chunks__ = Symbol();
diff --git a/esm/dom/text.js b/esm/dom/text.js
index 7815238..ed78dc6 100644
--- a/esm/dom/text.js
+++ b/esm/dom/text.js
@@ -3,7 +3,7 @@ import { TEXT_ELEMENTS } from 'domconstants/re';
import { escape } from 'html-escaper';
import CharacterData from './character-data.js';
-import { parentNode, localName, ownerDocument, value } from './symbols.js';
+import { parentNode, localName, ownerDocument, value, __chunks__ } from './symbols.js';
export default class Text extends CharacterData {
constructor(data = '', owner = null) {
@@ -17,6 +17,10 @@ export default class Text extends CharacterData {
toString() {
const { [parentNode]: parent, [value]: data } = this;
return parent && TEXT_ELEMENTS.test(parent[localName]) ?
- data : escape(data);
+ data :
+ (this[ownerDocument]?.[__chunks__] && this.previousSibling?.nodeType === TEXT_NODE ?
+ `${escape(data)}` :
+ escape(data)
+ );
}
}
diff --git a/esm/hydro.js b/esm/hydro.js
index 2943658..432a229 100644
--- a/esm/hydro.js
+++ b/esm/hydro.js
@@ -1,4 +1,4 @@
-import { PersistentFragment } from './persistent-fragment.js';
+import { COMMENT_NODE, TEXT_NODE } from 'domconstants/constants';
import { abc, cache, detail } from './literals.js';
import { empty, find, set } from './utils.js';
import { array, hole } from './handler.js';
@@ -14,24 +14,79 @@ import {
const parseHTML = parse(false, true);
const parseSVG = parse(true, true);
-const hydrate = (fragment, {s, t, v}) => {
+const parent = () => ({ childNodes: [] });
+
+const skip = (node, data) => {
+
+};
+
+const reMap = (parentNode, { childNodes }) => {
+ for (let first = true, { length } = childNodes; length--;) {
+ let node = childNodes[length];
+ switch (node.nodeType) {
+ case COMMENT_NODE:
+ if (node.data === '>') {
+ let nested = 0;
+ while (node = node.previousSibling) {
+ length--;
+ if (node.nodeType === COMMENT_NODE) {
+ if (node.data === '>') nested++;
+ else if (node.data === '<>') {
+ if (!nested--) break;
+ }
+ }
+ else
+ parentNode.childNodes.unshift(node);
+ }
+ }
+ else if (/\[(\d+)\]/.test(node.data)) {
+ let many = +RegExp.$1;
+ parentNode.childNodes.unshift(node);
+ while (many--) {
+ node = node.previousSibling;
+ if (node.nodeType === COMMENT_NODE && node.data === '}') {
+ node = skip(node, '{');
+ }
+ }
+ }
+ break;
+ case TEXT_NODE:
+ // ignore browser artifacts on closing fragments
+ if (first && !node.data.trim()) break;
+ default:
+ parentNode.childNodes.unshift(node);
+ break;
+ }
+ first = false;
+ }
+ return parentNode;
+};
+
+const hydrate = (root, {s, t, v}) => {
+ debugger;
const { b: entries, c: direct } = (s ? parseSVG : parseHTML)(t, v);
const { length } = entries;
- if (length !== v.length) return noHydration;
- let root = fragment, details = length ? [] : empty;
- if (!direct) {
- if (
- fragment.firstChild?.data !== '<>' ||
- fragment.lastChild?.data !== '>'
- ) return noHydration;
- root = PersistentFragment.adopt(fragment);
- }
+ // let's assume hydro is used on purpose with valid templates
+ // to use entries meaningfully re-map the container.
+ // This is complicated yet possible.
+ // * fragments are allowed only top-level
+ // * nested fragments will likely be wrapped in holes
+ // * arrays can point at either fragments, DOM nodes, or holes
+ // * arrays can't be path-addressed if not for the comment itself
+ // * ideally their previous content should be pre-populated with nodes, holes and fragments
+ // * it is possible that the whole dance is inside-out so that nested normalized content
+ // can be then addressed (as already live) by the outer content
+ const fake = reMap(parent(), root, direct);
+ const details = length ? [] : empty;
for (let current, prev, i = 0; i < length; i++) {
const { a: path, b: update, c: name } = entries[i];
+ // adjust the length of the first path node
+ if (!direct) path[path.length - 1]++;
// TODO: node should be adjusted if it's array or hole
// * if it's array, no way caching it as current helps
// * if it's a hole or attribute/text thing, current helps
let node = path === prev ? current : (current = find(root, (prev = path)));
+ if (!direct) path[path.length - 1]--;
details[i] = detail(
update,
node,
@@ -45,7 +100,6 @@ const hydrate = (fragment, {s, t, v}) => {
};
const known = new WeakMap;
-const noHydration = cache();
const render = (where, what) => {
const hole = typeof what === 'function' ? what() : what;
diff --git a/esm/parser.js b/esm/parser.js
index ecac08b..6ceb9bf 100644
--- a/esm/parser.js
+++ b/esm/parser.js
@@ -44,7 +44,7 @@ const resolve = (template, values, xml, holed) => {
let entries = empty, markup = parser(template, prefix, xml);
if (holed) markup = markup.replace(
new RegExp(``, 'g'),
- '$&'
+ '$&'
);
const content = createContent(markup, xml);
const { length } = template;
@@ -54,16 +54,25 @@ const resolve = (template, values, xml, holed) => {
let i = 0, search = `${prefix}${i++}`;
entries = [];
while (i < length) {
- const node = tw.nextNode();
+ let node = tw.nextNode();
// these are holes or arrays
if (node.nodeType === COMMENT_NODE) {
if (node.data === search) {
// ⚠️ once array, always array!
const update = isArray(values[i - 1]) ? array : hole;
if (update === hole) replace.push(node);
+ else if (holed) {
+ // ⚠️ this operation works only with uhtml/dom
+ // it would bail out native TreeWalker
+ const { previousSibling, nextSibling } = node;
+ previousSibling.data = '[]';
+ nextSibling.remove();
+ }
entries.push(abc(createPath(node), update, null));
search = `${prefix}${i++}`;
}
+ // ⚠️ this operation works only with uhtml/dom
+ else if (holed && node.data === '#') node.remove();
}
else {
let path;
diff --git a/esm/persistent-fragment.js b/esm/persistent-fragment.js
index 5c6a8f8..7a90d06 100644
--- a/esm/persistent-fragment.js
+++ b/esm/persistent-fragment.js
@@ -28,15 +28,6 @@ const comment = value => document.createComment(value);
/** @extends {DocumentFragment} */
export class PersistentFragment extends custom(DocumentFragment) {
- static adopt(content) {
- const pf = new PersistentFragment(
- document.createDocumentFragment()
- );
- pf.#firstChild = content.firstChild;
- pf.#lastChild = content.lastChild;
- pf.#nodes = [...content.childNodes];
- return pf;
- }
#firstChild = comment('<>');
#lastChild = comment('>');
#nodes = empty;
diff --git a/package.json b/package.json
index f3bee74..8034565 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
"rollup:es": "rollup --config rollup/es.config.js",
"rollup:init": "rollup --config rollup/init.config.js",
"server": "npx static-handler .",
- "size": "echo \"index $(cat index.js | brotli | wc -c)\";echo \"keyed $(cat keyed.js | brotli | wc -c)\";echo \"reactive $(cat reactive.js | brotli | wc -c)\";echo \"preactive $(cat preactive.js | brotli | wc -c)\";echo \"signal $(cat signal.js | brotli | wc -c)\";echo \"node $(cat node.js | brotli | wc -c)\";",
+ "size": "echo \"index $(cat index.js | brotli | wc -c)\";echo \"keyed $(cat keyed.js | brotli | wc -c)\";echo \"reactive $(cat reactive.js | brotli | wc -c)\";echo \"preactive $(cat preactive.js | brotli | wc -c)\";echo \"signal $(cat signal.js | brotli | wc -c)\";echo \"node $(cat node.js | brotli | wc -c)\";echo \"hydro $(cat hydro.js | brotli | wc -c)\";",
"test": "c8 node test/coverage.js && node test/modern.mjs",
"coverage": "mkdir -p ./coverage; c8 report --reporter=text-lcov > ./coverage/lcov.info",
"ts": "tsc -p ."
diff --git a/rollup/ssr.cjs b/rollup/ssr.cjs
index 1256581..29e1d45 100644
--- a/rollup/ssr.cjs
+++ b/rollup/ssr.cjs
@@ -6,7 +6,8 @@ const uhtml = readFileSync(init).toString();
const content = [
'const document = content ? new DOMParser().parseFromString(content, ...rest) : new Document;',
- 'const { constructor: DocumentFragment } = document.createDocumentFragment();'
+ 'const { constructor: DocumentFragment } = document.createDocumentFragment();',
+ 'document[__chunks__] = true;',
];
writeFileSync(init, `
@@ -14,6 +15,7 @@ writeFileSync(init, `
import Document from './dom/document.js';
import DOMParser from './dom/dom-parser.js';
+import { __chunks__ } from './dom/symbols.js';
/**
* @param {Document} document
diff --git a/test/hydro.html b/test/hydro.html
index 7c36415..faf5cbc 100644
--- a/test/hydro.html
+++ b/test/hydro.html
@@ -7,7 +7,9 @@
import { render, html } from '../hydro.js';
function App(state) {
return html`
+ ${state.title}