UNPKG

ripple

Version:

Ripple is an elegant TypeScript UI framework

268 lines (234 loc) 8.02 kB
/** @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; }