malevic
Version:
Malevič.js - minimalistic reactive UI library
1,194 lines (1,175 loc) • 35.2 kB
JavaScript
/* malevic@0.20.2 - Aug 10, 2024 */
function createPluginsStore() {
const plugins = [];
return {
add(plugin) {
plugins.push(plugin);
return this;
},
apply(props) {
let result;
let plugin;
const usedPlugins = new Set();
for (let i = plugins.length - 1; i >= 0; i--) {
plugin = plugins[i];
if (usedPlugins.has(plugin)) {
continue;
}
result = plugin(props);
if (result != null) {
return result;
}
usedPlugins.add(plugin);
}
return null;
},
delete(plugin) {
for (let i = plugins.length - 1; i >= 0; i--) {
if (plugins[i] === plugin) {
plugins.splice(i, 1);
break;
}
}
return this;
},
empty() {
return plugins.length === 0;
},
};
}
function iterateComponentPlugins(type, pairs, iterator) {
pairs
.filter(([key]) => type[key])
.forEach(([key, plugins]) => {
return type[key].forEach((plugin) => iterator(plugins, plugin));
});
}
function addComponentPlugins(type, pairs) {
iterateComponentPlugins(type, pairs, (plugins, plugin) => plugins.add(plugin));
}
function deleteComponentPlugins(type, pairs) {
iterateComponentPlugins(type, pairs, (plugins, plugin) => plugins.delete(plugin));
}
function createPluginsAPI(key) {
const api = {
add(type, plugin) {
if (!type[key]) {
type[key] = [];
}
type[key].push(plugin);
return api;
},
};
return api;
}
const XHTML_NS = 'http://www.w3.org/1999/xhtml';
const SVG_NS = 'http://www.w3.org/2000/svg';
const PLUGINS_CREATE_ELEMENT = Symbol();
const pluginsCreateElement = createPluginsStore();
function createElement(spec, parent) {
const result = pluginsCreateElement.apply({ spec, parent });
if (result) {
return result;
}
const tag = spec.type;
if (tag === 'svg') {
return document.createElementNS(SVG_NS, 'svg');
}
const namespace = parent.namespaceURI;
if (namespace === XHTML_NS || namespace == null) {
return document.createElement(tag);
}
return document.createElementNS(namespace, tag);
}
function classes(...args) {
const classes = [];
const process = (c) => {
if (!c)
return;
if (typeof c === 'string') {
classes.push(c);
}
else if (Array.isArray(c)) {
c.forEach(process);
}
else if (typeof c === 'object') {
classes.push(...Object.keys(c).filter((key) => Boolean(c[key])));
}
};
args.forEach(process);
return classes.join(' ');
}
function setInlineCSSPropertyValue(element, prop, $value) {
if ($value != null && $value !== '') {
let value = String($value);
let important = '';
if (value.endsWith('!important')) {
value = value.substring(0, value.length - 10);
important = 'important';
}
element.style.setProperty(prop, value, important);
}
else {
element.style.removeProperty(prop);
}
}
function isObject(value) {
return value != null && typeof value === 'object';
}
const eventListeners = new WeakMap();
function addEventListener(element, event, listener) {
let listeners;
if (eventListeners.has(element)) {
listeners = eventListeners.get(element);
}
else {
listeners = new Map();
eventListeners.set(element, listeners);
}
if (listeners.get(event) !== listener) {
if (listeners.has(event)) {
element.removeEventListener(event, listeners.get(event));
}
element.addEventListener(event, listener);
listeners.set(event, listener);
}
}
function removeEventListener(element, event) {
if (!eventListeners.has(element)) {
return;
}
const listeners = eventListeners.get(element);
element.removeEventListener(event, listeners.get(event));
listeners.delete(event);
}
function setClassObject(element, classObj) {
const cls = Array.isArray(classObj)
? classes(...classObj)
: classes(classObj);
if (cls) {
element.setAttribute('class', cls);
}
else {
element.removeAttribute('class');
}
}
function mergeValues(obj, old) {
const values = new Map();
const newProps = new Set(Object.keys(obj));
const oldProps = Object.keys(old);
oldProps
.filter((prop) => !newProps.has(prop))
.forEach((prop) => values.set(prop, null));
newProps.forEach((prop) => values.set(prop, obj[prop]));
return values;
}
function setStyleObject(element, styleObj, prev) {
let prevObj;
if (isObject(prev)) {
prevObj = prev;
}
else {
prevObj = {};
element.removeAttribute('style');
}
const declarations = mergeValues(styleObj, prevObj);
declarations.forEach(($value, prop) => setInlineCSSPropertyValue(element, prop, $value));
}
function setEventListener(element, event, listener) {
if (typeof listener === 'function') {
addEventListener(element, event, listener);
}
else {
removeEventListener(element, event);
}
}
const specialAttrs = new Set([
'key',
'oncreate',
'onupdate',
'onrender',
'onremove',
]);
const PLUGINS_SET_ATTRIBUTE = Symbol();
const pluginsSetAttribute = createPluginsStore();
function getPropertyValue(obj, prop) {
return obj && obj.hasOwnProperty(prop) ? obj[prop] : null;
}
function syncAttrs(element, attrs, prev) {
const values = mergeValues(attrs, prev || {});
values.forEach((value, attr) => {
if (!pluginsSetAttribute.empty()) {
const result = pluginsSetAttribute.apply({
element,
attr,
value,
get prev() {
return getPropertyValue(prev, attr);
},
});
if (result != null) {
return;
}
}
if (attr === 'class' && isObject(value)) {
setClassObject(element, value);
}
else if (attr === 'style' && isObject(value)) {
const prevValue = getPropertyValue(prev, attr);
setStyleObject(element, value, prevValue);
}
else if (attr.startsWith('on')) {
const event = attr.substring(2);
setEventListener(element, event, value);
}
else if (specialAttrs.has(attr)) ;
else if (value == null || value === false) {
element.removeAttribute(attr);
}
else {
element.setAttribute(attr, value === true ? '' : String(value));
}
});
}
class LinkedList {
constructor(...items) {
this.nexts = new WeakMap();
this.prevs = new WeakMap();
this.first = null;
this.last = null;
items.forEach((item) => this.push(item));
}
empty() {
return this.first == null;
}
push(item) {
if (this.empty()) {
this.first = item;
this.last = item;
}
else {
this.nexts.set(this.last, item);
this.prevs.set(item, this.last);
this.last = item;
}
}
insertBefore(newItem, refItem) {
const prev = this.before(refItem);
this.prevs.set(newItem, prev);
this.nexts.set(newItem, refItem);
this.prevs.set(refItem, newItem);
prev && this.nexts.set(prev, newItem);
refItem === this.first && (this.first = newItem);
}
delete(item) {
const prev = this.before(item);
const next = this.after(item);
prev && this.nexts.set(prev, next);
next && this.prevs.set(next, prev);
item === this.first && (this.first = next);
item === this.last && (this.last = prev);
}
before(item) {
return this.prevs.get(item) || null;
}
after(item) {
return this.nexts.get(item) || null;
}
loop(iterator) {
if (this.empty()) {
return;
}
let current = this.first;
do {
if (iterator(current)) {
break;
}
} while ((current = this.after(current)));
}
copy() {
const list = new LinkedList();
this.loop((item) => {
list.push(item);
return false;
});
return list;
}
forEach(iterator) {
this.loop((item) => {
iterator(item);
return false;
});
}
find(iterator) {
let result = null;
this.loop((item) => {
if (iterator(item)) {
result = item;
return true;
}
return false;
});
return result;
}
map(iterator) {
const results = [];
this.loop((item) => {
results.push(iterator(item));
return false;
});
return results;
}
}
function matchChildren(vnode, old) {
const oldChildren = old.children();
const oldChildrenByKey = new Map();
const oldChildrenWithoutKey = [];
oldChildren.forEach((v) => {
const key = v.key();
if (key == null) {
oldChildrenWithoutKey.push(v);
}
else {
oldChildrenByKey.set(key, v);
}
});
const children = vnode.children();
const matches = [];
const unmatched = new Set(oldChildren);
const keys = new Set();
children.forEach((v) => {
let match = null;
let guess = null;
const key = v.key();
if (key != null) {
if (keys.has(key)) {
throw new Error('Duplicate key');
}
keys.add(key);
if (oldChildrenByKey.has(key)) {
guess = oldChildrenByKey.get(key);
}
}
else if (oldChildrenWithoutKey.length > 0) {
guess = oldChildrenWithoutKey.shift();
}
if (v.matches(guess)) {
match = guess;
}
matches.push([v, match]);
if (match) {
unmatched.delete(match);
}
});
return { matches, unmatched };
}
function execute(vnode, old, vdom) {
const didMatch = vnode && old && vnode.matches(old);
if (didMatch && vnode.parent() === old.parent()) {
vdom.replaceVNode(old, vnode);
}
else if (vnode) {
vdom.addVNode(vnode);
}
const context = vdom.getVNodeContext(vnode);
const oldContext = vdom.getVNodeContext(old);
if (old && !didMatch) {
old.detach(oldContext);
old.children().forEach((v) => execute(null, v, vdom));
old.detached(oldContext);
}
if (vnode && !didMatch) {
vnode.attach(context);
vnode.children().forEach((v) => execute(v, null, vdom));
vnode.attached(context);
}
if (didMatch) {
const result = vnode.update(old, context);
if (result !== vdom.LEAVE) {
const { matches, unmatched } = matchChildren(vnode, old);
unmatched.forEach((v) => execute(null, v, vdom));
matches.forEach(([v, o]) => execute(v, o, vdom));
vnode.updated(context);
}
}
}
function m(tagOrComponent, props, ...children) {
props = props || {};
if (typeof tagOrComponent === 'string') {
const tag = tagOrComponent;
return { type: tag, props, children };
}
if (typeof tagOrComponent === 'function') {
const component = tagOrComponent;
return { type: component, props, children };
}
throw new Error('Unsupported spec type');
}
function isSpec(x) {
return isObject(x) && x.type != null && x.nodeType == null;
}
function isNodeSpec(x) {
return isSpec(x) && typeof x.type === 'string';
}
function isComponentSpec(x) {
return isSpec(x) && typeof x.type === 'function';
}
class VNodeBase {
constructor(parent) {
this.parentVNode = parent;
}
key() {
return null;
}
parent(vnode) {
if (vnode) {
this.parentVNode = vnode;
return;
}
return this.parentVNode;
}
children() {
return [];
}
attach(context) { }
detach(context) { }
update(old, context) {
return null;
}
attached(context) { }
detached(context) { }
updated(context) { }
}
function nodeMatchesSpec(node, spec) {
return (node instanceof Element &&
((node.namespaceURI === XHTML_NS &&
spec.type === node.tagName.toLocaleLowerCase()) ||
(node.namespaceURI !== XHTML_NS && spec.type === node.tagName)));
}
const refinedElements = new WeakMap();
function markElementAsRefined(element, vdom) {
let refined;
if (refinedElements.has(vdom)) {
refined = refinedElements.get(vdom);
}
else {
refined = new WeakSet();
refinedElements.set(vdom, refined);
}
refined.add(element);
}
function isElementRefined(element, vdom) {
return refinedElements.has(vdom) && refinedElements.get(vdom).has(element);
}
class ElementVNode extends VNodeBase {
constructor(spec, parent) {
super(parent);
this.spec = spec;
}
matches(other) {
return (other instanceof ElementVNode && this.spec.type === other.spec.type);
}
key() {
return this.spec.props.key;
}
children() {
return [this.child];
}
getExistingElement(context) {
const parent = context.parent;
const existing = context.node;
let element;
if (nodeMatchesSpec(existing, this.spec)) {
element = existing;
}
else if (!isElementRefined(parent, context.vdom) &&
context.vdom.isDOMNodeCaptured(parent)) {
const sibling = context.sibling;
const guess = sibling
? sibling.nextElementSibling
: parent.firstElementChild;
if (guess && !context.vdom.isDOMNodeCaptured(guess)) {
if (nodeMatchesSpec(guess, this.spec)) {
element = guess;
}
else {
parent.removeChild(guess);
}
}
}
return element;
}
attach(context) {
let element;
const existing = this.getExistingElement(context);
if (existing) {
element = existing;
}
else {
element = createElement(this.spec, context.parent);
markElementAsRefined(element, context.vdom);
}
syncAttrs(element, this.spec.props, null);
this.child = createDOMVNode(element, this.spec.children, this, false);
}
update(prev, context) {
const prevContext = context.vdom.getVNodeContext(prev);
const element = prevContext.node;
syncAttrs(element, this.spec.props, prev.spec.props);
this.child = createDOMVNode(element, this.spec.children, this, false);
}
attached(context) {
const { oncreate, onrender } = this.spec.props;
if (oncreate) {
oncreate(context.node);
}
if (onrender) {
onrender(context.node);
}
}
detached(context) {
const { onremove } = this.spec.props;
if (onremove) {
onremove(context.node);
}
}
updated(context) {
const { onupdate, onrender } = this.spec.props;
if (onupdate) {
onupdate(context.node);
}
if (onrender) {
onrender(context.node);
}
}
}
const symbols = {
CREATED: Symbol(),
REMOVED: Symbol(),
UPDATED: Symbol(),
RENDERED: Symbol(),
ACTIVE: Symbol(),
DEFAULTS_ASSIGNED: Symbol(),
};
const domPlugins = [
[PLUGINS_CREATE_ELEMENT, pluginsCreateElement],
[PLUGINS_SET_ATTRIBUTE, pluginsSetAttribute],
];
class ComponentVNode extends VNodeBase {
constructor(spec, parent) {
super(parent);
this.lock = false;
this.spec = spec;
this.prev = null;
this.store = {};
this.store[symbols.ACTIVE] = this;
}
matches(other) {
return (other instanceof ComponentVNode &&
this.spec.type === other.spec.type);
}
key() {
return this.spec.props.key;
}
children() {
return [this.child];
}
createContext(context) {
const { parent } = context;
const { spec, prev, store } = this;
return {
spec,
prev,
store,
get node() {
return context.node;
},
get nodes() {
return context.nodes;
},
parent,
onCreate: (fn) => (store[symbols.CREATED] = fn),
onUpdate: (fn) => (store[symbols.UPDATED] = fn),
onRemove: (fn) => (store[symbols.REMOVED] = fn),
onRender: (fn) => (store[symbols.RENDERED] = fn),
refresh: () => {
const activeVNode = store[symbols.ACTIVE];
activeVNode.refresh(context);
},
leave: () => context.vdom.LEAVE,
getStore: (defaults) => {
if (defaults && !store[symbols.DEFAULTS_ASSIGNED]) {
Object.entries(defaults).forEach(([prop, value]) => {
store[prop] = value;
});
store[symbols.DEFAULTS_ASSIGNED] = true;
}
return store;
},
};
}
unbox(context) {
const Component = this.spec.type;
const props = this.spec.props;
const children = this.spec.children;
this.lock = true;
const prevContext = ComponentVNode.context;
ComponentVNode.context = this.createContext(context);
let unboxed = null;
try {
unboxed = Component(props, ...children);
}
finally {
ComponentVNode.context = prevContext;
this.lock = false;
}
return unboxed;
}
refresh(context) {
if (this.lock) {
throw new Error('Calling refresh during unboxing causes infinite loop');
}
this.prev = this.spec;
const latestContext = context.vdom.getVNodeContext(this);
const unboxed = this.unbox(latestContext);
if (unboxed === context.vdom.LEAVE) {
return;
}
const prevChild = this.child;
this.child = createVNode(unboxed, this);
context.vdom.execute(this.child, prevChild);
this.updated(context);
}
addPlugins() {
addComponentPlugins(this.spec.type, domPlugins);
}
deletePlugins() {
deleteComponentPlugins(this.spec.type, domPlugins);
}
attach(context) {
this.addPlugins();
const unboxed = this.unbox(context);
const childSpec = unboxed === context.vdom.LEAVE ? null : unboxed;
this.child = createVNode(childSpec, this);
}
update(prev, context) {
this.store = prev.store;
this.prev = prev.spec;
this.store[symbols.ACTIVE] = this;
const prevContext = context.vdom.getVNodeContext(prev);
this.addPlugins();
const unboxed = this.unbox(prevContext);
let result = null;
if (unboxed === context.vdom.LEAVE) {
result = unboxed;
this.child = prev.child;
context.vdom.adoptVNode(this.child, this);
}
else {
this.child = createVNode(unboxed, this);
}
return result;
}
handle(event, context) {
const fn = this.store[event];
if (fn) {
const nodes = context.nodes.length === 0 ? [null] : context.nodes;
fn(...nodes);
}
}
attached(context) {
this.deletePlugins();
this.handle(symbols.CREATED, context);
this.handle(symbols.RENDERED, context);
}
detached(context) {
this.handle(symbols.REMOVED, context);
}
updated(context) {
this.deletePlugins();
this.handle(symbols.UPDATED, context);
this.handle(symbols.RENDERED, context);
}
}
ComponentVNode.context = null;
function getComponentContext() {
return ComponentVNode.context;
}
class TextVNode extends VNodeBase {
constructor(text, parent) {
super(parent);
this.text = text;
}
matches(other) {
return other instanceof TextVNode;
}
children() {
return [this.child];
}
getExistingNode(context) {
const { parent } = context;
let node;
if (context.node instanceof Text) {
node = context.node;
}
else if (!isElementRefined(parent, context.vdom) &&
context.vdom.isDOMNodeCaptured(parent)) {
const sibling = context.sibling;
const guess = sibling ? sibling.nextSibling : parent.firstChild;
if (guess &&
!context.vdom.isDOMNodeCaptured(guess) &&
guess instanceof Text) {
node = guess;
}
}
return node;
}
attach(context) {
const existing = this.getExistingNode(context);
let node;
if (existing) {
node = existing;
node.textContent = this.text;
}
else {
node = document.createTextNode(this.text);
}
this.child = createVNode(node, this);
}
update(prev, context) {
const prevContext = context.vdom.getVNodeContext(prev);
const { node } = prevContext;
if (this.text !== prev.text) {
node.textContent = this.text;
}
this.child = createVNode(node, this);
}
}
class InlineFunctionVNode extends VNodeBase {
constructor(fn, parent) {
super(parent);
this.fn = fn;
}
matches(other) {
return other instanceof InlineFunctionVNode;
}
children() {
return [this.child];
}
call(context) {
const fn = this.fn;
const inlineFnContext = {
parent: context.parent,
get node() {
return context.node;
},
get nodes() {
return context.nodes;
},
};
const result = fn(inlineFnContext);
this.child = createVNode(result, this);
}
attach(context) {
this.call(context);
}
update(prev, context) {
const prevContext = context.vdom.getVNodeContext(prev);
this.call(prevContext);
}
}
class NullVNode extends VNodeBase {
matches(other) {
return other instanceof NullVNode;
}
}
class DOMVNode extends VNodeBase {
constructor(node, childSpecs, parent, isNative) {
super(parent);
this.node = node;
this.childSpecs = childSpecs;
this.isNative = isNative;
}
matches(other) {
return other instanceof DOMVNode && this.node === other.node;
}
wrap() {
this.childVNodes = this.childSpecs.map((spec) => createVNode(spec, this));
}
insertNode(context) {
const { parent, sibling } = context;
const shouldInsert = !(parent === this.node.parentElement &&
sibling === this.node.previousSibling);
if (shouldInsert) {
const target = sibling ? sibling.nextSibling : parent.firstChild;
parent.insertBefore(this.node, target);
}
}
attach(context) {
this.wrap();
this.insertNode(context);
}
detach(context) {
context.parent.removeChild(this.node);
}
update(prev, context) {
this.wrap();
this.insertNode(context);
}
cleanupDOMChildren(context) {
const element = this.node;
for (let current = element.lastChild; current != null;) {
if (context.vdom.isDOMNodeCaptured(current)) {
current = current.previousSibling;
}
else {
const prev = current.previousSibling;
element.removeChild(current);
current = prev;
}
}
}
refine(context) {
if (!this.isNative) {
this.cleanupDOMChildren(context);
}
const element = this.node;
markElementAsRefined(element, context.vdom);
}
attached(context) {
const { node } = this;
if (node instanceof Element &&
!isElementRefined(node, context.vdom) &&
context.vdom.isDOMNodeCaptured(node)) {
this.refine(context);
}
}
children() {
return this.childVNodes;
}
}
function isDOMVNode(v) {
return v instanceof DOMVNode;
}
function createDOMVNode(node, childSpecs, parent, isNative) {
return new DOMVNode(node, childSpecs, parent, isNative);
}
class ArrayVNode extends VNodeBase {
constructor(items, key, parent) {
super(parent);
this.items = items;
this.id = key;
}
matches(other) {
return other instanceof ArrayVNode;
}
key() {
return this.id;
}
children() {
return this.childVNodes;
}
wrap() {
this.childVNodes = this.items.map((spec) => createVNode(spec, this));
}
attach() {
this.wrap();
}
update() {
this.wrap();
}
}
function createVNode(spec, parent) {
if (isNodeSpec(spec)) {
return new ElementVNode(spec, parent);
}
if (isComponentSpec(spec)) {
if (spec.type === Array) {
return new ArrayVNode(spec.children, spec.props.key, parent);
}
return new ComponentVNode(spec, parent);
}
if (typeof spec === 'string') {
return new TextVNode(spec, parent);
}
if (spec == null) {
return new NullVNode(parent);
}
if (typeof spec === 'function') {
return new InlineFunctionVNode(spec, parent);
}
if (spec instanceof Node) {
return createDOMVNode(spec, [], parent, true);
}
if (Array.isArray(spec)) {
return new ArrayVNode(spec, null, parent);
}
throw new Error('Unable to create virtual node for spec');
}
function createVDOM(rootNode) {
const contexts = new WeakMap();
const hubs = new WeakMap();
const parentNodes = new WeakMap();
const passingLinks = new WeakMap();
const linkedParents = new WeakSet();
const LEAVE = Symbol();
function execute$1(vnode, old) {
execute(vnode, old, vdom);
}
function creatVNodeContext(vnode) {
const parentNode = parentNodes.get(vnode);
contexts.set(vnode, {
parent: parentNode,
get node() {
const linked = passingLinks
.get(vnode)
.find((link) => link.node != null);
return linked ? linked.node : null;
},
get nodes() {
return passingLinks
.get(vnode)
.map((link) => link.node)
.filter((node) => node);
},
get sibling() {
if (parentNode === rootNode.parentElement) {
return passingLinks.get(vnode).first.node.previousSibling;
}
const hub = hubs.get(parentNode);
let current = passingLinks.get(vnode).first;
while ((current = hub.links.before(current))) {
if (current.node) {
return current.node;
}
}
return null;
},
vdom,
});
}
function createRootVNodeLinks(vnode) {
const parentNode = rootNode.parentElement || document.createDocumentFragment();
const node = rootNode;
const links = new LinkedList({
parentNode,
node,
});
passingLinks.set(vnode, links.copy());
parentNodes.set(vnode, parentNode);
hubs.set(parentNode, {
node: parentNode,
links,
});
}
function createVNodeLinks(vnode) {
const parent = vnode.parent();
const isBranch = linkedParents.has(parent);
const parentNode = isDOMVNode(parent)
? parent.node
: parentNodes.get(parent);
parentNodes.set(vnode, parentNode);
const vnodeLinks = new LinkedList();
passingLinks.set(vnode, vnodeLinks);
if (isBranch) {
const newLink = {
parentNode,
node: null,
};
let current = vnode;
do {
passingLinks.get(current).push(newLink);
current = current.parent();
} while (current && !isDOMVNode(current));
hubs.get(parentNode).links.push(newLink);
}
else {
linkedParents.add(parent);
const links = isDOMVNode(parent)
? hubs.get(parentNode).links
: passingLinks.get(parent);
links.forEach((link) => vnodeLinks.push(link));
}
}
function connectDOMVNode(vnode) {
if (isDOMVNode(vnode)) {
const { node } = vnode;
hubs.set(node, {
node,
links: new LinkedList({
parentNode: node,
node: null,
}),
});
passingLinks.get(vnode).forEach((link) => (link.node = node));
}
}
function addVNode(vnode) {
const parent = vnode.parent();
if (parent == null) {
createRootVNodeLinks(vnode);
}
else {
createVNodeLinks(vnode);
}
connectDOMVNode(vnode);
creatVNodeContext(vnode);
}
function getVNodeContext(vnode) {
return contexts.get(vnode);
}
function getAncestorsLinks(vnode) {
const parentNode = parentNodes.get(vnode);
const hub = hubs.get(parentNode);
const allLinks = [];
let current = vnode;
while ((current = current.parent()) && !isDOMVNode(current)) {
allLinks.push(passingLinks.get(current));
}
allLinks.push(hub.links);
return allLinks;
}
function replaceVNode(old, vnode) {
if (vnode.parent() == null) {
addVNode(vnode);
return;
}
const oldContext = contexts.get(old);
const { parent: parentNode } = oldContext;
parentNodes.set(vnode, parentNode);
const oldLinks = passingLinks.get(old);
const newLink = {
parentNode,
node: null,
};
getAncestorsLinks(vnode).forEach((links) => {
const nextLink = links.after(oldLinks.last);
oldLinks.forEach((link) => links.delete(link));
if (nextLink) {
links.insertBefore(newLink, nextLink);
}
else {
links.push(newLink);
}
});
const vnodeLinks = new LinkedList(newLink);
passingLinks.set(vnode, vnodeLinks);
creatVNodeContext(vnode);
}
function adoptVNode(vnode, parent) {
const vnodeLinks = passingLinks.get(vnode);
const parentLinks = passingLinks.get(parent).copy();
vnode.parent(parent);
getAncestorsLinks(vnode).forEach((links) => {
vnodeLinks.forEach((link) => links.insertBefore(link, parentLinks.first));
parentLinks.forEach((link) => links.delete(link));
});
}
function isDOMNodeCaptured(node) {
return hubs.has(node) && node !== rootNode.parentElement;
}
const vdom = {
execute: execute$1,
addVNode,
getVNodeContext,
replaceVNode,
adoptVNode,
isDOMNodeCaptured,
LEAVE,
};
return vdom;
}
const roots = new WeakMap();
const vdoms = new WeakMap();
function realize(node, vnode) {
const old = roots.get(node) || null;
roots.set(node, vnode);
let vdom;
if (vdoms.has(node)) {
vdom = vdoms.get(node);
}
else {
vdom = createVDOM(node);
vdoms.set(node, vdom);
}
vdom.execute(vnode, old);
return vdom.getVNodeContext(vnode);
}
function render(element, spec) {
const vnode = createDOMVNode(element, Array.isArray(spec) ? spec : [spec], null, false);
realize(element, vnode);
return element;
}
function sync(node, spec) {
const vnode = createVNode(spec, null);
const context = realize(node, vnode);
const { nodes } = context;
if (nodes.length !== 1 || nodes[0] !== node) {
throw new Error('Spec does not match the node');
}
return nodes[0];
}
function teardown(node) {
roots.delete(node);
vdoms.delete(node);
}
function createComponent(fn) {
const component = (props, ...children) => {
const context = getComponentContext();
return fn(context, props, ...children);
};
return (props, ...children) => {
return m(component, props, ...children);
};
}
function normalize(attrsOrChild, ...otherChildren) {
const attrs = isObject(attrsOrChild) && !isSpec(attrsOrChild) ? attrsOrChild : null;
const children = attrs == null
? [attrsOrChild].concat(otherChildren)
: otherChildren;
return { attrs, children };
}
function createTagFunction(tag) {
return (attrsOrChild, ...otherChildren) => {
const { attrs, children } = normalize(attrsOrChild, otherChildren);
return m(tag, attrs, children);
};
}
const tags = new Proxy({}, {
get: (_, tag) => {
return createTagFunction(tag);
},
});
function specFromNode(node) {
return walkNode(node);
}
function walkNode(node) {
if (node instanceof Text) {
return node.textContent
.trim()
.replaceAll(/\r/g, '')
.replaceAll(/\s*?\n\s*/g, '\n');
}
if (!(node instanceof Element)) {
return null;
}
const tag = node.namespaceURI === XHTML_NS
? node.tagName.toLocaleLowerCase()
: node.tagName;
const attrs = {};
for (let i = 0; i < node.attributes.length; i++) {
const attr = node.attributes[i];
attrs[attr.name] = String(attr.value);
}
const children = Array.from(node.childNodes)
.map(walkNode)
.filter((c) => c === null ||
(typeof c === 'string' && c !== '') ||
typeof c === 'object');
return m(tag, attrs, ...children);
}
const plugins = {
createElement: createPluginsAPI(PLUGINS_CREATE_ELEMENT),
setAttribute: createPluginsAPI(PLUGINS_SET_ATTRIBUTE),
};
export { createComponent as component, getComponentContext as getContext, plugins, render, specFromNode, sync, createTagFunction as tag, tags, teardown };