Versie 0.2.0

This commit is contained in:
Atreides Assets committed 2026-10-29 11:57:29 +01:00
commit e77d8fd373
495 files changed
+81026

No files matched your search

+459
View File
@@ -0,0 +1,459 @@
// Generated, do not edit. sha256:db0c8db151ef8689662b07ab525d02d912551c1706e34e778f8bcf4190dc7dc0
const VOID = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'source', 'track', 'wbr']);
const RAW_TEXT = new Set(['textarea', 'script', 'style', 'title']);
const SELF_CLOSING_OK = new Set(['svg', 'path', 'line', 'circle', 'rect', 'polyline', 'polygon', 'g', 'use']);
const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
export function decodeEntities(s) {
return s.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (all, name) => {
if (name[0] === '#') {
const code = name[1] === 'x' || name[1] === 'X' ? parseInt(name.slice(2), 16) : parseInt(name.slice(1), 10);
return Number.isFinite(code) ? String.fromCodePoint(code) : all;
}
return Object.prototype.hasOwnProperty.call(ENTITIES, name.toLowerCase()) ? ENTITIES[name.toLowerCase()] : all;
});
}
const escText = s => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const escAttr = s => String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;');
const hidden = (obj, key, value) => Object.defineProperty(obj, key, { value, writable: true, enumerable: false, configurable: true });
class Node {
constructor(doc) { hidden(this, 'ownerDocument', doc); hidden(this, 'parentNode', null); this.childNodes = []; }
get parentElement() { return this.parentNode instanceof Element ? this.parentNode : null; }
get isConnected() {
let n = this;
while (n) { if (n === this.ownerDocument) return true; n = n.parentNode; }
return false;
}
appendChild(child) {
if (child.parentNode) child.parentNode._removeChild(child);
child.parentNode = this;
this.childNodes.push(child);
return child;
}
append(...children) { for (const c of children) this.appendChild(c); }
_removeChild(child) {
const i = this.childNodes.indexOf(child);
if (i >= 0) this.childNodes.splice(i, 1);
child.parentNode = null;
}
remove() { if (this.parentNode) this.parentNode._removeChild(this); }
contains(other) {
for (let n = other; n; n = n.parentNode) if (n === this) return true;
return false;
}
}
class Text extends Node {
constructor(doc, data) { super(doc); this.nodeType = 3; this.data = data; }
get textContent() { return this.data; }
set textContent(v) { this.data = String(v); }
}
class ClassList {
constructor(el) { this.el = el; }
_get() { return (this.el.getAttribute('class') || '').split(/\s+/).filter(Boolean); }
_set(list) { this.el.setAttribute('class', list.join(' ')); }
contains(c) { return this._get().includes(c); }
add(...cs) { const l = this._get(); for (const c of cs) if (c && !l.includes(c)) l.push(c); this._set(l); }
remove(...cs) { this._set(this._get().filter(c => !cs.includes(c))); }
toggle(c, force) {
const on = force === undefined ? !this.contains(c) : !!force;
if (on) this.add(c); else this.remove(c);
return on;
}
}
const kebab = s => s.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
const camel = s => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
function makeStyle() {
const props = new Map();
return new Proxy({
setProperty(k, v) { props.set(k, String(v)); },
getPropertyValue(k) { return props.get(k) ?? ''; },
removeProperty(k) { props.delete(k); },
}, {
get(target, key) { return key in target ? target[key] : (props.get(kebab(String(key))) ?? ''); },
set(_t, key, v) { props.set(kebab(String(key)), String(v)); return true; },
});
}
export class Element extends Node {
constructor(doc, tag) {
super(doc);
this.nodeType = 1;
this.localName = tag.toLowerCase();
this.attrs = new Map();
hidden(this, 'style', makeStyle());
hidden(this, 'classList', new ClassList(this));
hidden(this, 'listeners', []);
this.scrollTop = 0;
this.scrollLeft = 0;
}
get tagName() { return this.localName.toUpperCase(); }
[Symbol.for('nodejs.util.inspect.custom')]() {
const attrs = [...this.attrs].map(([k, v]) => (v === '' ? ` ${k}` : ` ${k}="${v}"`)).join('');
return `<${this.localName}${attrs}>`;
}
getAttribute(n) { return this.attrs.has(n.toLowerCase()) ? this.attrs.get(n.toLowerCase()) : null; }
setAttribute(n, v) { this.attrs.set(n.toLowerCase(), String(v)); }
removeAttribute(n) { this.attrs.delete(n.toLowerCase()); }
hasAttribute(n) { return this.attrs.has(n.toLowerCase()); }
get id() { return this.getAttribute('id') || ''; }
set id(v) { this.setAttribute('id', v); }
get className() { return this.getAttribute('class') || ''; }
set className(v) { this.setAttribute('class', v); }
get title() { return this.getAttribute('title') || ''; }
set title(v) { this.setAttribute('title', v); }
get src() { return this.getAttribute('src') || ''; }
set src(v) { this.setAttribute('src', v); }
get type() { return this.localName === 'input' ? (this.getAttribute('type') || 'text').toLowerCase() : (this.getAttribute('type') || ''); }
set type(v) { this.setAttribute('type', v); }
get disabled() { return this.hasAttribute('disabled'); }
set disabled(v) { if (v) this.setAttribute('disabled', ''); else this.removeAttribute('disabled'); }
get dataset() {
const el = this;
return new Proxy({}, {
get(_t, key) { return typeof key === 'string' ? (el.getAttribute('data-' + kebab(key)) ?? undefined) : undefined; },
set(_t, key, v) { el.setAttribute('data-' + kebab(String(key)), v); return true; },
has(_t, key) { return el.hasAttribute('data-' + kebab(String(key))); },
ownKeys() { return [...el.attrs.keys()].filter(k => k.startsWith('data-')).map(k => camel(k.slice(5))); },
getOwnPropertyDescriptor(_t, key) {
const v = el.getAttribute('data-' + kebab(String(key)));
return v === null ? undefined : { value: v, enumerable: true, configurable: true };
},
});
}
get children() { return this.childNodes.filter(n => n instanceof Element); }
get value() {
if (this.localName === 'select') {
if (this._value !== undefined) return this._value;
const opts = this.querySelectorAll('option');
const sel = opts.find(o => o.hasAttribute('selected')) || opts[0];
return sel ? sel.value : '';
}
if (this.localName === 'option') return this.getAttribute('value') ?? this.textContent;
if (this.localName === 'textarea') return this._value !== undefined ? this._value : this.textContent;
return this._value !== undefined ? this._value : (this.getAttribute('value') ?? '');
}
set value(v) { this._value = String(v); }
get checked() { return this._checked !== undefined ? this._checked : this.hasAttribute('checked'); }
set checked(v) { this._checked = !!v; }
get selectionStart() { return this._selStart ?? this.value.length; }
get selectionEnd() { return this._selEnd ?? this.value.length; }
setSelectionRange(a, b) { this._selStart = a; this._selEnd = b; }
get offsetHeight() { return 0; }
get offsetWidth() { return 0; }
get scrollHeight() { return 0; }
get clientHeight() { return 0; }
get clientWidth() { return 0; }
getBoundingClientRect() { return { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }; }
scrollIntoView(opts) { this.ownerDocument.scrolledIntoView.push({ el: this, opts }); }
focus() { this.ownerDocument.activeElement = this; }
blur() { if (this.ownerDocument.activeElement === this) this.ownerDocument.activeElement = this.ownerDocument.body; }
get innerHTML() { return this.childNodes.map(serialize).join(''); }
set innerHTML(html) {
for (const c of [...this.childNodes]) this._removeChild(c);
for (const c of parseHTML(this.ownerDocument, String(html), this.localName)) this.appendChild(c);
this._value = undefined;
}
get outerHTML() { return serialize(this); }
get textContent() { return this.childNodes.map(n => n.textContent).join(''); }
set textContent(v) {
for (const c of [...this.childNodes]) this._removeChild(c);
if (v !== '' && v != null) this.appendChild(new Text(this.ownerDocument, String(v)));
}
querySelectorAll(sel) {
const list = parseSelector(sel);
const out = [];
walk(this, el => { if (list.some(s => matchComplex(el, s, this))) out.push(el); });
return out;
}
querySelector(sel) { return this.querySelectorAll(sel)[0] || null; }
matches(sel) { return parseSelector(sel).some(s => matchComplex(this, s, null)); }
closest(sel) {
const list = parseSelector(sel);
for (let n = this; n instanceof Element; n = n.parentNode) if (list.some(s => matchComplex(n, s, null))) return n;
return null;
}
addEventListener(type, fn, opts) { this.listeners.push({ type, fn, capture: opts === true || !!(opts && opts.capture) }); }
removeEventListener(type, fn, opts) {
const capture = opts === true || !!(opts && opts.capture);
this.listeners = this.listeners.filter(l => !(l.type === type && l.fn === fn && l.capture === capture));
}
dispatchEvent(ev) { return dispatch(this, ev.type, { bubbles: ev.bubbles, ...pickInit(ev) }); }
}
function pickInit(ev) {
const init = {};
for (const k of ['key', 'ctrlKey', 'metaKey', 'shiftKey', 'altKey', 'relatedTarget', 'detail']) if (ev[k] !== undefined) init[k] = ev[k];
return init;
}
function walk(root, fn) {
for (const c of root.childNodes) {
if (c instanceof Element) { fn(c); walk(c, fn); }
}
}
function serialize(n) {
if (n instanceof Text) return escText(n.data);
if (!(n instanceof Element)) return '';
const attrs = [...n.attrs].map(([k, v]) => (v === '' ? ` ${k}` : ` ${k}="${escAttr(v)}"`)).join('');
if (VOID.has(n.localName)) return `<${n.localName}${attrs}>`;
return `<${n.localName}${attrs}>${n.childNodes.map(serialize).join('')}</${n.localName}>`;
}
export function parseHTML(doc, html) {
const root = new Element(doc, '#fragment');
const stack = [root];
const top = () => stack[stack.length - 1];
let i = 0;
while (i < html.length) {
if (html.startsWith('<!--', i)) {
const end = html.indexOf('-->', i + 4);
i = end < 0 ? html.length : end + 3;
continue;
}
if (html[i] === '<' && html[i + 1] === '/') {
const end = html.indexOf('>', i);
const name = html.slice(i + 2, end < 0 ? html.length : end).trim().toLowerCase();
i = end < 0 ? html.length : end + 1;
const at = stack.map(n => n.localName).lastIndexOf(name);
if (at > 0) stack.length = at;
continue;
}
if (html[i] === '<' && /[a-zA-Z]/.test(html[i + 1] || '')) {
let j = i + 1, quote = null;
for (; j < html.length; j++) {
const ch = html[j];
if (quote) { if (ch === quote) quote = null; continue; }
if (ch === '"' || ch === "'") { quote = ch; continue; }
if (ch === '>') break;
}
let body = html.slice(i + 1, j);
i = j + 1;
const selfClose = body.endsWith('/');
if (selfClose) body = body.slice(0, -1);
const name = body.match(/^[^\s/>]+/)[0].toLowerCase();
const el = new Element(doc, name);
const attrRe = /([^\s"'>\/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
const rest = body.slice(name.length);
let m;
while ((m = attrRe.exec(rest))) {
const v = m[2] ?? m[3] ?? m[4] ?? '';
el.setAttribute(m[1], decodeEntities(v));
}
top().appendChild(el);
if (VOID.has(name) || (selfClose && SELF_CLOSING_OK.has(name))) continue;
if (RAW_TEXT.has(name)) {
const close = html.toLowerCase().indexOf(`</${name}`, i);
const raw = html.slice(i, close < 0 ? html.length : close);
if (raw) el.appendChild(new Text(doc, decodeEntities(raw)));
const end = close < 0 ? html.length : html.indexOf('>', close);
i = end < 0 ? html.length : end + 1;
continue;
}
stack.push(el);
continue;
}
const next = html.indexOf('<', i + 1);
const text = html.slice(i, next < 0 ? html.length : next);
i = next < 0 ? html.length : next;
if (text) top().appendChild(new Text(doc, decodeEntities(text)));
}
const nodes = [...root.childNodes];
for (const n of nodes) root._removeChild(n);
return nodes;
}
const cache = new Map();
export function parseSelector(sel) {
if (cache.has(sel)) return cache.get(sel);
const list = splitTop(sel, ',').map(s => s.trim()).filter(Boolean).map(parseComplex);
if (!list.length) throw new Error(`mini-dom: empty selector "${sel}"`);
cache.set(sel, list);
return list;
}
function splitTop(s, sep) {
const out = [];
let depth = 0, quote = null, start = 0;
for (let i = 0; i < s.length; i++) {
const ch = s[i];
if (quote) { if (ch === quote) quote = null; continue; }
if (ch === '"' || ch === "'") quote = ch;
else if (ch === '(' || ch === '[') depth++;
else if (ch === ')' || ch === ']') depth--;
else if (ch === sep && depth === 0) { out.push(s.slice(start, i)); start = i + 1; }
}
out.push(s.slice(start));
return out;
}
function parseComplex(s) {
const parts = [];
let i = 0;
let combinator = null;
while (i < s.length) {
if (/\s/.test(s[i]) || s[i] === '>') {
let comb = ' ';
while (i < s.length && (/\s/.test(s[i]) || s[i] === '>')) { if (s[i] === '>') comb = '>'; i++; }
if (parts.length) combinator = comb;
continue;
}
const [compound, next] = parseCompound(s, i);
if (parts.length) parts[parts.length - 1].combinator = combinator || ' ';
parts.push({ compound, combinator: null });
combinator = null;
i = next;
}
return parts;
}
function parseCompound(s, i) {
const c = { tag: null, id: null, classes: [], attrs: [], nots: [] };
const ident = (k) => { const m = s.slice(k).match(/^[A-Za-z0-9_-]+/); if (!m) throw new Error(`mini-dom: unsupported selector "${s}"`); return m[0]; };
if (s[i] === '*') i++;
else if (/[A-Za-z]/.test(s[i])) { c.tag = ident(i).toLowerCase(); i += c.tag.length; }
while (i < s.length && !/[\s>]/.test(s[i])) {
const ch = s[i];
if (ch === '#') { const n = ident(i + 1); c.id = n; i += 1 + n.length; }
else if (ch === '.') { const n = ident(i + 1); c.classes.push(n); i += 1 + n.length; }
else if (ch === '[') {
const end = findClose(s, i, '[', ']');
const inner = s.slice(i + 1, end).trim();
const m = inner.match(/^([A-Za-z0-9_:-]+)\s*(?:=\s*(?:"([^"]*)"|'([^']*)'|([^\s\]]+)))?$/);
if (!m) throw new Error(`mini-dom: unsupported attribute selector "[${inner}]"`);
c.attrs.push({ name: m[1].toLowerCase(), value: m[2] ?? m[3] ?? m[4] ?? null });
i = end + 1;
} else if (s.startsWith(':not(', i)) {
const end = findClose(s, i + 4, '(', ')');
c.nots.push(parseSelector(s.slice(i + 5, end)));
i = end + 1;
} else {
throw new Error(`mini-dom: unsupported selector "${s}" at "${s.slice(i)}"`);
}
}
return [c, i];
}
function findClose(s, open, a, b) {
let depth = 0, quote = null;
for (let k = open; k < s.length; k++) {
const ch = s[k];
if (quote) { if (ch === quote) quote = null; continue; }
if (ch === '"' || ch === "'") quote = ch;
else if (ch === a) depth++;
else if (ch === b && --depth === 0) return k;
}
throw new Error(`mini-dom: unclosed "${a}" in selector "${s}"`);
}
function matchCompound(el, c) {
if (!(el instanceof Element)) return false;
if (c.tag && el.localName !== c.tag) return false;
if (c.id && el.getAttribute('id') !== c.id) return false;
for (const k of c.classes) if (!el.classList.contains(k)) return false;
for (const a of c.attrs) {
if (!el.hasAttribute(a.name)) return false;
if (a.value !== null && el.getAttribute(a.name) !== a.value) return false;
}
for (const list of c.nots) if (list.some(s => matchComplex(el, s, null))) return false;
return true;
}
function matchComplex(el, parts, _scope) {
const at = (k, node) => {
if (!matchCompound(node, parts[k].compound)) return false;
if (k === 0) return true;
const comb = parts[k - 1].combinator;
if (comb === '>') return node.parentNode instanceof Element && at(k - 1, node.parentNode);
for (let p = node.parentNode; p instanceof Element; p = p.parentNode) if (at(k - 1, p)) return true;
return false;
};
return at(parts.length - 1, el);
}
export function dispatch(target, type, init = {}) {
const doc = target.ownerDocument || target;
let stopped = false;
const ev = {
type, target, bubbles: init.bubbles !== false, defaultPrevented: false, currentTarget: null,
key: init.key, ctrlKey: !!init.ctrlKey, metaKey: !!init.metaKey, shiftKey: !!init.shiftKey, altKey: !!init.altKey,
relatedTarget: init.relatedTarget ?? null, detail: init.detail ?? null,
stopPropagation() { stopped = true; },
stopImmediatePropagation() { stopped = true; },
preventDefault() { this.defaultPrevented = true; },
};
const path = [];
for (let n = target; n; n = n.parentNode) path.push(n);
if (path[path.length - 1] !== doc) path.push(doc);
const run = (node, capture) => {
ev.currentTarget = node;
if (!capture && typeof node['on' + type] === 'function') node['on' + type](ev);
for (const l of [...(node.listeners || [])]) if (l.type === type && l.capture === capture) l.fn(ev);
};
for (let k = path.length - 1; k > 0 && !stopped; k--) run(path[k], true);
if (!stopped) { run(target, true); if (!stopped) run(target, false); }
if (ev.bubbles) for (let k = 1; k < path.length && !stopped; k++) run(path[k], false);
return !ev.defaultPrevented;
}
class Document extends Node {
constructor() {
super(null);
this.ownerDocument = this;
this.listeners = [];
this.documentElement = new Element(this, 'html');
this.head = new Element(this, 'head');
this.body = new Element(this, 'body');
this.appendChild(this.documentElement);
this.documentElement.appendChild(this.head);
this.documentElement.appendChild(this.body);
this.activeElement = this.body;
this.scrolledIntoView = [];
}
[Symbol.for('nodejs.util.inspect.custom')]() { return '#document'; }
createElement(tag) { return new Element(this, tag); }
createTextNode(t) { return new Text(this, t); }
querySelectorAll(sel) { return this.documentElement.querySelectorAll(sel); }
querySelector(sel) { return this.documentElement.querySelector(sel); }
getElementById(id) { return this.querySelector(`#${id}`); }
addEventListener(type, fn, opts) { Element.prototype.addEventListener.call(this, type, fn, opts); }
removeEventListener(type, fn, opts) { Element.prototype.removeEventListener.call(this, type, fn, opts); }
dispatchEvent(ev) { return dispatch(this, ev.type, { bubbles: ev.bubbles, ...pickInit(ev) }); }
}
export function installDom() {
const document = new Document();
const frames = [];
const win = {
innerWidth: 1280, innerHeight: 800, listeners: [],
addEventListener(type, fn, opts) { Element.prototype.addEventListener.call(this, type, fn, opts); },
removeEventListener(type, fn, opts) { Element.prototype.removeEventListener.call(this, type, fn, opts); },
};
globalThis.document = document;
globalThis.window = win;
globalThis.requestAnimationFrame = fn => { frames.push(fn); return frames.length; };
globalThis.getComputedStyle = el => ({ getPropertyValue: k => el.style.getPropertyValue(k) });
const fire = (el, type, init) => dispatch(el, type, init);
return {
document,
window: win,
fire,
click: el => fire(el, 'click'),
type(el, value) { el.value = value; fire(el, 'input'); fire(el, 'change'); },
choose(el, value) { el.value = value; fire(el, 'change'); },
key: (el, key, mods = {}) => fire(el, 'keydown', { key, ...mods }),
flushFrames() { while (frames.length) frames.shift()(); },
host() { const el = document.createElement('div'); document.body.appendChild(el); return el; },
};
}