ripple
Version:
Ripple is an elegant TypeScript UI framework
278 lines (255 loc) • 9.27 kB
JavaScript
/** @import { AppendIntoAnchor, Block } from '#client' */
import {
TEMPLATE_FRAGMENT,
TEMPLATE_USE_IMPORT_NODE,
TEMPLATE_SVG_NAMESPACE,
TEMPLATE_MATHML_NAMESPACE,
} from '../../../constants.js';
import { H, hydrate_node, hydrating } from './hydration.js';
import { create_text, get_first_child, is_firefox } from './operations.js';
import { active_block, active_namespace } from './runtime.js';
import { DEFAULT_NAMESPACE, NAMESPACE_URI } from './constants.js';
import { HYDRATION } from 'ripple/internal/client/hydration-enabled';
/**
* Assigns start and end nodes to the active block's state.
* @param {Node} start - The start node.
* @param {Node} end - The end node.
*/
export function assign_nodes(start, end) {
var block = /** @type {Block} */ (active_block);
var s = block.s;
if (s === null) {
block.s = {
start,
end,
};
} else if (s.start === null) {
s.start = start;
s.end = end;
}
}
/**
* Parses content that belongs in the SVG or MathML namespace. Installed by
* `template-ns.js` when a template or a dynamic element needs it, so an app
* with HTML templates only ships neither.
* @type {((html: string, ns: 'svg' | 'math') => DocumentFragment) | null}
*/
var parse_ns = null;
/**
* @param {(html: string, ns: 'svg' | 'math') => DocumentFragment} fn
*/
export function set_ns_parser(fn) {
parse_ns = fn;
}
/**
* Creates a DocumentFragment from an HTML string.
* @param {string} html - The HTML string.
* @param {string} ns - `'svg'`, `'math'`, or `''` for HTML.
* @returns {DocumentFragment}
*/
export function create_fragment_from_html(html, ns = '') {
if (ns !== '') {
return /** @type {NonNullable<typeof parse_ns>} */ (parse_ns)(
html,
/** @type {'svg' | 'math'} */ (ns),
);
}
var elem = document.createElement('template');
elem.innerHTML = html;
return elem.content;
}
/**
* A template's content and flags, shared by every clone of one `template()`
* instance, with the parsed node cached for the namespace it was parsed in.
* @typedef {{
* c: string;
* f: number;
* n: Node | DocumentFragment | undefined;
* ns: string | undefined;
* }} TemplateState
*/
/**
* The clone path of a {@link template} instance. Kept out of the per-template
* closure so a hydrating page never compiles it.
* @param {TemplateState} t
* @returns {Node}
*/
function clone_template(t) {
var flags = t.f;
var content = t.c;
var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0;
// The namespace the content parses in: a static one from the flags, else
// the active namespace (a comment placeholder needs none).
var ns =
content === '<!>'
? ''
: (flags & TEMPLATE_SVG_NAMESPACE) !== 0 || active_namespace === 'svg'
? 'svg'
: (flags & TEMPLATE_MATHML_NAMESPACE) !== 0 || active_namespace === 'mathml'
? 'math'
: '';
var node = t.n;
if (node === undefined || t.ns !== ns) {
// Content that opens with a placeholder comment gets one more in
// front, so the first child the compiler navigates to is the element.
node = create_fragment_from_html(content.startsWith('<!>') ? '<!>' + content : content, ns);
if (!is_fragment) node = /** @type {Node} */ (get_first_child(node));
t.n = node;
t.ns = ns;
}
/** @type {DocumentFragment | Node} */
var clone =
(flags & TEMPLATE_USE_IMPORT_NODE) !== 0 || is_firefox
? document.importNode(/** @type {Node} */ (node), true)
: /** @type {Node} */ (node).cloneNode(true);
var start = clone;
var end = clone;
if (is_fragment) {
// we know for sure that children exist
start = /** @type {Node} */ (get_first_child(/** @type {DocumentFragment} */ (clone)));
end = /** @type {Node} */ (/** @type {DocumentFragment} */ (clone).lastChild);
}
assign_nodes(start, end);
return clone;
}
/**
* Creates a template node or fragment from content and flags.
* @param {string} content - The template content.
* @param {number} flags - Flags for template type.
* @param {number} [count] - Pre-calculated count of top-level nodes (for fragments). When provided, avoids runtime parsing.
* @returns {() => Node}
*/
export function template(content, flags = 0, count = 1) {
var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0;
/** @type {TemplateState} */
var t = { c: content, f: flags, n: undefined, ns: undefined };
return () =>
HYDRATION && hydrating
? /** @type {import('./hydrate.js').HydrationRuntime} */ (H).t(is_fragment, count)
: clone_template(t);
}
/**
* The document that owns parsed template content: it has no browsing
* context, so an element created in it does not load its `src` (an image,
* a video) until a clone is adopted into the page, exactly like a node
* parsed from a `<template>`.
* @type {Document | undefined}
*/
var inert_document;
/**
* A nested element of a DOM-built template: its tag, attributes and children,
* trailing absences left out.
* @typedef {[string, (string[] | null)?, (string | TemplateChildren)?]} TemplateElementSpec
*/
/**
* The children of a DOM-built template element in order: a nested element,
* a text string, or `null` for a placeholder comment.
* @typedef {Array<string | null | TemplateElementSpec>} TemplateChildren
*/
/**
* Builds one element of a DOM-built template, in the inert document and
* the active namespace, as a parsed template would create it.
* @param {Document} doc
* @param {keyof typeof NAMESPACE_URI} ns
* @param {string} tag
* @param {string[] | null | undefined} attributes
* @param {string | TemplateChildren | undefined} children
* @returns {Element}
*/
function build_template_element(doc, ns, tag, attributes, children) {
var node =
ns === DEFAULT_NAMESPACE ? doc.createElement(tag) : doc.createElementNS(NAMESPACE_URI[ns], tag);
if (attributes != null) {
for (var i = 0; i < attributes.length; i += 2) {
node.setAttribute(attributes[i], attributes[i + 1]);
}
}
if (children === undefined || typeof children === 'string') {
if (children !== undefined && children !== '') {
node.textContent = children;
}
} else {
for (var j = 0; j < children.length; j++) {
var child = children[j];
node.appendChild(
child === null
? doc.createComment('')
: typeof child === 'string'
? doc.createTextNode(child)
: build_template_element(doc, ns, child[0], child[1], child[2]),
);
}
}
return node;
}
/**
* A template of a few HTML elements with static attributes, built with DOM
* calls instead of parsed: a `<template>` parse has a fixed cost that dwarfs
* the elements themselves, and an app's first render pays it once per
* distinct template, plus the parser's own start-up on a fresh page. The
* compiler decides which templates qualify (`dom_built_template` in the
* client transform); every other shape still parses. The nodes are created
* in the active namespace, as a parsed template would be, and in the inert
* template document when one of them could load a resource (`inert`), also
* as parsed; any other master is built in the document itself, which skips
* creating the inert document on a fresh page.
* @param {string} tag
* @param {string[] | null} [attributes] - flat name/value pairs
* @param {string | TemplateChildren} [children] - one text child, or the
* child nodes in order
* @param {number} [inert] - 1 when an element could load a resource from an
* attribute the master carries
* @returns {() => Node}
*/
export function template_el(tag, attributes = null, children = '', inert = 0) {
/** @type {Element | undefined} */
var node;
/** @type {string | undefined} */
var node_ns;
return () => {
if (HYDRATION && hydrating) {
return /** @type {import('./hydrate.js').HydrationRuntime} */ (H).t(false, 1);
}
var ns = active_namespace;
if (node === undefined || node_ns !== ns) {
var doc =
inert === 0
? document
: (inert_document ??= document.createElement('template').content.ownerDocument);
node = build_template_element(doc, ns, tag, attributes, children);
node_ns = ns;
}
var clone = is_firefox ? document.importNode(node, true) : node.cloneNode(true);
assign_nodes(clone, clone);
return clone;
};
}
/**
* Appends a DOM node before the anchor node.
* @param {ChildNode | AppendIntoAnchor} anchor - The anchor node.
* @param {Node} dom - The DOM node to append.
*/
export function append(anchor, dom) {
if (HYDRATION && hydrating) {
/** @type {import('./hydrate.js').HydrationRuntime} */ (H).a(anchor, dom);
return;
}
if (/** @type {AppendIntoAnchor} */ (anchor).into === true) {
// Append-into-parent sentinel: an all-component-children element passes a
// `{ parent }` object (no `nodeType`) so each component's root appends as
// the host's last child instead of inserting before a placeholder comment.
// The block still self-marks its range via assign_nodes, so teardown works.
/** @type {AppendIntoAnchor} */ (anchor).parent.appendChild(dom);
return;
}
/** @type {ChildNode} */ (anchor).before(/** @type {Node} */ (dom));
}
export function text(data = '') {
if (HYDRATION && hydrating) {
assign_nodes(/** @type {Node} */ (hydrate_node), /** @type {Node} */ (hydrate_node));
return /** @type {Node} */ (hydrate_node);
}
var node = create_text(data);
assign_nodes(node, node);
return node;
}