ripple
Version:
Ripple is an elegant TypeScript UI framework
268 lines (234 loc) • 8.02 kB
JavaScript
/** @import { Block } from '#client' */
import {
COMMENT_NODE,
HYDRATION_END,
HYDRATION_START,
TEMPLATE_FRAGMENT,
TEMPLATE_USE_IMPORT_NODE,
TEMPLATE_SVG_NAMESPACE,
TEMPLATE_MATHML_NAMESPACE,
} from '../../../constants.js';
import { hydrate_advance, hydrate_node, hydrating, pop } from './hydration.js';
import { create_text, get_first_child, get_next_sibling, is_firefox } from './operations.js';
import { active_block, active_namespace } from './runtime.js';
/**
* 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;
}
}
/**
* Creates a DocumentFragment from an HTML string.
* @param {string} html - The HTML string.
* @param {boolean} use_svg_namespace - Whether to use SVG namespace.
* @param {boolean} use_mathml_namespace - Whether to use MathML namespace.
* @returns {DocumentFragment}
*/
export function create_fragment_from_html(
html,
use_svg_namespace = false,
use_mathml_namespace = false,
) {
if (use_svg_namespace) {
return from_namespace(html, 'svg');
}
if (use_mathml_namespace) {
return from_namespace(html, 'math');
}
var elem = document.createElement('template');
elem.innerHTML = html;
return elem.content;
}
/**
* 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, count = 1) {
var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0;
var use_import_node = (flags & TEMPLATE_USE_IMPORT_NODE) !== 0;
var use_svg_namespace = (flags & TEMPLATE_SVG_NAMESPACE) !== 0;
var use_mathml_namespace = (flags & TEMPLATE_MATHML_NAMESPACE) !== 0;
/** @type {Node | DocumentFragment | undefined} */
var node;
var node_svg = false;
var node_mathml = false;
var is_comment = content === '<!>';
var has_start = !is_comment && !content.startsWith('<!>');
return () => {
if (hydrating) {
if (is_fragment) {
var start = /** @type {Node} */ (hydrate_node);
var end = start;
// Walk using compiler-provided hop count so hydration never
// parses template HTML into fragments.
for (var i = 1; i < count; i++) {
var next = get_next_sibling(end);
while (next !== null && next.nodeType === Node.COMMENT_NODE) {
next = get_next_sibling(next);
}
if (next === null) {
break;
}
end = next;
}
assign_nodes(start, end);
return start;
} else {
var node_to_use = /** @type {Node} */ (hydrate_node);
assign_nodes(node_to_use, node_to_use);
return node_to_use;
}
}
// If using runtime namespace, check active_namespace
var svg = !is_comment && (use_svg_namespace || active_namespace === 'svg');
var mathml = !is_comment && (use_mathml_namespace || active_namespace === 'mathml');
if (node === undefined || node_svg !== svg || node_mathml !== mathml) {
node = create_fragment_from_html(has_start ? content : '<!>' + content, svg, mathml);
node_svg = svg;
node_mathml = mathml;
if (!is_fragment) node = /** @type {Node} */ (get_first_child(node));
}
/** @type {DocumentFragment | Node} */
var clone =
use_import_node || is_firefox
? document.importNode(/** @type {Node} */ (node), true)
: /** @type {Node} */ (node).cloneNode(true);
if (is_fragment) {
// we know for sure that children exist
var start = /** @type {Node} */ (get_first_child(/** @type {DocumentFragment} */ (clone)));
var end = /** @type {Node} */ (/** @type {DocumentFragment} */ (clone).lastChild);
assign_nodes(start, end);
} else {
assign_nodes(clone, clone);
}
return clone;
};
}
/**
* Appends a DOM node before the anchor node.
* @param {ChildNode} anchor - The anchor node.
* @param {Node} dom - The DOM node to append.
* @param {boolean} [skip_advance] - If true, don't advance hydrate_node (used when next() already positioned it).
*/
export function append(anchor, dom, skip_advance) {
if (hydrating) {
// When skip_advance is true, the caller (e.g., a fragment component) has already
// used next() to position hydrate_node correctly. We must NOT reset it.
if (skip_advance) {
return;
}
// During hydration, if anchor === dom, we're hydrating a child component
// where the "anchor" IS the content. If the cursor is still somewhere
// inside dom (at any depth), reset it to dom's level so sibling traversal
// works. But if the cursor has advanced past dom (e.g., because internal
// control flow blocks like switch/if/for advanced it through their
// hydration markers), preserve the advanced position.
if (anchor === dom) {
if (hydrate_node !== null && hydrate_node !== dom && dom.contains(hydrate_node)) {
pop(dom);
}
return;
}
// If the hydration cursor has descended into dom's children (e.g. after
// child()/sibling() traversal inside a single-node template), we need
// pop() to reset back to dom's sibling level before advancing.
// But if the cursor is already at dom's sibling level (e.g. because
// nested control flow blocks advanced it past dom via sibling traversal),
// pop() would incorrectly reset backwards — so we skip it.
if (hydrate_node !== null && hydrate_node !== dom && dom.contains(hydrate_node)) {
pop(dom);
} else if (hydrate_node !== dom) {
// Cursor has advanced past dom via sibling traversal (due to nested
// block processing). Update the branch block's end to reflect the
// actual extent, which may be past the statically-assigned end from
// the template's assign_nodes call.
var block = /** @type {Block} */ (active_block);
var s = block.s;
if (s !== null) {
s.end = /** @type {Node} */ (hydrate_node);
}
if (is_after_hydration_block(dom, hydrate_node)) {
return;
}
}
// Only advance if there's a next sibling. At the end of a component's
// content, there might not be more siblings, and that's fine.
hydrate_advance();
return;
}
anchor.before(/** @type {Node} */ (dom));
}
/**
* @param {Node} start
* @param {Node | null} target
* @returns {boolean}
*/
function is_after_hydration_block(start, target) {
if (
target === null ||
start.nodeType !== COMMENT_NODE ||
/** @type {Comment} */ (start).data !== HYDRATION_START
) {
return false;
}
var current = get_next_sibling(start);
var depth = 0;
while (current !== null && current !== target) {
if (current.nodeType === COMMENT_NODE) {
var data = /** @type {Comment} */ (current).data;
if (data === HYDRATION_START) {
depth += 1;
} else if (data === HYDRATION_END) {
if (depth === 0) {
return true;
}
depth -= 1;
}
}
current = get_next_sibling(current);
}
return false;
}
export function text(data = '') {
if (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;
}
/**
* Create fragment with proper namespace using Svelte's wrapping approach
* @param {string} content
* @param {'svg' | 'math'} ns
* @returns {DocumentFragment}
*/
function from_namespace(content, ns = 'svg') {
var wrapped = `<${ns}>${content}</${ns}>`;
var elem = document.createElement('template');
elem.innerHTML = wrapped;
var fragment = elem.content;
var root = /** @type {Element} */ (get_first_child(fragment));
var result = document.createDocumentFragment();
var first;
while ((first = get_first_child(root))) {
result.appendChild(/** @type {Node} */ (first));
}
return result;
}