UNPKG

svelte

Version:

Cybernetically enhanced web apps

378 lines (314 loc) 9.5 kB
/** @import { Effect, TemplateNode } from '#client' */ /** @import { TemplateStructure } from './types' */ import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from './hydration.js'; import { create_text, get_first_child, is_firefox, create_element, create_fragment, create_comment, set_attribute } from './operations.js'; import { create_fragment_from_html } from './reconciler.js'; import { active_effect } from '../runtime.js'; import { NAMESPACE_MATHML, NAMESPACE_SVG, TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE, TEMPLATE_USE_MATHML, TEMPLATE_USE_SVG } from '../../../constants.js'; import { COMMENT_NODE, DOCUMENT_FRAGMENT_NODE, TEXT_NODE } from '#client/constants'; /** * @param {TemplateNode} start * @param {TemplateNode | null} end */ export function assign_nodes(start, end) { var effect = /** @type {Effect} */ (active_effect); if (effect.nodes_start === null) { effect.nodes_start = start; effect.nodes_end = end; } } /** * @param {string} content * @param {number} flags * @returns {() => Node | Node[]} */ /*#__NO_SIDE_EFFECTS__*/ export function from_html(content, flags) { var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0; var use_import_node = (flags & TEMPLATE_USE_IMPORT_NODE) !== 0; /** @type {Node} */ var node; /** * Whether or not the first item is a text/element node. If not, we need to * create an additional comment node to act as `effect.nodes.start` */ var has_start = !content.startsWith('<!>'); return () => { if (hydrating) { assign_nodes(hydrate_node, null); return hydrate_node; } if (node === undefined) { node = create_fragment_from_html(has_start ? content : '<!>' + content); if (!is_fragment) node = /** @type {Node} */ (get_first_child(node)); } var clone = /** @type {TemplateNode} */ ( use_import_node || is_firefox ? document.importNode(node, true) : node.cloneNode(true) ); if (is_fragment) { var start = /** @type {TemplateNode} */ (get_first_child(clone)); var end = /** @type {TemplateNode} */ (clone.lastChild); assign_nodes(start, end); } else { assign_nodes(clone, clone); } return clone; }; } /** * @param {string} content * @param {number} flags * @param {'svg' | 'math'} ns * @returns {() => Node | Node[]} */ /*#__NO_SIDE_EFFECTS__*/ function from_namespace(content, flags, ns = 'svg') { /** * Whether or not the first item is a text/element node. If not, we need to * create an additional comment node to act as `effect.nodes.start` */ var has_start = !content.startsWith('<!>'); var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0; var wrapped = `<${ns}>${has_start ? content : '<!>' + content}</${ns}>`; /** @type {Element | DocumentFragment} */ var node; return () => { if (hydrating) { assign_nodes(hydrate_node, null); return hydrate_node; } if (!node) { var fragment = /** @type {DocumentFragment} */ (create_fragment_from_html(wrapped)); var root = /** @type {Element} */ (get_first_child(fragment)); if (is_fragment) { node = document.createDocumentFragment(); while (get_first_child(root)) { node.appendChild(/** @type {Node} */ (get_first_child(root))); } } else { node = /** @type {Element} */ (get_first_child(root)); } } var clone = /** @type {TemplateNode} */ (node.cloneNode(true)); if (is_fragment) { var start = /** @type {TemplateNode} */ (get_first_child(clone)); var end = /** @type {TemplateNode} */ (clone.lastChild); assign_nodes(start, end); } else { assign_nodes(clone, clone); } return clone; }; } /** * @param {string} content * @param {number} flags */ /*#__NO_SIDE_EFFECTS__*/ export function from_svg(content, flags) { return from_namespace(content, flags, 'svg'); } /** * @param {string} content * @param {number} flags */ /*#__NO_SIDE_EFFECTS__*/ export function from_mathml(content, flags) { return from_namespace(content, flags, 'math'); } /** * @param {TemplateStructure[]} structure * @param {NAMESPACE_SVG | NAMESPACE_MATHML | undefined} [ns] */ function fragment_from_tree(structure, ns) { var fragment = create_fragment(); for (var item of structure) { if (typeof item === 'string') { fragment.append(create_text(item)); continue; } // if `preserveComments === true`, comments are represented as `['// <data>']` if (item === undefined || item[0][0] === '/') { fragment.append(create_comment(item ? item[0].slice(3) : '')); continue; } const [name, attributes, ...children] = item; const namespace = name === 'svg' ? NAMESPACE_SVG : name === 'math' ? NAMESPACE_MATHML : ns; var element = create_element(name, namespace, attributes?.is); for (var key in attributes) { set_attribute(element, key, attributes[key]); } if (children.length > 0) { var target = element.tagName === 'TEMPLATE' ? /** @type {HTMLTemplateElement} */ (element).content : element; target.append( fragment_from_tree(children, element.tagName === 'foreignObject' ? undefined : namespace) ); } fragment.append(element); } return fragment; } /** * @param {TemplateStructure[]} structure * @param {number} flags * @returns {() => Node | Node[]} */ /*#__NO_SIDE_EFFECTS__*/ export function from_tree(structure, flags) { var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0; var use_import_node = (flags & TEMPLATE_USE_IMPORT_NODE) !== 0; /** @type {Node} */ var node; return () => { if (hydrating) { assign_nodes(hydrate_node, null); return hydrate_node; } if (node === undefined) { const ns = (flags & TEMPLATE_USE_SVG) !== 0 ? NAMESPACE_SVG : (flags & TEMPLATE_USE_MATHML) !== 0 ? NAMESPACE_MATHML : undefined; node = fragment_from_tree(structure, ns); if (!is_fragment) node = /** @type {Node} */ (get_first_child(node)); } var clone = /** @type {TemplateNode} */ ( use_import_node || is_firefox ? document.importNode(node, true) : node.cloneNode(true) ); if (is_fragment) { var start = /** @type {TemplateNode} */ (get_first_child(clone)); var end = /** @type {TemplateNode} */ (clone.lastChild); assign_nodes(start, end); } else { assign_nodes(clone, clone); } return clone; }; } /** * @param {() => Element | DocumentFragment} fn */ export function with_script(fn) { return () => run_scripts(fn()); } /** * Creating a document fragment from HTML that contains script tags will not execute * the scripts. We need to replace the script tags with new ones so that they are executed. * @param {Element | DocumentFragment} node * @returns {Node | Node[]} */ function run_scripts(node) { // scripts were SSR'd, in which case they will run if (hydrating) return node; const is_fragment = node.nodeType === DOCUMENT_FRAGMENT_NODE; const scripts = /** @type {HTMLElement} */ (node).tagName === 'SCRIPT' ? [/** @type {HTMLScriptElement} */ (node)] : node.querySelectorAll('script'); const effect = /** @type {Effect} */ (active_effect); for (const script of scripts) { const clone = document.createElement('script'); for (var attribute of script.attributes) { clone.setAttribute(attribute.name, attribute.value); } clone.textContent = script.textContent; // The script has changed - if it's at the edges, the effect now points at dead nodes if (is_fragment ? node.firstChild === script : node === script) { effect.nodes_start = clone; } if (is_fragment ? node.lastChild === script : node === script) { effect.nodes_end = clone; } script.replaceWith(clone); } return node; } /** * Don't mark this as side-effect-free, hydration needs to walk all nodes * @param {any} value */ export function text(value = '') { if (!hydrating) { var t = create_text(value + ''); assign_nodes(t, t); return t; } var node = hydrate_node; if (node.nodeType !== TEXT_NODE) { // if an {expression} is empty during SSR, we need to insert an empty text node node.before((node = create_text())); set_hydrate_node(node); } assign_nodes(node, node); return node; } export function comment() { // we're not delegating to `template` here for performance reasons if (hydrating) { assign_nodes(hydrate_node, null); return hydrate_node; } var frag = document.createDocumentFragment(); var start = document.createComment(''); var anchor = create_text(); frag.append(start, anchor); assign_nodes(start, anchor); return frag; } /** * Assign the created (or in hydration mode, traversed) dom elements to the current block * and insert the elements into the dom (in client mode). * @param {Text | Comment | Element} anchor * @param {DocumentFragment | Element} dom */ export function append(anchor, dom) { if (hydrating) { /** @type {Effect} */ (active_effect).nodes_end = hydrate_node; hydrate_next(); return; } if (anchor === null) { // edge case — void `<svelte:element>` with content return; } anchor.before(/** @type {Node} */ (dom)); } /** * Create (or hydrate) an unique UID for the component instance. */ export function props_id() { if ( hydrating && hydrate_node && hydrate_node.nodeType === COMMENT_NODE && hydrate_node.textContent?.startsWith(`#`) ) { const id = hydrate_node.textContent.substring(1); hydrate_next(); return id; } // @ts-expect-error This way we ensure the id is unique even across Svelte runtimes (window.__svelte ??= {}).uid ??= 1; // @ts-expect-error return `c${window.__svelte.uid++}`; }