{
"version": 3,
"sources": ["../library/src/engine/consts.ts", "../library/src/engine/types.ts", "../library/src/utils/text.ts", "../library/src/plugins/official/core/attributes/computed.ts", "../library/src/plugins/official/core/attributes/signals.ts", "../library/src/plugins/official/core/attributes/star.ts", "../library/src/utils/dom.ts", "../library/src/engine/errors.ts", "../library/src/vendored/preact-core.ts", "../library/src/engine/signals.ts", "../library/src/engine/engine.ts", "../library/src/engine/index.ts", "../library/src/vendored/fetch-event-source.ts", "../library/src/plugins/official/backend/shared.ts", "../library/src/plugins/official/backend/actions/sse.ts", "../library/src/plugins/official/backend/actions/delete.ts", "../library/src/plugins/official/backend/actions/get.ts", "../library/src/plugins/official/backend/actions/patch.ts", "../library/src/plugins/official/backend/actions/post.ts", "../library/src/plugins/official/backend/actions/put.ts", "../library/src/plugins/official/backend/attributes/indicator.ts", "../library/src/plugins/official/backend/watchers/executeScript.ts", "../library/src/utils/view-transtions.ts", "../library/src/vendored/idiomorph.esm.js", "../library/src/plugins/official/backend/watchers/mergeFragments.ts", "../library/src/plugins/official/backend/watchers/mergeSignals.ts", "../library/src/plugins/official/backend/watchers/removeFragments.ts", "../library/src/plugins/official/backend/watchers/removeSignals.ts", "../library/src/plugins/official/browser/actions/clipboard.ts", "../library/src/plugins/official/browser/attributes/customValidity.ts", "../library/src/plugins/official/browser/attributes/intersects.ts", "../library/src/plugins/official/browser/attributes/persist.ts", "../library/src/plugins/official/browser/attributes/replaceUrl.ts", "../library/src/plugins/official/browser/attributes/scrollIntoView.ts", "../library/src/plugins/official/browser/attributes/show.ts", "../library/src/plugins/official/browser/attributes/viewTransition.ts", "../library/src/plugins/official/dom/attributes/attr.ts", "../library/src/plugins/official/dom/attributes/bind.ts", "../library/src/plugins/official/dom/attributes/class.ts", "../library/src/utils/tags.ts", "../library/src/utils/timing.ts", "../library/src/plugins/official/dom/attributes/on.ts", "../library/src/plugins/official/dom/attributes/ref.ts", "../library/src/plugins/official/dom/attributes/text.ts", "../library/src/plugins/official/logic/actions/fit.ts", "../library/src/plugins/official/logic/actions/setAll.ts", "../library/src/plugins/official/logic/actions/toggleAll.ts", "../library/src/bundles/datastar.ts"],
"sourcesContent": ["// This is auto-generated by Datastar. DO NOT EDIT.\nconst lol = /\uD83D\uDD95JS_DS\uD83D\uDE80/.source\nexport const DSP = lol.slice(0, 5)\nexport const DSS = lol.slice(4)\n\nexport const DATASTAR = \"datastar\";\nexport const DATASTAR_EVENT = \"datastar-event\";\nexport const DATASTAR_REQUEST = \"Datastar-Request\";\n\n// #region Defaults\n\n// #region Default durations\n\n// The default duration for settling during fragment merges. Allows for CSS transitions to complete.\nexport const DefaultFragmentsSettleDurationMs = 300;\n// The default duration for retrying SSE on connection reset. This is part of the underlying retry mechanism of SSE.\nexport const DefaultSseRetryDurationMs = 1000;\n\n// #endregion\n\n\n// #region Default strings\n\n// The default attributes for element use when executing scripts. It is a set of key-value pairs delimited by a newline \\\\n character.\nexport const DefaultExecuteScriptAttributes = \"type module\";\n\n// #endregion\n\n\n// #region Default booleans\n\n// Should fragments be merged using the ViewTransition API?\nexport const DefaultFragmentsUseViewTransitions = false;\n\n// Should a given set of signals merge if they are missing?\nexport const DefaultMergeSignalsOnlyIfMissing = false;\n\n// Should script element remove itself after execution?\nexport const DefaultExecuteScriptAutoRemove = true;\n\n// #endregion\n\n\n// #region Enums\n\n// The mode in which a fragment is merged into the DOM.\nexport const FragmentMergeModes = {\n // Morphs the fragment into the existing element using idiomorph.\n Morph: \"morph\",\n // Replaces the inner HTML of the existing element.\n Inner: \"inner\",\n // Replaces the outer HTML of the existing element.\n Outer: \"outer\",\n // Prepends the fragment to the existing element.\n Prepend: \"prepend\",\n // Appends the fragment to the existing element.\n Append: \"append\",\n // Inserts the fragment before the existing element.\n Before: \"before\",\n // Inserts the fragment after the existing element.\n After: \"after\",\n // Upserts the attributes of the existing element.\n UpsertAttributes: \"upsertAttributes\",\n} as const;\n\n// Default value for FragmentMergeMode\nexport const DefaultFragmentMergeMode = FragmentMergeModes.Morph;\n\n// The type protocol on top of SSE which allows for core pushed based communication between the server and the client.\nexport const EventTypes = {\n // An event for merging HTML fragments into the DOM.\n MergeFragments: \"datastar-merge-fragments\",\n // An event for merging signals.\n MergeSignals: \"datastar-merge-signals\",\n // An event for removing HTML fragments from the DOM.\n RemoveFragments: \"datastar-remove-fragments\",\n // An event for removing signals.\n RemoveSignals: \"datastar-remove-signals\",\n // An event for executing elements in the browser.\n ExecuteScript: \"datastar-execute-script\",\n} as const;\n// #endregion\n\n// #endregion", "import type { EffectFn, Signal } from '../vendored/preact-core'\nimport { DATASTAR } from './consts'\nimport type { SignalsRoot } from './signals'\n\nexport type OnRemovalFn = () => void\n\nexport enum PluginType {\n Attribute = 1,\n Watcher = 2,\n Action = 3,\n}\n\nexport interface DatastarPlugin {\n type: PluginType // The type of plugin\n name: string // The name of the plugin\n}\n\nexport enum Requirement {\n Allowed = 0,\n Must = 1,\n Denied = 2,\n Exclusive = 3,\n}\n\nexport interface DatastarSignalEvent {\n added: Array\n removed: Array\n updated: Array\n}\nexport const DATASTAR_SIGNAL_EVENT = `${DATASTAR}-signals`\nexport interface CustomEventMap {\n [DATASTAR_SIGNAL_EVENT]: CustomEvent\n}\nexport type WatcherFn = (\n this: Document,\n ev: CustomEventMap[K],\n) => void\ndeclare global {\n interface Document {\n dispatchEvent(ev: CustomEventMap[K]): void\n addEventListener(\n type: K,\n listener: WatcherFn,\n ): void\n removeEventListener(\n type: K,\n listener: WatcherFn,\n ): void\n }\n}\n\n// A plugin accesible via a `data-${name}` attribute on an element\nexport interface AttributePlugin extends DatastarPlugin {\n type: PluginType.Attribute\n onGlobalInit?: (ctx: InitContext) => void // Called once on registration of the plugin\n onLoad: (ctx: RuntimeContext) => OnRemovalFn | void // Return a function to be called on removal\n mods?: Set // If not provided, all modifiers are allowed\n keyReq?: Requirement // The rules for the key requirements\n valReq?: Requirement // The rules for the value requirements\n argNames?: string[] // argument names for the reactive expression\n}\n\n// A plugin that runs on the global scope of the DastaStar instance\nexport interface WatcherPlugin extends DatastarPlugin {\n type: PluginType.Watcher\n onGlobalInit?: (ctx: InitContext) => void\n}\n\nexport type ActionPlugins = Record\nexport type ActionMethod = (ctx: RuntimeContext, ...args: any[]) => any\n\nexport interface ActionPlugin extends DatastarPlugin {\n type: PluginType.Action\n fn: ActionMethod\n}\n\nexport type GlobalInitializer = (ctx: InitContext) => void\n\nexport type InitContext = {\n plugin: DatastarPlugin\n signals: SignalsRoot\n effect: (fn: EffectFn) => OnRemovalFn\n actions: Readonly\n apply: (el?: HTMLorSVGElement) => void\n}\n\nexport type HTMLorSVGElement = Element & (HTMLElement | SVGElement)\nexport type Modifiers = Map> // mod name -> tags\n\nexport type RuntimeContext = InitContext & {\n plugin: DatastarPlugin // The name of the plugin\n el: HTMLorSVGElement // The element the attribute is on\n rawKey: Readonly // no parsing data-* key\n key: Readonly // data-* key without the prefix or tags\n value: Readonly // value of data-* attribute\n mods: Modifiers // the tags and their arguments\n genRX: () => (...args: any[]) => T // a reactive expression\n fnContent?: string // the content of the function\n}\n\nexport type NestedValues = { [key: string]: NestedValues | any }\nexport type NestedSignal = {\n [key: string]: NestedSignal | Signal\n}\n\nexport type RuntimeExpressionFunction = (\n ctx: RuntimeContext,\n ...args: any[]\n) => any\n", "import type { Modifiers } from '../engine/types'\n\nexport const isBoolString = (str: string) => str.trim() === 'true'\n\nexport const kebab = (str: string) =>\n str.replace(\n /[A-Z]+(?![a-z])|[A-Z]/g,\n ($, ofs) => (ofs ? '-' : '') + $.toLowerCase(),\n )\n\nexport const camel = (str: string) =>\n kebab(str).replace(/-./g, (x) => x[1].toUpperCase())\n\nexport const snake = (str: string) => kebab(str).replace(/-/g, '_')\n\nexport const pascal = (str: string) =>\n camel(str).replace(/^./, (x) => x[0].toUpperCase())\n\nexport const jsStrToObject = (raw: string) =>\n new Function(`return Object.assign({}, ${raw})`)()\n\nexport const trimDollarSignPrefix = (str: string) =>\n str.startsWith('$') ? str.slice(1) : str\n\nconst caseFns: Record string> = { kebab, snake, pascal }\n\nexport function modifyCasing(str: string, mods: Modifiers) {\n for (const c of mods.get('case') || []) {\n const fn = caseFns[c]\n if (fn) str = fn(str)\n }\n return str\n}\n", "import {\n type AttributePlugin,\n PluginType,\n Requirement,\n} from '../../../../engine/types'\nimport { modifyCasing } from '../../../../utils/text'\n\nconst name = 'computed'\nexport const Computed: AttributePlugin = {\n type: PluginType.Attribute,\n name,\n keyReq: Requirement.Must,\n valReq: Requirement.Must,\n onLoad: ({ key, mods, signals, genRX }) => {\n key = modifyCasing(key, mods)\n const rx = genRX()\n signals.setComputed(key, rx)\n },\n}\n", "import {\n type AttributePlugin,\n type NestedValues,\n PluginType,\n} from '../../../../engine/types'\nimport { jsStrToObject, modifyCasing } from '../../../../utils/text'\n\nexport const Signals: AttributePlugin = {\n type: PluginType.Attribute,\n name: 'signals',\n onLoad: (ctx) => {\n const { key, mods, signals, value, genRX } = ctx\n const ifMissing = mods.has('ifmissing')\n\n if (key !== '') {\n const k = modifyCasing(key, mods)\n const v = value === '' ? value : genRX()()\n if (ifMissing) {\n signals.upsertIfMissing(k, v)\n } else {\n signals.setValue(k, v)\n }\n } else {\n const obj = jsStrToObject(ctx.value)\n ctx.value = JSON.stringify(obj)\n const rx = genRX()\n const nv = rx()\n signals.merge(nv, ifMissing)\n }\n },\n}\n", "import {\n type AttributePlugin,\n PluginType,\n Requirement,\n} from '../../../../engine/types'\n\nexport const Star: AttributePlugin = {\n type: PluginType.Attribute,\n name: 'star',\n keyReq: Requirement.Denied,\n valReq: Requirement.Denied,\n onLoad: () => {\n alert('YOU ARE PROBABLY OVERCOMPLICATING IT')\n },\n}\n", "import { DATASTAR } from '../engine/consts'\nimport type { HTMLorSVGElement } from '../engine/types'\n\nexport class Hash {\n #value = 0\n #prefix: string\n\n constructor(prefix = DATASTAR) {\n this.#prefix = prefix\n }\n\n with(x: number | string | boolean): Hash {\n if (typeof x === 'string') {\n for (const c of x.split('')) {\n this.with(c.charCodeAt(0))\n }\n } else if (typeof x === 'boolean') {\n this.with(1 << (x ? 7 : 3))\n } else {\n // use djb2 favored by bernstein http://www.cse.yorku.ca/~oz/hash.html\n this.#value = (this.#value * 33) ^ x\n }\n return this\n }\n\n get value() {\n return this.#value\n }\n\n get string() {\n return this.#prefix + Math.abs(this.#value).toString(36)\n }\n}\n\nexport function elUniqId(el: Element) {\n if (el.id) return el.id\n const hash = new Hash()\n\n let currentEl = el\n while (currentEl) {\n hash.with(currentEl.tagName || '')\n if (currentEl.id) {\n hash.with(currentEl.id)\n break\n }\n const p = currentEl?.parentNode\n if (p) hash.with([...p.children].indexOf(currentEl))\n\n currentEl = p as Element\n }\n return hash.string\n}\n\nexport function attrHash(key: string, val: string) {\n return new Hash().with(key).with(val).value\n}\n\nexport function walkDOM(\n element: Element | null,\n callback: (el: HTMLorSVGElement) => void,\n) {\n if (\n !element ||\n !(element instanceof HTMLElement || element instanceof SVGElement)\n ) {\n return null\n }\n const dataset = element.dataset\n if ('starIgnore' in dataset) {\n return null\n }\n if (!('starIgnore__self' in dataset)) {\n callback(element)\n }\n let el = element.firstElementChild\n while (el) {\n walkDOM(el, callback)\n el = el.nextElementSibling\n }\n}\n", "import { snake } from '../utils/text'\nimport { DATASTAR } from './consts'\nimport { type InitContext, PluginType, type RuntimeContext } from './types'\n\nconst url = 'https://data-star.dev/errors'\n\ninterface Metadata {\n error?: string\n [key: string]: any\n}\n\nfunction dserr(type: string, reason: string, metadata: Metadata = {}) {\n const e = new Error()\n e.name = `${DATASTAR} ${type} error`\n const r = snake(reason)\n const q = new URLSearchParams({\n metadata: JSON.stringify(metadata),\n }).toString()\n const c = JSON.stringify(metadata, null, 2)\n e.message = `${reason}\\nMore info: ${url}/${type}/${r}?${q}\\nContext: ${c}`\n return e\n}\n\nexport function internalErr(from: string, reason: string, args = {}) {\n return dserr('internal', reason, Object.assign({ from }, args))\n}\n\nexport function initErr(reason: string, ctx: InitContext, metadata = {}) {\n const errCtx = {\n plugin: {\n name: ctx.plugin.name,\n type: PluginType[ctx.plugin.type],\n },\n }\n return dserr('init', reason, Object.assign(errCtx, metadata))\n}\n\nexport function runtimeErr(reason: string, ctx: RuntimeContext, metadata = {}) {\n const errCtx = {\n plugin: {\n name: ctx.plugin.name,\n type: PluginType[ctx.plugin.type],\n },\n element: {\n id: ctx.el.id,\n tag: ctx.el.tagName,\n },\n expression: {\n rawKey: ctx.rawKey,\n key: ctx.key,\n value: ctx.value,\n validSignals: ctx.signals.paths(),\n fnContent: ctx.fnContent,\n },\n }\n return dserr('runtime', reason, Object.assign(errCtx, metadata))\n}\n", "import { internalErr } from '../engine/errors'\nimport type { OnRemovalFn } from '../engine/types'\n\nconst from = 'preact-signals'\n\n// An named symbol/brand for detecting Signal instances even when they weren't\n// created using the same signals library version.\nconst BRAND_SYMBOL = Symbol.for('preact-signals')\n\n// Flags for Computed and Effect.\nconst RUNNING = 1 << 0\nconst NOTIFIED = 1 << 1\nconst OUTDATED = 1 << 2\nconst DISPOSED = 1 << 3\nconst HAS_ERROR = 1 << 4\nconst TRACKING = 1 << 5\n\n// A linked list node used to track dependencies (sources) and dependents (targets).\n// Also used to remember the source's last version number that the target saw.\ntype Node = {\n // A source whose value the target depends on.\n _source: Signal\n _prevSource?: Node\n _nextSource?: Node\n\n // A target that depends on the source and should be notified when the source changes.\n _target: Computed | Effect\n _prevTarget?: Node\n _nextTarget?: Node\n\n // The version number of the source that target has last seen. We use version numbers\n // instead of storing the source value, because source values can take arbitrary amount\n // of memory, and computeds could hang on to them forever because they're lazily evaluated.\n // Use the special value -1 to mark potentially unused but recyclable nodes.\n _version: number\n\n // Used to remember & roll back the source's previous `._node` value when entering &\n // exiting a new evaluation context.\n _rollbackNode?: Node\n}\n\nfunction startBatch() {\n batchDepth++\n}\n\nfunction endBatch() {\n if (batchDepth > 1) {\n batchDepth--\n return\n }\n\n let error: unknown\n let hasError = false\n\n while (batchedEffect !== undefined) {\n let effect: Effect | undefined = batchedEffect\n batchedEffect = undefined\n\n batchIteration++\n\n while (effect !== undefined) {\n const next: Effect | undefined = effect._nextBatchedEffect\n effect._nextBatchedEffect = undefined\n effect._flags &= ~NOTIFIED\n\n if (!(effect._flags & DISPOSED) && needsToRecompute(effect)) {\n try {\n effect._callback()\n } catch (err) {\n if (!hasError) {\n error = err\n hasError = true\n }\n }\n }\n effect = next\n }\n }\n batchIteration = 0\n batchDepth--\n\n if (hasError) throw error\n}\n\n/**\n * Combine multiple value updates into one \"commit\" at the end of the provided callback.\n *\n * Batches can be nested and changes are only flushed once the outermost batch callback\n * completes.\n *\n * Accessing a signal that has been modified within a batch will reflect its updated\n * value.\n *\n * @param fn The callback function.\n * @returns The value returned by the callback.\n */\nfunction batch(fn: () => T): T {\n if (batchDepth > 0) {\n return fn()\n }\n /*@__INLINE__**/ startBatch()\n try {\n return fn()\n } finally {\n endBatch()\n }\n}\n\n// Currently evaluated computed or effect.\nlet evalContext: Computed | Effect | undefined = undefined\n\n/**\n * Run a callback function that can access signal values without\n * subscribing to the signal updates.\n *\n * @param fn The callback function.\n * @returns The value returned by the callback.\n */\nfunction untracked(fn: () => T): T {\n const prevContext = evalContext\n evalContext = undefined\n try {\n return fn()\n } finally {\n evalContext = prevContext\n }\n}\n\n// Effects collected into a batch.\nlet batchedEffect: Effect | undefined = undefined\nlet batchDepth = 0\nlet batchIteration = 0\n\n// A global version number for signals, used for fast-pathing repeated\n// computed.peek()/computed.value calls when nothing has changed globally.\nlet globalVersion = 0\n\nfunction addDependency(signal: Signal): Node | undefined {\n if (evalContext === undefined) {\n return undefined\n }\n\n let node = signal._node\n if (node === undefined || node._target !== evalContext) {\n /**\n * `signal` is a new dependency. Create a new dependency node, and set it\n * as the tail of the current context's dependency list. e.g:\n *\n * { A <-> B }\n * \u2191 \u2191\n * tail node (new)\n * \u2193\n * { A <-> B <-> C }\n * \u2191\n * tail (evalContext._sources)\n */\n node = {\n _version: 0,\n _source: signal,\n _prevSource: evalContext._sources,\n _nextSource: undefined,\n _target: evalContext,\n _prevTarget: undefined,\n _nextTarget: undefined,\n _rollbackNode: node,\n }\n\n if (evalContext._sources !== undefined) {\n evalContext._sources._nextSource = node\n }\n evalContext._sources = node\n signal._node = node\n\n // Subscribe to change notifications from this dependency if we're in an effect\n // OR evaluating a computed signal that in turn has subscribers.\n if (evalContext._flags & TRACKING) {\n signal._subscribe(node)\n }\n return node\n }\n if (node._version === -1) {\n // `signal` is an existing dependency from a previous evaluation. Reuse it.\n node._version = 0\n\n /**\n * If `node` is not already the current tail of the dependency list (i.e.\n * there is a next node in the list), then make the `node` the new tail. e.g:\n *\n * { A <-> B <-> C <-> D }\n * \u2191 \u2191\n * node \u250C\u2500\u2500\u2500 tail (evalContext._sources)\n * \u2514\u2500\u2500\u2500\u2500\u2500\u2502\u2500\u2500\u2500\u2500\u2500\u2510\n * \u2193 \u2193\n * { A <-> C <-> D <-> B }\n * \u2191\n * tail (evalContext._sources)\n */\n if (node._nextSource !== undefined) {\n node._nextSource._prevSource = node._prevSource\n\n if (node._prevSource !== undefined) {\n node._prevSource._nextSource = node._nextSource\n }\n\n node._prevSource = evalContext._sources\n node._nextSource = undefined\n\n evalContext._sources!._nextSource = node\n evalContext._sources = node\n }\n\n // We can assume that the currently evaluated effect / computed signal is already\n // subscribed to change notifications from `signal` if needed.\n return node\n }\n return undefined\n}\n\n/**\n * The base class for plain and computed signals.\n */\n// @ts-ignore: \"Cannot redeclare exported variable 'Signal'.\"\n//\n// A function with the same name is defined later, so we need to ignore TypeScript's\n// warning about a redeclared variable.\n//\n// The class is declared here, but later implemented with ES5-style protoTYPEOF_\n// This enables better control of the transpiled output size.\ndeclare class Signal {\n /** @internal */\n _onChange: (change: { old: T; revised: T }) => void\n\n /** @internal */\n _value: unknown\n\n /**\n * @internal\n * Version numbers should always be >= 0, because the special value -1 is used\n * by Nodes to signify potentially unused but recyclable nodes.\n */\n _version: number\n\n /** @internal */\n _node?: Node\n\n /** @internal */\n _targets?: Node\n\n constructor(value?: T)\n\n /** @internal */\n _refresh(): boolean\n\n /** @internal */\n _subscribe(node: Node): void\n\n /** @internal */\n _unsubscribe(node: Node): void\n\n subscribe(fn: (value: T) => void): () => void\n\n valueOf(): T\n\n toString(): string\n\n toJSON(): T\n\n peek(): T\n\n brand: typeof BRAND_SYMBOL\n\n get value(): T\n set value(value: T)\n}\n\n/** @internal */\n// @ts-ignore: \"Cannot redeclare exported variable 'Signal'.\"\n//\n// A class with the same name has already been declared, so we need to ignore\n// TypeScript's warning about a redeclared variable.\n//\n// The previously declared class is implemented here with ES5-style protoTYPEOF_\n// This enables better control of the transpiled output size.\n// biome-ignore lint/suspicious/noRedeclare: \nfunction Signal(this: Signal, value?: unknown) {\n this._value = value\n this._version = 0\n this._node = undefined\n this._targets = undefined\n}\n\nSignal.prototype.brand = BRAND_SYMBOL\n\nSignal.prototype._refresh = () => true\n\nSignal.prototype._subscribe = function (node) {\n if (this._targets !== node && node._prevTarget === undefined) {\n node._nextTarget = this._targets\n if (this._targets !== undefined) {\n this._targets._prevTarget = node\n }\n this._targets = node\n }\n}\n\nSignal.prototype._unsubscribe = function (node) {\n // Only run the unsubscribe step if the signal has any subscribers to begin with.\n if (this._targets !== undefined) {\n const prev = node._prevTarget\n const next = node._nextTarget\n if (prev !== undefined) {\n prev._nextTarget = next\n node._prevTarget = undefined\n }\n if (next !== undefined) {\n next._prevTarget = prev\n node._nextTarget = undefined\n }\n if (node === this._targets) {\n this._targets = next\n }\n }\n}\n\nSignal.prototype.subscribe = function (fn) {\n return effect(() => {\n const value = this.value\n\n const prevContext = evalContext\n evalContext = undefined\n try {\n fn(value)\n } finally {\n evalContext = prevContext\n }\n })\n}\n\nSignal.prototype.valueOf = function () {\n return this.value\n}\n\nSignal.prototype.toString = function () {\n return `${this.value}`\n}\n\nSignal.prototype.toJSON = function () {\n return this.value\n}\n\nSignal.prototype.peek = function () {\n const prevContext = evalContext\n evalContext = undefined\n try {\n return this.value\n } finally {\n evalContext = prevContext\n }\n}\n\nObject.defineProperty(Signal.prototype, 'value', {\n get(this: Signal) {\n const node = addDependency(this)\n if (node !== undefined) {\n node._version = this._version\n }\n return this._value\n },\n set(this: Signal, value) {\n if (value !== this._value) {\n if (batchIteration > 100) {\n throw internalErr(from, 'SignalCycleDetected')\n }\n const old = this._value\n const revised = value\n\n this._value = value\n this._version++\n globalVersion++\n\n /**@__INLINE__*/ startBatch()\n try {\n for (\n let node = this._targets;\n node !== undefined;\n node = node._nextTarget\n ) {\n node._target._notify()\n }\n } finally {\n endBatch()\n }\n\n this?._onChange({ old, revised })\n }\n },\n})\n\n/**\n * Create a new plain signal.\n *\n * @param value The initial value for the signal.\n * @returns A new signal.\n */\nexport function signal(value: T): Signal\nexport function signal(): Signal\nexport function signal(value?: T): Signal {\n return new Signal(value)\n}\n\nfunction needsToRecompute(target: Computed | Effect): boolean {\n // Check the dependencies for changed values. The dependency list is already\n // in order of use. Therefore if multiple dependencies have changed values, only\n // the first used dependency is re-evaluated at this point.\n for (\n let node = target._sources;\n node !== undefined;\n node = node._nextSource\n ) {\n // If there's a new version of the dependency before or after refreshing,\n // or the dependency has something blocking it from refreshing at all (e.g. a\n // dependency cycle), then we need to recompute.\n if (\n node._source._version !== node._version ||\n !node._source._refresh() ||\n node._source._version !== node._version\n ) {\n return true\n }\n }\n // If none of the dependencies have changed values since last recompute then\n // there's no need to recompute.\n return false\n}\n\nfunction prepareSources(target: Computed | Effect) {\n /**\n * 1. Mark all current sources as re-usable nodes (version: -1)\n * 2. Set a rollback node if the current node is being used in a different context\n * 3. Point 'target._sources' to the tail of the doubly-linked list, e.g:\n *\n * { undefined <- A <-> B <-> C -> undefined }\n * \u2191 \u2191\n * \u2502 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n * target._sources = A; (node is head) \u2502\n * \u2193 \u2502\n * target._sources = C; (node is tail) \u2500\u2518\n */\n for (\n let node = target._sources;\n node !== undefined;\n node = node._nextSource\n ) {\n const rollbackNode = node._source._node\n if (rollbackNode !== undefined) {\n node._rollbackNode = rollbackNode\n }\n node._source._node = node\n node._version = -1\n\n if (node._nextSource === undefined) {\n target._sources = node\n break\n }\n }\n}\n\nfunction cleanupSources(target: Computed | Effect) {\n let node = target._sources\n let head: Node | undefined = undefined\n\n /**\n * At this point 'target._sources' points to the tail of the doubly-linked list.\n * It contains all existing sources + new sources in order of use.\n * Iterate backwards until we find the head node while dropping old dependencies.\n */\n while (node !== undefined) {\n const prev = node._prevSource\n\n /**\n * The node was not re-used, unsubscribe from its change notifications and remove itself\n * from the doubly-linked list. e.g:\n *\n * { A <-> B <-> C }\n * \u2193\n * { A <-> C }\n */\n if (node._version === -1) {\n node._source._unsubscribe(node)\n\n if (prev !== undefined) {\n prev._nextSource = node._nextSource\n }\n if (node._nextSource !== undefined) {\n node._nextSource._prevSource = prev\n }\n } else {\n /**\n * The new head is the last node seen which wasn't removed/unsubscribed\n * from the doubly-linked list. e.g:\n *\n * { A <-> B <-> C }\n * \u2191 \u2191 \u2191\n * \u2502 \u2502 \u2514 head = node\n * \u2502 \u2514 head = node\n * \u2514 head = node\n */\n head = node\n }\n\n node._source._node = node._rollbackNode\n if (node._rollbackNode !== undefined) {\n node._rollbackNode = undefined\n }\n\n node = prev\n }\n\n target._sources = head\n}\n\nexport declare class Computed extends Signal {\n _fn: () => T\n _sources?: Node\n _globalVersion: number\n _flags: number\n\n constructor(fn: () => T)\n\n _notify(): void\n get value(): T\n}\n\n// biome-ignore lint/suspicious/noRedeclare: \nexport function Computed(this: Computed, fn: () => unknown) {\n Signal.call(this, undefined)\n\n this._fn = fn\n this._sources = undefined\n this._globalVersion = globalVersion - 1\n this._flags = OUTDATED\n}\n\nComputed.prototype = new Signal() as Computed\n\nComputed.prototype._refresh = function () {\n this._flags &= ~NOTIFIED\n\n if (this._flags & RUNNING) {\n return false\n }\n\n // If this computed signal has subscribed to updates from its dependencies\n // (TRACKING flag set) and none of them have notified about changes (OUTDATED\n // flag not set), then the computed value can't have changed.\n if ((this._flags & (OUTDATED | TRACKING)) === TRACKING) {\n return true\n }\n this._flags &= ~OUTDATED\n\n if (this._globalVersion === globalVersion) {\n return true\n }\n this._globalVersion = globalVersion\n\n // Mark this computed signal running before checking the dependencies for value\n // changes, so that the RUNNING flag can be used to notice cyclical dependencies.\n this._flags |= RUNNING\n if (this._version > 0 && !needsToRecompute(this)) {\n this._flags &= ~RUNNING\n return true\n }\n\n const prevContext = evalContext\n try {\n prepareSources(this)\n evalContext = this\n const value = this._fn()\n if (\n this._flags & HAS_ERROR ||\n this._value !== value ||\n this._version === 0\n ) {\n this._value = value\n this._flags &= ~HAS_ERROR\n this._version++\n }\n } catch (err) {\n this._value = err\n this._flags |= HAS_ERROR\n this._version++\n }\n evalContext = prevContext\n cleanupSources(this)\n this._flags &= ~RUNNING\n return true\n}\n\nComputed.prototype._subscribe = function (node) {\n if (this._targets === undefined) {\n this._flags |= OUTDATED | TRACKING\n\n // A computed signal subscribes lazily to its dependencies when it\n // gets its first subscriber.\n for (\n let node = this._sources;\n node !== undefined;\n node = node._nextSource\n ) {\n node._source._subscribe(node)\n }\n }\n Signal.prototype._subscribe.call(this, node)\n}\n\nComputed.prototype._unsubscribe = function (node) {\n // Only run the unsubscribe step if the computed signal has any subscribers.\n if (this._targets !== undefined) {\n Signal.prototype._unsubscribe.call(this, node)\n\n // Computed signal unsubscribes from its dependencies when it loses its last subscriber.\n // This makes it possible for unreferences subgraphs of computed signals to get garbage collected.\n if (this._targets === undefined) {\n this._flags &= ~TRACKING\n\n for (\n let node = this._sources;\n node !== undefined;\n node = node._nextSource\n ) {\n node._source._unsubscribe(node)\n }\n }\n }\n}\n\nComputed.prototype._notify = function () {\n if (!(this._flags & NOTIFIED)) {\n this._flags |= OUTDATED | NOTIFIED\n\n for (\n let node = this._targets;\n node !== undefined;\n node = node._nextTarget\n ) {\n node._target._notify()\n }\n }\n}\n\nObject.defineProperty(Computed.prototype, 'value', {\n get(this: Computed) {\n if (this._flags & RUNNING) {\n // Cycle detected\n throw internalErr(from, 'SignalCycleDetected')\n }\n const node = addDependency(this)\n this._refresh()\n if (node !== undefined) {\n node._version = this._version\n }\n if (this._flags & HAS_ERROR) {\n throw internalErr(from, 'GetComputedError', { value: this._value })\n }\n return this._value\n },\n})\n\n/**\n * An interface for read-only signals.\n */\ninterface ReadonlySignal {\n readonly value: T\n peek(): T\n\n subscribe(fn: (value: T) => void): () => void\n valueOf(): T\n toString(): string\n toJSON(): T\n brand: typeof BRAND_SYMBOL\n}\n\n/**\n * Create a new signal that is computed based on the values of other signals.\n *\n * The returned computed signal is read-only, and its value is automatically\n * updated when any signals accessed from within the callback function change.\n *\n * @param fn The effect callback.\n * @returns A new read-only signal.\n */\nfunction computed(fn: () => T): ReadonlySignal {\n return new Computed(fn)\n}\n\nfunction cleanupEffect(effect: Effect) {\n const cleanup = effect._cleanup\n effect._cleanup = undefined\n\n if (typeof cleanup === 'function') {\n /*@__INLINE__**/ startBatch()\n\n // Run cleanup functions always outside of any context.\n const prevContext = evalContext\n evalContext = undefined\n try {\n cleanup!()\n } catch (error) {\n effect._flags &= ~RUNNING\n effect._flags |= DISPOSED\n disposeEffect(effect)\n throw internalErr(from, 'CleanupEffectError', { error })\n } finally {\n evalContext = prevContext\n endBatch()\n }\n }\n}\n\nfunction disposeEffect(effect: Effect) {\n for (\n let node = effect._sources;\n node !== undefined;\n node = node._nextSource\n ) {\n node._source._unsubscribe(node)\n }\n effect._fn = undefined\n effect._sources = undefined\n\n cleanupEffect(effect)\n}\n\nfunction endEffect(this: Effect, prevContext?: Computed | Effect) {\n if (evalContext !== this) {\n throw internalErr(from, 'EndEffectError')\n }\n cleanupSources(this)\n evalContext = prevContext\n\n this._flags &= ~RUNNING\n if (this._flags & DISPOSED) {\n disposeEffect(this)\n }\n endBatch()\n}\n\nexport type EffectFn = () => OnRemovalFn | void | Promise\n\ndeclare class Effect {\n _fn?: EffectFn\n _cleanup?: () => void\n _sources?: Node\n _nextBatchedEffect?: Effect\n _flags: number\n\n constructor(fn: EffectFn)\n\n _callback(): void\n _start(): () => void\n _notify(): void\n _dispose(): void\n}\n\n// biome-ignore lint/suspicious/noRedeclare: \nfunction Effect(this: Effect, fn: EffectFn) {\n this._fn = fn\n this._cleanup = undefined\n this._sources = undefined\n this._nextBatchedEffect = undefined\n this._flags = TRACKING\n}\n\nEffect.prototype._callback = function () {\n const finish = this._start()\n try {\n if (this._flags & DISPOSED) return\n if (this._fn === undefined) return\n\n const cleanup = this._fn()\n if (typeof cleanup === 'function') {\n this._cleanup = cleanup!\n }\n } finally {\n finish()\n }\n}\n\nEffect.prototype._start = function () {\n if (this._flags & RUNNING) {\n throw internalErr(from, 'SignalCycleDetected')\n }\n this._flags |= RUNNING\n this._flags &= ~DISPOSED\n cleanupEffect(this)\n prepareSources(this)\n\n /*@__INLINE__**/ startBatch()\n const prevContext = evalContext\n evalContext = this\n return endEffect.bind(this, prevContext)\n}\n\nEffect.prototype._notify = function () {\n if (!(this._flags & NOTIFIED)) {\n this._flags |= NOTIFIED\n this._nextBatchedEffect = batchedEffect\n batchedEffect = this\n }\n}\n\nEffect.prototype._dispose = function () {\n this._flags |= DISPOSED\n\n if (!(this._flags & RUNNING)) {\n disposeEffect(this)\n }\n}\n\n/**\n * Create an effect to run arbitrary code in response to signal changes.\n *\n * An effect tracks which signals are accessed within the given callback\n * function `fn`, and re-runs the callback when those signals change.\n *\n * The callback may return a cleanup function. The cleanup function gets\n * run once, either when the callback is next called or when the effect\n * gets disposed, whichever happens first.\n *\n * @param fn The effect callback.\n * @returns A function for disposing the effect.\n */\nfunction effect(fn: EffectFn): () => void {\n const effect = new Effect(fn)\n try {\n effect._callback()\n } catch (error) {\n effect._dispose()\n // Throw the error, since it is already a Datastar error.\n throw error\n }\n // Return a bound function instead of a wrapper like `() => effect._dispose()`,\n // because bound functions seem to be just as fast and take up a lot less memory.\n return effect._dispose.bind(effect)\n}\n\nexport { batch, computed, effect, Signal, untracked }\nexport type { ReadonlySignal }\n", "import { type Computed, Signal, computed } from '../vendored/preact-core'\nimport { internalErr } from './errors'\nimport {\n DATASTAR_SIGNAL_EVENT,\n type DatastarSignalEvent,\n type NestedSignal,\n type NestedValues,\n} from './types'\n\nconst from = 'namespacedSignals'\n\nconst dispatchSignalEvent = (evt: Partial) => {\n document.dispatchEvent(\n new CustomEvent(DATASTAR_SIGNAL_EVENT, {\n detail: Object.assign({ added: [], removed: [], updated: [] }, evt),\n }),\n )\n}\n// If onlyPublic is true, only signals not starting with an underscore are included\nfunction nestedValues(\n signal: NestedSignal,\n onlyPublic = false,\n): Record {\n const kv: Record = {}\n for (const key in signal) {\n if (Object.hasOwn(signal, key)) {\n if (onlyPublic && key.startsWith('_')) {\n continue\n }\n const value = signal[key]\n if (value instanceof Signal) {\n kv[key] = value.value\n } else {\n kv[key] = nestedValues(value)\n }\n }\n }\n return kv\n}\n\nfunction mergeNested(\n target: NestedValues,\n values: NestedValues,\n onlyIfMissing = false,\n) {\n const evt: DatastarSignalEvent = {\n added: [],\n removed: [],\n updated: [],\n }\n for (const key in values) {\n if (Object.hasOwn(values, key)) {\n if (key.match(/\\_\\_+/)) {\n throw internalErr(from, 'InvalidSignalKey', { key })\n }\n\n const value = values[key]\n if (value instanceof Object && !Array.isArray(value)) {\n if (!target[key]) {\n target[key] = {}\n }\n const subEvt = mergeNested(\n target[key] as NestedValues,\n value as NestedValues,\n onlyIfMissing,\n )\n evt.added.push(...subEvt.added.map((k) => `${key}.${k}`))\n evt.removed.push(...subEvt.removed.map((k) => `${key}.${k}`))\n evt.updated.push(...subEvt.updated.map((k) => `${key}.${k}`))\n } else {\n const hasKey = Object.hasOwn(target, key)\n if (hasKey) {\n if (onlyIfMissing) continue\n const t = target[key]\n if (t instanceof Signal) {\n const oldValue = t.value\n t.value = value\n if (oldValue !== value) {\n evt.updated.push(key)\n }\n continue\n }\n }\n\n const s = new Signal(value)\n s._onChange = () => {\n dispatchSignalEvent({ updated: [key] })\n }\n target[key] = s\n\n evt.added.push(key)\n }\n }\n }\n return evt\n}\n\nfunction walkNestedSignal(\n signal: NestedSignal,\n cb: (dotDeliminatedPath: string, signal: Signal) => void,\n): void {\n for (const key in signal) {\n if (Object.hasOwn(signal, key)) {\n const value = signal[key]\n if (value instanceof Signal) {\n cb(key, value)\n } else {\n walkNestedSignal(value, (path, value) => {\n cb(`${key}.${path}`, value)\n })\n }\n }\n }\n}\n\n// Recursive function to subset a nested object, each key is a dot-delimited path\nfunction nestedSubset(original: NestedValues, ...keys: string[]): NestedValues {\n const subset: NestedValues = {}\n for (const key of keys) {\n const parts = key.split('.')\n let subOriginal = original\n let subSubset = subset\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i]\n if (!subOriginal[part]) {\n return {}\n }\n if (!subSubset[part]) {\n subSubset[part] = {}\n }\n subOriginal = subOriginal[part] as NestedValues\n subSubset = subSubset[part] as NestedValues\n }\n const last = parts[parts.length - 1]\n subSubset[last] = subOriginal[last]\n }\n return subset\n}\n\n// Recursively walk a NestedValue with a callback and dot-delimited path\nexport function walkNestedValues(\n nv: NestedValues,\n cb: (path: string, value: any) => void,\n) {\n for (const key in nv) {\n if (Object.hasOwn(nv, key)) {\n const value = nv[key]\n if (value instanceof Object && !Array.isArray(value)) {\n walkNestedValues(value, (path, value) => {\n cb(`${key}.${path}`, value)\n })\n } else {\n cb(key, value)\n }\n }\n }\n}\n\nexport class SignalsRoot {\n #signals: NestedSignal = {}\n\n exists(dotDelimitedPath: string): boolean {\n return !!this.signal(dotDelimitedPath)\n }\n\n signal(dotDelimitedPath: string): Signal | null {\n const parts = dotDelimitedPath.split('.')\n let subSignals = this.#signals\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i]\n if (!subSignals[part]) {\n return null\n }\n subSignals = subSignals[part] as NestedSignal\n }\n const last = parts[parts.length - 1]\n const signal = subSignals[last]\n if (!signal)\n throw internalErr(from, 'SignalNotFound', { path: dotDelimitedPath })\n return signal as Signal\n }\n\n setSignal>(dotDelimitedPath: string, signal: T) {\n const parts = dotDelimitedPath.split('.')\n let subSignals = this.#signals\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i]\n if (!subSignals[part]) {\n subSignals[part] = {}\n }\n subSignals = subSignals[part] as NestedSignal\n }\n const last = parts[parts.length - 1]\n subSignals[last] = signal\n }\n\n setComputed(dotDelimitedPath: string, fn: () => T) {\n const signal = computed(() => fn()) as Computed\n this.setSignal(dotDelimitedPath, signal)\n }\n\n value(dotDelimitedPath: string): T {\n const signal = this.signal(dotDelimitedPath) as Signal\n return signal?.value\n }\n\n setValue(dotDelimitedPath: string, value: T) {\n const { signal } = this.upsertIfMissing(dotDelimitedPath, value)\n const oldValue = signal.value\n signal.value = value\n if (oldValue !== value) {\n dispatchSignalEvent({ updated: [dotDelimitedPath] })\n }\n }\n\n upsertIfMissing(dotDelimitedPath: string, defaultValue: T) {\n const parts = dotDelimitedPath.split('.')\n let subSignals = this.#signals\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i]\n if (!subSignals[part]) {\n subSignals[part] = {}\n }\n subSignals = subSignals[part] as NestedSignal\n }\n const last = parts[parts.length - 1]\n\n const current = subSignals[last]\n if (current instanceof Signal) {\n return { signal: current as Signal, inserted: false }\n }\n\n const signal = new Signal(defaultValue)\n signal._onChange = () => {\n dispatchSignalEvent({ updated: [dotDelimitedPath] })\n }\n subSignals[last] = signal\n\n dispatchSignalEvent({ added: [dotDelimitedPath] })\n\n return { signal: signal, inserted: true }\n }\n\n remove(...dotDelimitedPaths: string[]) {\n if (!dotDelimitedPaths.length) {\n this.#signals = {}\n return\n }\n const removed = Array()\n for (const path of dotDelimitedPaths) {\n const parts = path.split('.')\n let subSignals = this.#signals\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i]\n if (!subSignals[part]) {\n return\n }\n subSignals = subSignals[part] as NestedSignal\n }\n const last = parts[parts.length - 1]\n delete subSignals[last]\n removed.push(path)\n }\n dispatchSignalEvent({ removed })\n }\n\n merge(other: NestedValues, onlyIfMissing = false) {\n const evt = mergeNested(this.#signals, other, onlyIfMissing)\n if (evt.added.length || evt.removed.length || evt.updated.length) {\n dispatchSignalEvent(evt)\n }\n }\n\n subset(...keys: string[]): NestedValues {\n return nestedSubset(this.values(), ...keys)\n }\n\n walk(cb: (name: string, signal: Signal) => void) {\n walkNestedSignal(this.#signals, cb)\n }\n\n paths() {\n const signalNames = new Array()\n this.walk((path) => signalNames.push(path))\n return signalNames\n }\n\n values(onlyPublic = false): NestedValues {\n return nestedValues(this.#signals, onlyPublic)\n }\n\n JSON(shouldIndent = true, onlyPublic = false) {\n const values = this.values(onlyPublic)\n if (!shouldIndent) {\n return JSON.stringify(values)\n }\n return JSON.stringify(values, null, 2)\n }\n\n public toString() {\n return this.JSON()\n }\n}\n", "import { Hash, attrHash, elUniqId, walkDOM } from '../utils/dom'\nimport { camel } from '../utils/text'\nimport { effect } from '../vendored/preact-core'\nimport { DSP, DSS } from './consts'\nimport { initErr, runtimeErr } from './errors'\nimport { SignalsRoot } from './signals'\nimport {\n type ActionPlugin,\n type ActionPlugins,\n type AttributePlugin,\n type DatastarPlugin,\n type GlobalInitializer,\n type HTMLorSVGElement,\n type InitContext,\n type OnRemovalFn,\n PluginType,\n Requirement,\n type RuntimeContext,\n type RuntimeExpressionFunction,\n type WatcherPlugin,\n} from './types'\n\nconst signals: SignalsRoot = new SignalsRoot()\nconst plugins: AttributePlugin[] = []\nconst actions: ActionPlugins = {}\nconst watchers: WatcherPlugin[] = []\n\nlet alias = ''\nexport function setAlias(value: string) {\n alias = value\n}\nlet mutationObserver: MutationObserver | null = null\n\n// Map of cleanup functions by element, keyed by the dataset key and value\nconst removals = new Map>()\n\nexport function load(...pluginsToLoad: DatastarPlugin[]) {\n for (const plugin of pluginsToLoad) {\n const ctx: InitContext = {\n signals,\n effect: (cb: () => void): OnRemovalFn => effect(cb),\n actions: actions,\n plugin,\n apply,\n }\n\n let globalInitializer: GlobalInitializer | undefined\n switch (plugin.type) {\n case PluginType.Watcher: {\n const wp = plugin as WatcherPlugin\n watchers.push(wp)\n globalInitializer = wp.onGlobalInit\n break\n }\n case PluginType.Action: {\n actions[plugin.name] = plugin as ActionPlugin\n break\n }\n case PluginType.Attribute: {\n const ap = plugin as AttributePlugin\n plugins.push(ap)\n globalInitializer = ap.onGlobalInit\n break\n }\n default: {\n throw initErr('InvalidPluginType', ctx)\n }\n }\n if (globalInitializer) {\n globalInitializer(ctx)\n }\n }\n\n // Sort attribute plugins by descending length then alphabetically\n plugins.sort((a, b) => {\n const lenDiff = b.name.length - a.name.length\n if (lenDiff !== 0) return lenDiff\n return a.name.localeCompare(b.name)\n })\n}\n\n// Apply all plugins to the element and its children\nexport function apply(\n rootElement: HTMLorSVGElement = document.documentElement,\n) {\n walkDOM(rootElement, (el) => {\n // Check if the element has any data attributes already\n const toApply = new Array()\n const elCleanups = removals.get(el) || new Map()\n const toCleanup = new Map([...elCleanups])\n const hashes = new Map()\n\n // Apply the plugins to the element in order of application\n // since DOMStringMap is ordered, we can be deterministic\n for (const datasetKey of Object.keys(el.dataset)) {\n // Ignore data attributes that don\u2019t start with the alias\n if (!datasetKey.startsWith(alias)) {\n break\n }\n\n const datasetValue = el.dataset[datasetKey] || ''\n const currentHash = attrHash(datasetKey, datasetValue)\n hashes.set(datasetKey, currentHash)\n\n // If the hash hasn't changed, ignore\n // otherwise keep the old cleanup and add new to applys\n if (elCleanups.has(currentHash)) {\n toCleanup.delete(currentHash)\n } else {\n toApply.push(datasetKey)\n }\n }\n\n // Clean up any old plugins and apply the new ones\n for (const [_, cleanup] of toCleanup) cleanup()\n for (const key of toApply) {\n const h = hashes.get(key)!\n applyAttributePlugin(el, key, h)\n }\n })\n\n observe()\n}\n\n// Set up a mutation observer to run plugin removal and apply functions\nfunction observe() {\n if (mutationObserver) {\n return\n }\n\n mutationObserver = new MutationObserver((mutations) => {\n const toRemove = new Set()\n const toApply = new Set()\n for (const { target, type, addedNodes, removedNodes } of mutations) {\n switch (type) {\n case 'childList':\n {\n for (const node of removedNodes) {\n toRemove.add(node as HTMLorSVGElement)\n }\n for (const node of addedNodes) {\n toApply.add(node as HTMLorSVGElement)\n }\n }\n break\n case 'attributes': {\n toApply.add(target as HTMLorSVGElement)\n\n break\n }\n }\n }\n for (const el of toRemove) {\n const elTracking = removals.get(el)\n if (elTracking) {\n for (const [h, cleanup] of elTracking) {\n cleanup()\n elTracking.delete(h)\n }\n if (elTracking.size === 0) {\n removals.delete(el)\n }\n }\n }\n for (const el of toApply) {\n apply(el)\n }\n })\n\n mutationObserver.observe(document.body, {\n attributes: true,\n attributeOldValue: true,\n childList: true,\n subtree: true,\n })\n}\n\nfunction applyAttributePlugin(\n el: HTMLorSVGElement,\n camelCasedKey: string,\n hash: number,\n) {\n // Extract the raw key from the dataset\n const rawKey = camel(camelCasedKey.slice(alias.length))\n\n // Find the plugin that matches, since the plugins are sorted by length descending and alphabetically. The first match will be the most specific.\n const plugin = plugins.find((p) => rawKey.startsWith(p.name))\n\n // Skip if no plugin is found\n if (!plugin) return\n\n // Ensure the element has an id\n if (!el.id.length) el.id = elUniqId(el)\n\n // Extract the key and modifiers\n let [key, ...rawModifiers] = rawKey.slice(plugin.name.length).split(/\\_\\_+/)\n\n const hasKey = key.length > 0\n if (hasKey) {\n key = camel(key)\n }\n const value = el.dataset[camelCasedKey] || ''\n const hasValue = value.length > 0\n\n // Create the runtime context\n const ctx: RuntimeContext = {\n signals,\n apply,\n effect: (cb: () => void): OnRemovalFn => effect(cb),\n actions: actions,\n genRX: () => genRX(ctx, ...(plugin.argNames || [])),\n plugin,\n el,\n rawKey,\n key,\n value,\n mods: new Map(),\n }\n\n // Check the requirements\n const keyReq = plugin.keyReq || Requirement.Allowed\n if (hasKey) {\n if (keyReq === Requirement.Denied) {\n throw runtimeErr(`${plugin.name}KeyNotAllowed`, ctx)\n }\n } else if (keyReq === Requirement.Must) {\n throw runtimeErr(`${plugin.name}KeyRequired`, ctx)\n }\n\n const valReq = plugin.valReq || Requirement.Allowed\n if (hasValue) {\n if (valReq === Requirement.Denied) {\n throw runtimeErr(`${plugin.name}ValueNotAllowed`, ctx)\n }\n } else if (valReq === Requirement.Must) {\n throw runtimeErr(`${plugin.name}ValueRequired`, ctx)\n }\n\n // Check for exclusive requirements\n if (keyReq === Requirement.Exclusive || valReq === Requirement.Exclusive) {\n if (hasKey && hasValue) {\n throw runtimeErr(`${plugin.name}KeyAndValueProvided`, ctx)\n }\n if (!hasKey && !hasValue) {\n throw runtimeErr(`${plugin.name}KeyOrValueRequired`, ctx)\n }\n }\n\n for (const rawMod of rawModifiers) {\n const [label, ...mod] = rawMod.split('.')\n ctx.mods.set(camel(label), new Set(mod.map((t) => t.toLowerCase())))\n }\n\n // Load the plugin and store any cleanup functions\n const cleanup = plugin.onLoad(ctx)\n if (cleanup) {\n let elTracking = removals.get(el)\n if (!elTracking) {\n elTracking = new Map()\n removals.set(el, elTracking)\n }\n elTracking.set(hash, cleanup)\n }\n}\n\nfunction genRX(\n ctx: RuntimeContext,\n ...argNames: string[]\n): RuntimeExpressionFunction {\n let userExpression = ''\n\n // This regex allows Datastar expressions to support nested\n // regex and strings that contain ; without breaking.\n //\n // Each of these regex defines a block type we want to match\n // (importantly we ignore the content within these blocks):\n //\n // regex \\/(\\\\\\/|[^\\/])*\\/\n // double quotes \"(\\\\\"|[^\\\"])*\"\n // single quotes '(\\\\'|[^'])*'\n // ticks `(\\\\`|[^`])*`\n //\n // We also want to match the non delimiter part of statements\n // note we only support ; statement delimiters:\n //\n // [^;]\n //\n const statementRe =\n /(\\/(\\\\\\/|[^\\/])*\\/|\"(\\\\\"|[^\\\"])*\"|'(\\\\'|[^'])*'|`(\\\\`|[^`])*`|[^;])+/gm\n const statements = ctx.value.trim().match(statementRe)\n if (statements) {\n const lastIdx = statements.length - 1\n const last = statements[lastIdx].trim()\n if (!last.startsWith('return')) {\n statements[lastIdx] = `return (${last});`\n }\n userExpression = statements.join(';\\n')\n }\n\n // Ignore any escaped values\n const escaped = new Map()\n const escapeRe = new RegExp(`(?:${DSP})(.*?)(?:${DSS})`, 'gm')\n for (const match of userExpression.matchAll(escapeRe)) {\n const k = match[1]\n const v = new Hash('dsEscaped').with(k).string\n escaped.set(v, k)\n userExpression = userExpression.replace(DSP + k + DSS, v)\n }\n\n const fnCall = /@(\\w*)\\(/gm\n const matches = userExpression.matchAll(fnCall)\n const methodsCalled = new Set()\n for (const match of matches) {\n methodsCalled.add(match[1])\n }\n\n // Replace any action calls\n const actionsRe = new RegExp(`@(${Object.keys(actions).join('|')})\\\\(`, 'gm')\n\n // Add ctx to action calls\n userExpression = userExpression.replaceAll(\n actionsRe,\n 'ctx.actions.$1.fn(ctx,',\n )\n\n // Replace any signal calls\n const signalNames = ctx.signals.paths()\n if (signalNames.length) {\n // Match any valid `$signalName` followed by a non-word character or end of string\n const signalsRe = new RegExp(`\\\\$(${signalNames.join('|')})(\\\\W|$)`, 'gm')\n userExpression = userExpression.replaceAll(\n signalsRe,\n `ctx.signals.signal('$1').value$2`,\n )\n }\n\n // Replace any escaped values\n for (const [k, v] of escaped) {\n userExpression = userExpression.replace(k, v)\n }\n\n const fnContent = `return (()=> {\\n${userExpression}\\n})()` // Wrap in IIFE\n ctx.fnContent = fnContent\n\n try {\n const fn = new Function('ctx', ...argNames, fnContent)\n return (...args: any[]) => {\n try {\n return fn(ctx, ...args)\n } catch (error: any) {\n throw runtimeErr('ExecuteExpression', ctx, {\n error: error.message,\n })\n }\n }\n } catch (error: any) {\n throw runtimeErr('GenerateExpression', ctx, {\n error: error.message,\n })\n }\n}\n", "import { DSP } from '../engine/consts'\n// @ts-ignore\nconst _ = DSP // This is to force the import of DSP first in the compiled code\n\nimport { Computed } from '../plugins/official/core/attributes/computed'\nimport { Signals } from '../plugins/official/core/attributes/signals'\nimport { Star } from '../plugins/official/core/attributes/star'\nimport { apply, load, setAlias } from './engine'\n\nload(Star, Signals, Computed)\n\nexport { apply, load, setAlias }\n", "import { runtimeErr } from '../engine/errors'\nimport type { RuntimeContext } from '../engine/types'\n\n/**\n * Represents a message sent in an event stream\n * https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#Event_stream_format\n */\nexport interface EventSourceMessage {\n /** The event ID to set the EventSource object's last event ID value. */\n id: string\n /** A string identifying the type of event described. */\n event: string\n /** The event data */\n data: string\n /** The reconnection interval (in milliseconds) to wait before retrying the connection */\n retry?: number\n}\n\n/**\n * Converts a ReadableStream into a callback pattern.\n * @param stream The input ReadableStream.\n * @param onChunk A function that will be called on each new byte chunk in the stream.\n * @returns {Promise} A promise that will be resolved when the stream closes.\n */\nexport async function getBytes(\n stream: ReadableStream,\n onChunk: (arr: Uint8Array) => void,\n) {\n const reader = stream.getReader()\n let result: ReadableStreamReadResult\n while (!(result = await reader.read()).done) {\n onChunk(result.value)\n }\n}\n\nenum ControlChars {\n NewLine = 10,\n CarriageReturn = 13,\n Space = 32,\n Colon = 58,\n}\n\n/**\n * Parses arbitary byte chunks into EventSource line buffers.\n * Each line should be of the format \"field: value\" and ends with \\r, \\n, or \\r\\n.\n * @param onLine A function that will be called on each new EventSource line.\n * @returns A function that should be called for each incoming byte chunk.\n */\nexport function getLines(\n onLine: (line: Uint8Array, fieldLength: number) => void,\n) {\n let buffer: Uint8Array | undefined\n let position: number // current read position\n let fieldLength: number // length of the `field` portion of the line\n let discardTrailingNewline = false\n\n // return a function that can process each incoming byte chunk:\n return function onChunk(arr: Uint8Array) {\n if (buffer === undefined) {\n buffer = arr\n position = 0\n fieldLength = -1\n } else {\n // we're still parsing the old line. Append the new bytes into buffer:\n buffer = concat(buffer, arr)\n }\n\n const bufLength = buffer.length\n let lineStart = 0 // index where the current line starts\n while (position < bufLength) {\n if (discardTrailingNewline) {\n if (buffer[position] === ControlChars.NewLine) {\n lineStart = ++position // skip to next char\n }\n\n discardTrailingNewline = false\n }\n\n // start looking forward till the end of line:\n let lineEnd = -1 // index of the \\r or \\n char\n for (; position < bufLength && lineEnd === -1; ++position) {\n switch (buffer[position]) {\n case ControlChars.Colon:\n if (fieldLength === -1) {\n // first colon in line\n fieldLength = position - lineStart\n }\n break\n // @ts-ignore:7029 \\r case below should fallthrough to \\n:\n // biome-ignore lint/suspicious/noFallthroughSwitchClause: \n case ControlChars.CarriageReturn:\n discardTrailingNewline = true\n case ControlChars.NewLine:\n lineEnd = position\n break\n }\n }\n\n if (lineEnd === -1) {\n // We reached the end of the buffer but the line hasn't ended.\n // Wait for the next arr and then continue parsing:\n break\n }\n\n // we've reached the line end, send it out:\n onLine(buffer.subarray(lineStart, lineEnd), fieldLength)\n lineStart = position // we're now on the next line\n fieldLength = -1\n }\n\n if (lineStart === bufLength) {\n buffer = undefined // we've finished reading it\n } else if (lineStart !== 0) {\n // Create a new view into buffer beginning at lineStart so we don't\n // need to copy over the previous lines when we get the new arr:\n buffer = buffer.subarray(lineStart)\n position -= lineStart\n }\n }\n}\n\n/**\n * Parses line buffers into EventSourceMessages.\n * @param onId A function that will be called on each `id` field.\n * @param onRetry A function that will be called on each `retry` field.\n * @param onMessage A function that will be called on each message.\n * @returns A function that should be called for each incoming line buffer.\n */\nexport function getMessages(\n onId: (id: string) => void,\n onRetry: (retry: number) => void,\n onMessage?: (msg: EventSourceMessage) => void,\n) {\n let message = newMessage()\n const decoder = new TextDecoder()\n\n // return a function that can process each incoming line buffer:\n return function onLine(line: Uint8Array, fieldLength: number) {\n if (line.length === 0) {\n // empty line denotes end of message. Trigger the callback and start a new message:\n onMessage?.(message)\n message = newMessage()\n } else if (fieldLength > 0) {\n // exclude comments and lines with no values\n // line is of format \":\" or \": \"\n // https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation\n const field = decoder.decode(line.subarray(0, fieldLength))\n const valueOffset =\n fieldLength + (line[fieldLength + 1] === ControlChars.Space ? 2 : 1)\n const value = decoder.decode(line.subarray(valueOffset))\n\n switch (field) {\n case 'data':\n // if this message already has data, append the new value to the old.\n // otherwise, just set to the new value:\n message.data = message.data ? `${message.data}\\n${value}` : value // otherwise,\n break\n case 'event':\n message.event = value\n break\n case 'id':\n onId((message.id = value))\n break\n case 'retry': {\n const retry = Number.parseInt(value, 10)\n if (!Number.isNaN(retry)) {\n // per spec, ignore non-integers\n onRetry((message.retry = retry))\n }\n break\n }\n }\n }\n }\n}\n\nfunction concat(a: Uint8Array, b: Uint8Array) {\n const res = new Uint8Array(a.length + b.length)\n res.set(a)\n res.set(b, a.length)\n return res\n}\n\nfunction newMessage(): EventSourceMessage {\n // data, event, and id must be initialized to empty strings:\n // https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation\n // retry should be initialized to undefined so we return a consistent shape\n // to the js engine all the time: https://mathiasbynens.be/notes/shapes-ics#takeaways\n return {\n data: '',\n event: '',\n id: '',\n retry: undefined,\n }\n}\n\nexport const EventStreamContentType = 'text/event-stream'\n\nconst LastEventId = 'last-event-id'\n\nexport interface FetchEventSourceInit extends RequestInit {\n /**\n * The request headers. FetchEventSource only supports the Record format.\n */\n headers?: Record\n\n /**\n * Called when a response is received. Use this to validate that the response\n * actually matches what you expect (and throw if it doesn't.) If not provided,\n * will default to a basic validation to ensure the content-type is text/event-stream.\n */\n onopen?: (response: Response) => Promise\n\n /**\n * Called when a message is received. NOTE: Unlike the default browser\n * EventSource.onmessage, this callback is called for _all_ events,\n * even ones with a custom `event` field.\n */\n onmessage?: (ev: EventSourceMessage) => void\n\n /**\n * Called when a response finishes. If you don't expect the server to kill\n * the connection, you can throw an exception here and retry using onerror.\n */\n onclose?: () => void\n\n /**\n * Called when there is any error making the request / processing messages /\n * handling callbacks etc. Use this to control the retry strategy: if the\n * error is fatal, rethrow the error inside the callback to stop the entire\n * operation. Otherwise, you can return an interval (in milliseconds) after\n * which the request will automatically retry (with the last-event-id).\n * If this callback is not specified, or it returns undefined, fetchEventSource\n * will treat every error as retriable and will try again after 1 second.\n */\n onerror?: (err: any) => number | null | undefined | void\n\n /**\n * If true, will keep the request open even if the document is hidden.\n * By default, fetchEventSource will close the request and reopen it\n * automatically when the document becomes visible again.\n */\n openWhenHidden?: boolean\n\n /** The Fetch function to use. Defaults to window.fetch */\n fetch?: typeof fetch\n\n /** The retry interval in milliseconds. Defaults to 1_000 */\n retryInterval?: number\n\n /** The scaler for the retry interval. Defaults to 2 */\n retryScaler?: number\n\n /** The maximum retry interval in milliseconds. Defaults to 30_000 */\n retryMaxWaitMs?: number\n\n /** The maximum number of retries before giving up. Defaults to 10 */\n retryMaxCount?: number\n}\n\nexport function fetchEventSource(\n ctx: RuntimeContext,\n input: RequestInfo,\n {\n signal: inputSignal,\n headers: inputHeaders,\n onopen: inputOnOpen,\n onmessage,\n onclose,\n onerror,\n openWhenHidden,\n fetch: inputFetch,\n retryInterval = 1_000,\n retryScaler = 2,\n retryMaxWaitMs = 30_000,\n retryMaxCount = 10,\n ...rest\n }: FetchEventSourceInit,\n) {\n return new Promise((resolve, reject) => {\n let retries = 0\n\n // make a copy of the input headers since we may modify it below:\n const headers = { ...inputHeaders }\n if (!headers.accept) {\n headers.accept = EventStreamContentType\n }\n\n let curRequestController: AbortController\n function onVisibilityChange() {\n curRequestController.abort() // close existing request on every visibility change\n if (!document.hidden) {\n create() // page is now visible again, recreate request.\n }\n }\n\n if (!openWhenHidden) {\n document.addEventListener('visibilitychange', onVisibilityChange)\n }\n\n let retryTimer = 0\n function dispose() {\n document.removeEventListener('visibilitychange', onVisibilityChange)\n window.clearTimeout(retryTimer)\n curRequestController.abort()\n }\n\n // if the incoming signal aborts, dispose resources and resolve:\n inputSignal?.addEventListener('abort', () => {\n dispose()\n resolve() // don't waste time constructing/logging errors\n })\n\n const fetch = inputFetch ?? window.fetch\n const onopen = inputOnOpen ?? function defaultOnOpen() {}\n\n async function create() {\n curRequestController = new AbortController()\n try {\n const response = await fetch(input, {\n ...rest,\n headers,\n signal: curRequestController.signal,\n })\n\n await onopen(response)\n\n await getBytes(\n response.body!,\n getLines(\n getMessages(\n (id) => {\n if (id) {\n // signals the id and send it back on the next retry:\n headers[LastEventId] = id\n } else {\n // don't send the last-event-id header anymore:\n delete headers[LastEventId]\n }\n },\n (retry) => {\n retryInterval = retry\n },\n onmessage,\n ),\n ),\n )\n\n onclose?.()\n dispose()\n resolve()\n } catch (err) {\n if (!curRequestController.signal.aborted) {\n // if we haven't aborted the request ourselves:\n try {\n // check if we need to retry:\n const interval: any = onerror?.(err) ?? retryInterval\n window.clearTimeout(retryTimer)\n retryTimer = window.setTimeout(create, interval)\n retryInterval *= retryScaler // exponential backoff\n retryInterval = Math.min(retryInterval, retryMaxWaitMs)\n retries++\n if (retries >= retryMaxCount) {\n // we should not retry anymore:\n dispose()\n // Max retries hit, check your server or network connection\n reject(runtimeErr('SseMaxRetries', ctx, { retryMaxCount }))\n } else {\n console.error(\n `Datastar failed to reach ${rest.method}: ${input.toString()} retry in ${interval}ms`,\n )\n }\n } catch (innerErr) {\n // we should not retry anymore:\n dispose()\n reject(innerErr)\n }\n }\n }\n }\n\n create()\n })\n}\n", "import { DATASTAR } from '../../../engine/consts'\n\nexport const DATASTAR_SSE_EVENT = `${DATASTAR}-sse`\nexport const SETTLING_CLASS = `${DATASTAR}-settling`\nexport const SWAPPING_CLASS = `${DATASTAR}-swapping`\nexport const STARTED = 'started'\nexport const FINISHED = 'finished'\nexport const ERROR = 'error'\nexport const RETRYING = 'retrying'\n\nexport interface DatastarSSEEvent {\n type: string\n argsRaw: Record\n}\n\nexport interface CustomEventMap {\n [DATASTAR_SSE_EVENT]: CustomEvent\n}\nexport type WatcherFn = (\n this: Document,\n ev: CustomEventMap[K],\n) => void\n\ndeclare global {\n interface Document {\n //adds definition to Document, but you can do the same with HTMLElement\n addEventListener(\n type: K,\n listener: WatcherFn,\n ): void\n removeEventListener(\n type: K,\n listener: WatcherFn,\n ): void\n dispatchEvent(ev: CustomEventMap[K]): void\n }\n}\n\nexport function datastarSSEEventWatcher(\n eventType: string,\n fn: (argsRaw: Record) => void,\n) {\n document.addEventListener(\n DATASTAR_SSE_EVENT,\n (event: CustomEvent) => {\n if (event.detail.type !== eventType) return\n const { argsRaw } = event.detail\n fn(argsRaw)\n },\n )\n}\n", "// Icon: ic:baseline-get-app\n// Slug: Use a GET request to fetch data from a server using Server-Sent Events matching the Datastar SDK interface\n// Description: Remember, SSE is just a regular SSE request but with the ability to send 0-inf messages to the client.\n\nimport { DATASTAR, DATASTAR_REQUEST, DefaultSseRetryDurationMs } from '../../../../engine/consts'\nimport { runtimeErr } from '../../../../engine/errors'\nimport type { RuntimeContext } from '../../../../engine/types'\nimport {\n type FetchEventSourceInit,\n fetchEventSource,\n} from '../../../../vendored/fetch-event-source'\nimport {\n DATASTAR_SSE_EVENT,\n type DatastarSSEEvent,\n ERROR,\n FINISHED,\n RETRYING,\n STARTED,\n} from '../shared'\n\nfunction dispatchSSE(type: string, argsRaw: Record) {\n document.dispatchEvent(\n new CustomEvent(DATASTAR_SSE_EVENT, {\n detail: { type, argsRaw },\n }),\n )\n}\n\nconst isWrongContent = (err: any) => `${err}`.includes('text/event-stream')\n\nexport type SSEArgs = {\n headers?: Record\n openWhenHidden?: boolean\n retryInterval?: number\n retryScaler?: number\n retryMaxWaitMs?: number\n retryMaxCount?: number\n abort?: AbortSignal\n} & (\n | {\n contentType: 'json'\n includeLocal?: boolean\n }\n | {\n contentType: 'form'\n selector?: string\n }\n)\n\nexport const sse = async (\n ctx: RuntimeContext,\n method: string,\n url: string,\n args: SSEArgs,\n) => {\n const {\n el: { id: elId },\n el,\n signals,\n } = ctx\n const {\n headers: userHeaders,\n contentType,\n includeLocal,\n selector,\n openWhenHidden,\n retryInterval,\n retryScaler,\n retryMaxWaitMs,\n retryMaxCount,\n abort,\n } = Object.assign(\n {\n headers: {},\n contentType: 'json',\n includeLocal: false,\n selector: null,\n openWhenHidden: false, // will keep the request open even if the document is hidden.\n retryInterval: DefaultSseRetryDurationMs, // the retry interval in milliseconds\n retryScaler: 2, // the amount to multiply the retry interval by each time\n retryMaxWaitMs: 30_000, // the maximum retry interval in milliseconds\n retryMaxCount: 10, // the maximum number of retries before giving up\n abort: undefined,\n },\n args,\n )\n const action = method.toLowerCase()\n let cleanupFn = (): void => {}\n try {\n dispatchSSE(STARTED, { elId })\n if (!url?.length) {\n throw runtimeErr('SseNoUrlProvided', ctx, { action })\n }\n\n const initialHeaders: Record = {}\n initialHeaders[DATASTAR_REQUEST] = true\n // We ignore the content-type header if using form data\n // if missing the boundary will be set automatically\n if (contentType === 'json') {\n initialHeaders['Content-Type'] = 'application/json'\n }\n const headers = Object.assign({}, initialHeaders, userHeaders)\n\n const req: FetchEventSourceInit = {\n method,\n headers,\n openWhenHidden,\n retryInterval,\n retryScaler,\n retryMaxWaitMs,\n retryMaxCount,\n signal: abort,\n onopen: async (response: Response) => {\n if (response.status >= 400) {\n const status = response.status.toString()\n dispatchSSE(ERROR, { status })\n }\n },\n onmessage: (evt) => {\n if (!evt.event.startsWith(DATASTAR)) {\n return\n }\n const type = evt.event\n const argsRawLines: Record = {}\n\n const lines = evt.data.split('\\n')\n for (const line of lines) {\n const colonIndex = line.indexOf(' ')\n const key = line.slice(0, colonIndex)\n let argLines = argsRawLines[key]\n if (!argLines) {\n argLines = []\n argsRawLines[key] = argLines\n }\n const value = line.slice(colonIndex + 1)\n argLines.push(value)\n }\n\n const argsRaw: Record = {}\n for (const [key, lines] of Object.entries(argsRawLines)) {\n argsRaw[key] = lines.join('\\n')\n }\n\n // if you aren't seeing your event you can debug by using this line in the console\n dispatchSSE(type, argsRaw)\n },\n onerror: (error) => {\n if (isWrongContent(error)) {\n // don't retry if the content-type is wrong\n throw runtimeErr('InvalidContentType', ctx, { url })\n }\n // do nothing and it will retry\n if (error) {\n console.error(error.message)\n dispatchSSE(RETRYING, { message: error.message })\n }\n },\n }\n\n const urlInstance = new URL(url, window.location.origin)\n const queryParams = new URLSearchParams(urlInstance.search)\n\n if (contentType === 'json') {\n const json = signals.JSON(false, !includeLocal)\n if (method === 'GET') {\n queryParams.set(DATASTAR, json)\n } else {\n req.body = json\n }\n } else if (contentType === 'form') {\n const formEl = selector\n ? document.querySelector(selector)\n : el.closest('form')\n if (formEl === null) {\n if (selector) {\n throw runtimeErr('SseFormNotFound', ctx, { action, selector })\n }\n throw runtimeErr('SseClosestFormNotFound', ctx, { action })\n }\n if (el !== formEl) {\n const preventDefault = (evt: Event) => evt.preventDefault()\n formEl.addEventListener('submit', preventDefault)\n cleanupFn = (): void =>\n formEl.removeEventListener('submit', preventDefault)\n }\n if (!formEl.checkValidity()) {\n formEl.reportValidity()\n cleanupFn()\n return\n }\n const formData = new FormData(formEl)\n if (method === 'GET') {\n const formParams = new URLSearchParams(formData as any)\n for (const [key, value] of formParams) {\n queryParams.set(key, value)\n }\n } else {\n req.body = formData\n }\n } else {\n throw runtimeErr('SseInvalidContentType', ctx, { action, contentType })\n }\n\n urlInstance.search = queryParams.toString()\n\n try {\n await fetchEventSource(ctx, urlInstance.toString(), req)\n } catch (error) {\n if (!isWrongContent(error)) {\n throw runtimeErr('SseFetchFailed', ctx, { method, url, error })\n }\n // exit gracefully and do nothing if the content-type is wrong\n // this can happen if the client is sending a request\n // where no response is expected, and they haven't\n // set the content-type to text/event-stream\n }\n } finally {\n dispatchSSE(FINISHED, { elId })\n cleanupFn()\n }\n}\n", "// Icon: material-symbols:delete-outline\n// Slug: Use a DELETE request to fetch data from a server using Server-Sent Events matching the Datastar SDK interface\n// Description: Remember, SSE is just a regular SSE request but with the ability to send 0-inf messages to the client.\n\nimport {\n type ActionPlugin,\n PluginType,\n type RuntimeContext,\n} from '../../../../engine/types'\nimport { type SSEArgs, sse } from './sse'\n\nexport const DELETE: ActionPlugin = {\n type: PluginType.Action,\n name: 'delete',\n fn: async (ctx: RuntimeContext, url: string, args: SSEArgs) => {\n return sse(ctx, 'DELETE', url, { ...args })\n },\n}\n", "// Icon: ic:baseline-get-app\n// Slug: Use a GET request to fetch data from a server using Server-Sent Events matching the Datastar SDK interface\n// Description: Remember, SSE is just a regular SSE request but with the ability to send 0-inf messages to the client.\n\nimport {\n type ActionPlugin,\n PluginType,\n type RuntimeContext,\n} from '../../../../engine/types'\nimport { type SSEArgs, sse } from './sse'\n\nexport const GET: ActionPlugin = {\n type: PluginType.Action,\n name: 'get',\n fn: async (ctx: RuntimeContext, url: string, args: SSEArgs) => {\n return sse(ctx, 'GET', url, { ...args })\n },\n}\n", "// Icon: fluent:patch-24-filled\n// Slug: Use a PATCH request to fetch data from a server using Server-Sent Events matching the Datastar SDK interface\n// Description: Remember, SSE is just a regular SSE request but with the ability to send 0-inf messages to the client.\n\nimport {\n type ActionPlugin,\n PluginType,\n type RuntimeContext,\n} from '../../../../engine/types'\nimport { type SSEArgs, sse } from './sse'\n\nexport const PATCH: ActionPlugin = {\n type: PluginType.Action,\n name: 'patch',\n fn: async (ctx: RuntimeContext, url: string, args: SSEArgs) => {\n return sse(ctx, 'PATCH', url, { ...args })\n },\n}\n", "// Icon: ri:signpost-fill\n// Slug: Use a POST request to fetch data from a server using Server-Sent Events matching the Datastar SDK interface\n// Description: Remember, SSE is just a regular SSE request but with the ability to send 0-inf messages to the client.\n\nimport {\n type ActionPlugin,\n PluginType,\n type RuntimeContext,\n} from '../../../../engine/types'\nimport { type SSEArgs, sse } from './sse'\n\nexport const POST: ActionPlugin = {\n type: PluginType.Action,\n name: 'post',\n fn: async (ctx: RuntimeContext, url: string, args: SSEArgs) => {\n return sse(ctx, 'POST', url, { ...args })\n },\n}\n", "// Icon: material-symbols:arrows-input\n// Slug: Use a PUT request to fetch data from a server using Server-Sent Events matching the Datastar SDK interface\n// Description: Remember, SSE is just a regular SSE request but with the ability to send 0-inf messages to the client.\n\nimport {\n type ActionPlugin,\n PluginType,\n type RuntimeContext,\n} from '../../../../engine/types'\nimport { type SSEArgs, sse } from './sse'\n\nexport const PUT: ActionPlugin = {\n type: PluginType.Action,\n name: 'put',\n fn: async (ctx: RuntimeContext, url: string, args: SSEArgs) => {\n return sse(ctx, 'PUT', url, { ...args })\n },\n}\n", "// Icon: material-symbols:network-wifi\n// Slug: Sets the indicator signal used when fetching data via SSE\n// Description: must be a valid signal name\n\nimport {\n type AttributePlugin,\n PluginType,\n Requirement,\n} from '../../../../engine/types'\nimport { modifyCasing, trimDollarSignPrefix } from '../../../../utils/text'\nimport {\n DATASTAR_SSE_EVENT,\n type DatastarSSEEvent,\n FINISHED,\n STARTED,\n} from '../shared'\n\nexport const Indicator: AttributePlugin = {\n type: PluginType.Attribute,\n name: 'indicator',\n keyReq: Requirement.Exclusive,\n valReq: Requirement.Exclusive,\n onLoad: ({ el, key, mods, signals, value }) => {\n const signalName = key ? modifyCasing(key, mods) : trimDollarSignPrefix(value)\n const { signal } = signals.upsertIfMissing(signalName, false)\n const watcher = (event: CustomEvent) => {\n const {\n type,\n argsRaw: { elId },\n } = event.detail\n if (elId !== el.id) return\n switch (type) {\n case STARTED:\n signal.value = true\n break\n case FINISHED:\n signal.value = false\n break\n }\n }\n document.addEventListener(DATASTAR_SSE_EVENT, watcher)\n\n return () => {\n document.removeEventListener(DATASTAR_SSE_EVENT, watcher)\n }\n },\n}\n", "// Icon: tabler:file-type-js\n// Slug: Execute JavaScript using a Server-Sent Event\n// Description: Remember, SSE is just a regular SSE request but with the ability to send 0-inf messages to the client.\n\nimport {\n DefaultExecuteScriptAttributes,\n DefaultExecuteScriptAutoRemove,\n EventTypes,\n} from '../../../../engine/consts'\nimport { initErr } from '../../../../engine/errors'\nimport { PluginType, type WatcherPlugin } from '../../../../engine/types'\nimport { isBoolString } from '../../../../utils/text'\nimport { datastarSSEEventWatcher } from '../shared'\n\nexport const ExecuteScript: WatcherPlugin = {\n type: PluginType.Watcher,\n name: EventTypes.ExecuteScript,\n onGlobalInit: async (ctx) => {\n datastarSSEEventWatcher(\n EventTypes.ExecuteScript,\n ({\n autoRemove: autoRemoveRaw = `${DefaultExecuteScriptAutoRemove}`,\n attributes: attributesRaw = DefaultExecuteScriptAttributes,\n script,\n }) => {\n const autoRemove = isBoolString(autoRemoveRaw)\n if (!script?.length) {\n throw initErr('NoScriptProvided', ctx)\n }\n const scriptEl = document.createElement('script')\n for (const attr of attributesRaw.split('\\n')) {\n const pivot = attr.indexOf(' ')\n const key = pivot ? attr.slice(0, pivot) : attr\n const value = pivot ? attr.slice(pivot) : ''\n scriptEl.setAttribute(key.trim(), value.trim())\n }\n scriptEl.text = script\n document.head.appendChild(scriptEl)\n if (autoRemove) {\n scriptEl.remove()\n }\n },\n )\n },\n}\n", "export interface DocumentSupportingViewTransitionAPI {\n startViewTransition(\n updateCallback: () => Promise | void,\n ): IViewTransition\n}\n\nexport interface IViewTransition {\n finished: Promise\n ready: Promise\n updateCallbackDone: Promise\n skipTransition(): void\n}\n\nexport const docWithViewTransitionAPI =\n document as unknown as DocumentSupportingViewTransitionAPI\nexport const supportsViewTransitions =\n !!docWithViewTransitionAPI.startViewTransition\n", "/**\n * @typedef {object} ConfigHead\n *\n * @property {'merge' | 'append' | 'morph' | 'none'} [style]\n * @property {boolean} [block]\n * @property {boolean} [ignore]\n * @property {function(Element): boolean} [shouldPreserve]\n * @property {function(Element): boolean} [shouldReAppend]\n * @property {function(Element): boolean} [shouldRemove]\n * @property {function(Element, {added: Node[], kept: Element[], removed: Element[]}): void} [afterHeadMorphed]\n */\n\n/**\n * @typedef {object} ConfigCallbacks\n *\n * @property {function(Node): boolean} [beforeNodeAdded]\n * @property {function(Node): void} [afterNodeAdded]\n * @property {function(Element, Node): boolean} [beforeNodeMorphed]\n * @property {function(Element, Node): void} [afterNodeMorphed]\n * @property {function(Element): boolean} [beforeNodeRemoved]\n * @property {function(Element): void} [afterNodeRemoved]\n * @property {function(string, Element, \"update\" | \"remove\"): boolean} [beforeAttributeUpdated]\n */\n\n/**\n * @typedef {object} Config\n *\n * @property {'outerHTML' | 'innerHTML'} [morphStyle]\n * @property {boolean} [ignoreActive]\n * @property {boolean} [ignoreActiveValue]\n * @property {boolean} [restoreFocus]\n * @property {ConfigCallbacks} [callbacks]\n * @property {ConfigHead} [head]\n */\n\n/**\n * @typedef {function} NoOp\n *\n * @returns {void}\n */\n\n/**\n * @typedef {object} ConfigHeadInternal\n *\n * @property {'merge' | 'append' | 'morph' | 'none'} style\n * @property {boolean} [block]\n * @property {boolean} [ignore]\n * @property {(function(Element): boolean) | NoOp} shouldPreserve\n * @property {(function(Element): boolean) | NoOp} shouldReAppend\n * @property {(function(Element): boolean) | NoOp} shouldRemove\n * @property {(function(Element, {added: Node[], kept: Element[], removed: Element[]}): void) | NoOp} afterHeadMorphed\n */\n\n/**\n * @typedef {object} ConfigCallbacksInternal\n *\n * @property {(function(Node): boolean) | NoOp} beforeNodeAdded\n * @property {(function(Node): void) | NoOp} afterNodeAdded\n * @property {(function(Node, Node): boolean) | NoOp} beforeNodeMorphed\n * @property {(function(Node, Node): void) | NoOp} afterNodeMorphed\n * @property {(function(Node): boolean) | NoOp} beforeNodeRemoved\n * @property {(function(Node): void) | NoOp} afterNodeRemoved\n * @property {(function(string, Element, \"update\" | \"remove\"): boolean) | NoOp} beforeAttributeUpdated\n */\n\n/**\n * @typedef {object} ConfigInternal\n *\n * @property {'outerHTML' | 'innerHTML'} morphStyle\n * @property {boolean} [ignoreActive]\n * @property {boolean} [ignoreActiveValue]\n * @property {boolean} [restoreFocus]\n * @property {ConfigCallbacksInternal} callbacks\n * @property {ConfigHeadInternal} head\n */\n\n/**\n * @typedef {Object} IdSets\n * @property {Set} persistentIds\n * @property {Map>} idMap\n */\n\n/**\n * @typedef {Function} Morph\n *\n * @param {Element | Document} oldNode\n * @param {Element | Node | HTMLCollection | Node[] | string | null} newContent\n * @param {Config} [config]\n * @returns {undefined | Node[]}\n */\n\n// base IIFE to define idiomorph\n/**\n *\n * @type {{defaults: ConfigInternal, morph: Morph}}\n */\nvar Idiomorph = (function () {\n \"use strict\";\n\n /**\n * @typedef {object} MorphContext\n *\n * @property {Element} target\n * @property {Element} newContent\n * @property {ConfigInternal} config\n * @property {ConfigInternal['morphStyle']} morphStyle\n * @property {ConfigInternal['ignoreActive']} ignoreActive\n * @property {ConfigInternal['ignoreActiveValue']} ignoreActiveValue\n * @property {ConfigInternal['restoreFocus']} restoreFocus\n * @property {Map>} idMap\n * @property {Set} persistentIds\n * @property {ConfigInternal['callbacks']} callbacks\n * @property {ConfigInternal['head']} head\n * @property {HTMLDivElement} pantry\n */\n\n //=============================================================================\n // AND NOW IT BEGINS...\n //=============================================================================\n\n const noOp = () => {};\n /**\n * Default configuration values, updatable by users now\n * @type {ConfigInternal}\n */\n const defaults = {\n morphStyle: \"outerHTML\",\n callbacks: {\n beforeNodeAdded: noOp,\n afterNodeAdded: noOp,\n beforeNodeMorphed: noOp,\n afterNodeMorphed: noOp,\n beforeNodeRemoved: noOp,\n afterNodeRemoved: noOp,\n beforeAttributeUpdated: noOp,\n },\n head: {\n style: \"merge\",\n shouldPreserve: (elt) => elt.getAttribute(\"im-preserve\") === \"true\",\n shouldReAppend: (elt) => elt.getAttribute(\"im-re-append\") === \"true\",\n shouldRemove: noOp,\n afterHeadMorphed: noOp,\n },\n restoreFocus: true,\n };\n\n /**\n * Core idiomorph function for morphing one DOM tree to another\n *\n * @param {Element | Document} oldNode\n * @param {Element | Node | HTMLCollection | Node[] | string | null} newContent\n * @param {Config} [config]\n * @returns {Promise | Node[]}\n */\n function morph(oldNode, newContent, config = {}) {\n oldNode = normalizeElement(oldNode);\n const newNode = normalizeParent(newContent);\n const ctx = createMorphContext(oldNode, newNode, config);\n\n const morphedNodes = saveAndRestoreFocus(ctx, () => {\n return withHeadBlocking(\n ctx,\n oldNode,\n newNode,\n /** @param {MorphContext} ctx */ (ctx) => {\n if (ctx.morphStyle === \"innerHTML\") {\n morphChildren(ctx, oldNode, newNode);\n return Array.from(oldNode.childNodes);\n } else {\n return morphOuterHTML(ctx, oldNode, newNode);\n }\n },\n );\n });\n\n ctx.pantry.remove();\n return morphedNodes;\n }\n\n /**\n * Morph just the outerHTML of the oldNode to the newContent\n * We have to be careful because the oldNode could have siblings which need to be untouched\n * @param {MorphContext} ctx\n * @param {Element} oldNode\n * @param {Element} newNode\n * @returns {Node[]}\n */\n function morphOuterHTML(ctx, oldNode, newNode) {\n const oldParent = normalizeParent(oldNode);\n\n // basis for calulating which nodes were morphed\n // since there may be unmorphed sibling nodes\n let childNodes = Array.from(oldParent.childNodes);\n const index = childNodes.indexOf(oldNode);\n // how many elements are to the right of the oldNode\n const rightMargin = childNodes.length - (index + 1);\n\n morphChildren(\n ctx,\n oldParent,\n newNode,\n // these two optional params are the secret sauce\n oldNode, // start point for iteration\n oldNode.nextSibling, // end point for iteration\n );\n\n // return just the morphed nodes\n childNodes = Array.from(oldParent.childNodes);\n return childNodes.slice(index, childNodes.length - rightMargin);\n }\n\n /**\n * @param {MorphContext} ctx\n * @param {Function} fn\n * @returns {Promise | Node[]}\n */\n function saveAndRestoreFocus(ctx, fn) {\n if (!ctx.config.restoreFocus) return fn();\n let activeElement =\n /** @type {HTMLInputElement|HTMLTextAreaElement|null} */ (\n document.activeElement\n );\n\n // don't bother if the active element is not an input or textarea\n if (\n !(\n activeElement instanceof HTMLInputElement ||\n activeElement instanceof HTMLTextAreaElement\n )\n ) {\n return fn();\n }\n\n const { id: activeElementId, selectionStart, selectionEnd } = activeElement;\n\n const results = fn();\n\n if (activeElementId && activeElementId !== document.activeElement?.id) {\n activeElement = ctx.target.querySelector(`#${activeElementId}`);\n activeElement?.focus();\n }\n if (activeElement && !activeElement.selectionEnd && selectionEnd) {\n activeElement.setSelectionRange(selectionStart, selectionEnd);\n }\n\n return results;\n }\n\n const morphChildren = (function () {\n /**\n * This is the core algorithm for matching up children. The idea is to use id sets to try to match up\n * nodes as faithfully as possible. We greedily match, which allows us to keep the algorithm fast, but\n * by using id sets, we are able to better match up with content deeper in the DOM.\n *\n * Basic algorithm:\n * - for each node in the new content:\n * - search self and siblings for an id set match, falling back to a soft match\n * - if match found\n * - remove any nodes up to the match:\n * - pantry persistent nodes\n * - delete the rest\n * - morph the match\n * - elsif no match found, and node is persistent\n * - find its match by querying the old root (future) and pantry (past)\n * - move it and its children here\n * - morph it\n * - else\n * - create a new node from scratch as a last result\n *\n * @param {MorphContext} ctx the merge context\n * @param {Element} oldParent the old content that we are merging the new content into\n * @param {Element} newParent the parent element of the new content\n * @param {Node|null} [insertionPoint] the point in the DOM we start morphing at (defaults to first child)\n * @param {Node|null} [endPoint] the point in the DOM we stop morphing at (defaults to after last child)\n */\n function morphChildren(\n ctx,\n oldParent,\n newParent,\n insertionPoint = null,\n endPoint = null,\n ) {\n // normalize\n if (\n oldParent instanceof HTMLTemplateElement &&\n newParent instanceof HTMLTemplateElement\n ) {\n // @ts-ignore we can pretend the DocumentFragment is an Element\n oldParent = oldParent.content;\n // @ts-ignore ditto\n newParent = newParent.content;\n }\n insertionPoint ||= oldParent.firstChild;\n\n // run through all the new content\n for (const newChild of newParent.childNodes) {\n // once we reach the end of the old parent content skip to the end and insert the rest\n if (insertionPoint && insertionPoint != endPoint) {\n const bestMatch = findBestMatch(\n ctx,\n newChild,\n insertionPoint,\n endPoint,\n );\n if (bestMatch) {\n // if the node to morph is not at the insertion point then remove/move up to it\n if (bestMatch !== insertionPoint) {\n removeNodesBetween(ctx, insertionPoint, bestMatch);\n }\n morphNode(bestMatch, newChild, ctx);\n insertionPoint = bestMatch.nextSibling;\n continue;\n }\n }\n\n // if the matching node is elsewhere in the original content\n if (newChild instanceof Element && ctx.persistentIds.has(newChild.id)) {\n // move it and all its children here and morph\n const movedChild = moveBeforeById(\n oldParent,\n newChild.id,\n insertionPoint,\n ctx,\n );\n morphNode(movedChild, newChild, ctx);\n insertionPoint = movedChild.nextSibling;\n continue;\n }\n\n // last resort: insert the new node from scratch\n const insertedNode = createNode(\n oldParent,\n newChild,\n insertionPoint,\n ctx,\n );\n // could be null if beforeNodeAdded prevented insertion\n if (insertedNode) {\n insertionPoint = insertedNode.nextSibling;\n }\n }\n\n // remove any remaining old nodes that didn't match up with new content\n while (insertionPoint && insertionPoint != endPoint) {\n const tempNode = insertionPoint;\n insertionPoint = insertionPoint.nextSibling;\n removeNode(ctx, tempNode);\n }\n }\n\n /**\n * This performs the action of inserting a new node while handling situations where the node contains\n * elements with persistent ids and possible state info we can still preserve by moving in and then morphing\n *\n * @param {Element} oldParent\n * @param {Node} newChild\n * @param {Node|null} insertionPoint\n * @param {MorphContext} ctx\n * @returns {Node|null}\n */\n function createNode(oldParent, newChild, insertionPoint, ctx) {\n if (ctx.callbacks.beforeNodeAdded(newChild) === false) return null;\n if (ctx.idMap.has(newChild)) {\n // node has children with ids with possible state so create a dummy elt of same type and apply full morph algorithm\n const newEmptyChild = document.createElement(\n /** @type {Element} */ (newChild).tagName,\n );\n oldParent.insertBefore(newEmptyChild, insertionPoint);\n morphNode(newEmptyChild, newChild, ctx);\n ctx.callbacks.afterNodeAdded(newEmptyChild);\n return newEmptyChild;\n } else {\n // optimisation: no id state to preserve so we can just insert a clone of the newChild and its descendants\n const newClonedChild = document.importNode(newChild, true); // importNode to not mutate newParent\n oldParent.insertBefore(newClonedChild, insertionPoint);\n ctx.callbacks.afterNodeAdded(newClonedChild);\n return newClonedChild;\n }\n }\n\n //=============================================================================\n // Matching Functions\n //=============================================================================\n const findBestMatch = (function () {\n /**\n * Scans forward from the startPoint to the endPoint looking for a match\n * for the node. It looks for an id set match first, then a soft match.\n * We abort softmatching if we find two future soft matches, to reduce churn.\n * @param {Node} node\n * @param {MorphContext} ctx\n * @param {Node | null} startPoint\n * @param {Node | null} endPoint\n * @returns {Node | null}\n */\n function findBestMatch(ctx, node, startPoint, endPoint) {\n let softMatch = null;\n let nextSibling = node.nextSibling;\n let siblingSoftMatchCount = 0;\n\n let cursor = startPoint;\n while (cursor && cursor != endPoint) {\n // soft matching is a prerequisite for id set matching\n if (isSoftMatch(cursor, node)) {\n if (isIdSetMatch(ctx, cursor, node)) {\n return cursor; // found an id set match, we're done!\n }\n\n // we haven't yet saved a soft match fallback\n if (softMatch === null) {\n // the current soft match will hard match something else in the future, leave it\n if (!ctx.idMap.has(cursor)) {\n // save this as the fallback if we get through the loop without finding a hard match\n softMatch = cursor;\n }\n }\n }\n if (\n softMatch === null &&\n nextSibling &&\n isSoftMatch(cursor, nextSibling)\n ) {\n // The next new node has a soft match with this node, so\n // increment the count of future soft matches\n siblingSoftMatchCount++;\n nextSibling = nextSibling.nextSibling;\n\n // If there are two future soft matches, block soft matching for this node to allow\n // future siblings to soft match. This is to reduce churn in the DOM when an element\n // is prepended.\n if (siblingSoftMatchCount >= 2) {\n softMatch = undefined;\n }\n }\n\n // if the current node contains active element, stop looking for better future matches,\n // because if one is found, this node will be moved to the pantry, reparenting it and thus losing focus\n if (cursor.contains(document.activeElement)) break;\n\n cursor = cursor.nextSibling;\n }\n\n return softMatch || null;\n }\n\n /**\n *\n * @param {MorphContext} ctx\n * @param {Node} oldNode\n * @param {Node} newNode\n * @returns {boolean}\n */\n function isIdSetMatch(ctx, oldNode, newNode) {\n let oldSet = ctx.idMap.get(oldNode);\n let newSet = ctx.idMap.get(newNode);\n\n if (!newSet || !oldSet) return false;\n\n for (const id of oldSet) {\n // a potential match is an id in the new and old nodes that\n // has not already been merged into the DOM\n // But the newNode content we call this on has not been\n // merged yet and we don't allow duplicate IDs so it is simple\n if (newSet.has(id)) {\n return true;\n }\n }\n return false;\n }\n\n /**\n *\n * @param {Node} oldNode\n * @param {Node} newNode\n * @returns {boolean}\n */\n function isSoftMatch(oldNode, newNode) {\n // ok to cast: if one is not element, `id` and `tagName` will be undefined and we'll just compare that.\n const oldElt = /** @type {Element} */ (oldNode);\n const newElt = /** @type {Element} */ (newNode);\n\n return (\n oldElt.nodeType === newElt.nodeType &&\n oldElt.tagName === newElt.tagName &&\n // If oldElt has an `id` with possible state and it doesn't match newElt.id then avoid morphing.\n // We'll still match an anonymous node with an IDed newElt, though, because if it got this far,\n // its not persistent, and new nodes can't have any hidden state.\n (!oldElt.id || oldElt.id === newElt.id)\n );\n }\n\n return findBestMatch;\n })();\n\n //=============================================================================\n // DOM Manipulation Functions\n //=============================================================================\n\n /**\n * Gets rid of an unwanted DOM node; strategy depends on nature of its reuse:\n * - Persistent nodes will be moved to the pantry for later reuse\n * - Other nodes will have their hooks called, and then are removed\n * @param {MorphContext} ctx\n * @param {Node} node\n */\n function removeNode(ctx, node) {\n // are we going to id set match this later?\n if (ctx.idMap.has(node)) {\n // skip callbacks and move to pantry\n moveBefore(ctx.pantry, node, null);\n } else {\n // remove for realsies\n if (ctx.callbacks.beforeNodeRemoved(node) === false) return;\n node.parentNode?.removeChild(node);\n ctx.callbacks.afterNodeRemoved(node);\n }\n }\n\n /**\n * Remove nodes between the start and end nodes\n * @param {MorphContext} ctx\n * @param {Node} startInclusive\n * @param {Node} endExclusive\n * @returns {Node|null}\n */\n function removeNodesBetween(ctx, startInclusive, endExclusive) {\n /** @type {Node | null} */\n let cursor = startInclusive;\n // remove nodes until the endExclusive node\n while (cursor && cursor !== endExclusive) {\n let tempNode = /** @type {Node} */ (cursor);\n cursor = cursor.nextSibling;\n removeNode(ctx, tempNode);\n }\n return cursor;\n }\n\n /**\n * Search for an element by id within the document and pantry, and move it using moveBefore.\n *\n * @param {Element} parentNode - The parent node to which the element will be moved.\n * @param {string} id - The ID of the element to be moved.\n * @param {Node | null} after - The reference node to insert the element before.\n * If `null`, the element is appended as the last child.\n * @param {MorphContext} ctx\n * @returns {Element} The found element\n */\n function moveBeforeById(parentNode, id, after, ctx) {\n const target =\n /** @type {Element} - will always be found */\n (\n ctx.target.querySelector(`#${id}`) ||\n ctx.pantry.querySelector(`#${id}`)\n );\n removeElementFromAncestorsIdMaps(target, ctx);\n moveBefore(parentNode, target, after);\n return target;\n }\n\n /**\n * Removes an element from its ancestors' id maps. This is needed when an element is moved from the\n * \"future\" via `moveBeforeId`. Otherwise, its erstwhile ancestors could be mistakenly moved to the\n * pantry rather than being deleted, preventing their removal hooks from being called.\n *\n * @param {Element} element - element to remove from its ancestors' id maps\n * @param {MorphContext} ctx\n */\n function removeElementFromAncestorsIdMaps(element, ctx) {\n const id = element.id;\n /** @ts-ignore - safe to loop in this way **/\n while ((element = element.parentNode)) {\n let idSet = ctx.idMap.get(element);\n if (idSet) {\n idSet.delete(id);\n if (!idSet.size) {\n ctx.idMap.delete(element);\n }\n }\n }\n }\n\n /**\n * Moves an element before another element within the same parent.\n * Uses the proposed `moveBefore` API if available (and working), otherwise falls back to `insertBefore`.\n * This is essentialy a forward-compat wrapper.\n *\n * @param {Element} parentNode - The parent node containing the after element.\n * @param {Node} element - The element to be moved.\n * @param {Node | null} after - The reference node to insert `element` before.\n * If `null`, `element` is appended as the last child.\n */\n function moveBefore(parentNode, element, after) {\n // @ts-ignore - use proposed moveBefore feature\n if (parentNode.moveBefore) {\n try {\n // @ts-ignore - use proposed moveBefore feature\n parentNode.moveBefore(element, after);\n } catch (e) {\n // fall back to insertBefore as some browsers may fail on moveBefore when trying to move Dom disconnected nodes to pantry\n parentNode.insertBefore(element, after);\n }\n } else {\n parentNode.insertBefore(element, after);\n }\n }\n\n return morphChildren;\n })();\n\n //=============================================================================\n // Single Node Morphing Code\n //=============================================================================\n const morphNode = (function () {\n /**\n * @param {Node} oldNode root node to merge content into\n * @param {Node} newContent new content to merge\n * @param {MorphContext} ctx the merge context\n * @returns {Node | null} the element that ended up in the DOM\n */\n function morphNode(oldNode, newContent, ctx) {\n if (ctx.ignoreActive && oldNode === document.activeElement) {\n // don't morph focused element\n return null;\n }\n\n if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) {\n return oldNode;\n }\n\n if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) {\n // ignore the head element\n } else if (\n oldNode instanceof HTMLHeadElement &&\n ctx.head.style !== \"morph\"\n ) {\n // ok to cast: if newContent wasn't also a , it would've got caught in the `!isSoftMatch` branch above\n handleHeadElement(\n oldNode,\n /** @type {HTMLHeadElement} */ (newContent),\n ctx,\n );\n } else {\n morphAttributes(oldNode, newContent, ctx);\n if (!ignoreValueOfActiveElement(oldNode, ctx)) {\n // @ts-ignore newContent can be a node here because .firstChild will be null\n morphChildren(ctx, oldNode, newContent);\n }\n }\n ctx.callbacks.afterNodeMorphed(oldNode, newContent);\n return oldNode;\n }\n\n /**\n * syncs the oldNode to the newNode, copying over all attributes and\n * inner element state from the newNode to the oldNode\n *\n * @param {Node} oldNode the node to copy attributes & state to\n * @param {Node} newNode the node to copy attributes & state from\n * @param {MorphContext} ctx the merge context\n */\n function morphAttributes(oldNode, newNode, ctx) {\n let type = newNode.nodeType;\n\n // if is an element type, sync the attributes from the\n // new node into the new node\n if (type === 1 /* element type */) {\n const oldElt = /** @type {Element} */ (oldNode);\n const newElt = /** @type {Element} */ (newNode);\n\n const oldAttributes = oldElt.attributes;\n const newAttributes = newElt.attributes;\n for (const newAttribute of newAttributes) {\n if (ignoreAttribute(newAttribute.name, oldElt, \"update\", ctx)) {\n continue;\n }\n if (oldElt.getAttribute(newAttribute.name) !== newAttribute.value) {\n oldElt.setAttribute(newAttribute.name, newAttribute.value);\n }\n }\n // iterate backwards to avoid skipping over items when a delete occurs\n for (let i = oldAttributes.length - 1; 0 <= i; i--) {\n const oldAttribute = oldAttributes[i];\n\n // toAttributes is a live NamedNodeMap, so iteration+mutation is unsafe\n // e.g. custom element attribute callbacks can remove other attributes\n if (!oldAttribute) continue;\n\n if (!newElt.hasAttribute(oldAttribute.name)) {\n if (ignoreAttribute(oldAttribute.name, oldElt, \"remove\", ctx)) {\n continue;\n }\n oldElt.removeAttribute(oldAttribute.name);\n }\n }\n\n if (!ignoreValueOfActiveElement(oldElt, ctx)) {\n syncInputValue(oldElt, newElt, ctx);\n }\n }\n\n // sync text nodes\n if (type === 8 /* comment */ || type === 3 /* text */) {\n if (oldNode.nodeValue !== newNode.nodeValue) {\n oldNode.nodeValue = newNode.nodeValue;\n }\n }\n }\n\n /**\n * NB: many bothans died to bring us information:\n *\n * https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js\n * https://github.com/choojs/nanomorph/blob/master/lib/morph.jsL113\n *\n * @param {Element} oldElement the element to sync the input value to\n * @param {Element} newElement the element to sync the input value from\n * @param {MorphContext} ctx the merge context\n */\n function syncInputValue(oldElement, newElement, ctx) {\n if (\n oldElement instanceof HTMLInputElement &&\n newElement instanceof HTMLInputElement &&\n newElement.type !== \"file\"\n ) {\n let newValue = newElement.value;\n let oldValue = oldElement.value;\n\n // sync boolean attributes\n syncBooleanAttribute(oldElement, newElement, \"checked\", ctx);\n syncBooleanAttribute(oldElement, newElement, \"disabled\", ctx);\n\n if (!newElement.hasAttribute(\"value\")) {\n if (!ignoreAttribute(\"value\", oldElement, \"remove\", ctx)) {\n oldElement.value = \"\";\n oldElement.removeAttribute(\"value\");\n }\n } else if (oldValue !== newValue) {\n if (!ignoreAttribute(\"value\", oldElement, \"update\", ctx)) {\n oldElement.setAttribute(\"value\", newValue);\n oldElement.value = newValue;\n }\n }\n // TODO: QUESTION(1cg): this used to only check `newElement` unlike the other branches -- why?\n // did I break something?\n } else if (\n oldElement instanceof HTMLOptionElement &&\n newElement instanceof HTMLOptionElement\n ) {\n syncBooleanAttribute(oldElement, newElement, \"selected\", ctx);\n } else if (\n oldElement instanceof HTMLTextAreaElement &&\n newElement instanceof HTMLTextAreaElement\n ) {\n let newValue = newElement.value;\n let oldValue = oldElement.value;\n if (ignoreAttribute(\"value\", oldElement, \"update\", ctx)) {\n return;\n }\n if (newValue !== oldValue) {\n oldElement.value = newValue;\n }\n if (\n oldElement.firstChild &&\n oldElement.firstChild.nodeValue !== newValue\n ) {\n oldElement.firstChild.nodeValue = newValue;\n }\n }\n }\n\n /**\n * @param {Element} oldElement element to write the value to\n * @param {Element} newElement element to read the value from\n * @param {string} attributeName the attribute name\n * @param {MorphContext} ctx the merge context\n */\n function syncBooleanAttribute(oldElement, newElement, attributeName, ctx) {\n // @ts-ignore this function is only used on boolean attrs that are reflected as dom properties\n const newLiveValue = newElement[attributeName],\n // @ts-ignore ditto\n oldLiveValue = oldElement[attributeName];\n if (newLiveValue !== oldLiveValue) {\n const ignoreUpdate = ignoreAttribute(\n attributeName,\n oldElement,\n \"update\",\n ctx,\n );\n if (!ignoreUpdate) {\n // update attribute's associated DOM property\n // @ts-ignore this function is only used on boolean attrs that are reflected as dom properties\n oldElement[attributeName] = newElement[attributeName];\n }\n if (newLiveValue) {\n if (!ignoreUpdate) {\n // https://developer.mozilla.org/en-US/docs/Glossary/Boolean/HTML\n // this is the correct way to set a boolean attribute to \"true\"\n oldElement.setAttribute(attributeName, \"\");\n }\n } else {\n if (!ignoreAttribute(attributeName, oldElement, \"remove\", ctx)) {\n oldElement.removeAttribute(attributeName);\n }\n }\n }\n }\n\n /**\n * @param {string} attr the attribute to be mutated\n * @param {Element} element the element that is going to be updated\n * @param {\"update\" | \"remove\"} updateType\n * @param {MorphContext} ctx the merge context\n * @returns {boolean} true if the attribute should be ignored, false otherwise\n */\n function ignoreAttribute(attr, element, updateType, ctx) {\n if (\n attr === \"value\" &&\n ctx.ignoreActiveValue &&\n element === document.activeElement\n ) {\n return true;\n }\n return (\n ctx.callbacks.beforeAttributeUpdated(attr, element, updateType) ===\n false\n );\n }\n\n /**\n * @param {Node} possibleActiveElement\n * @param {MorphContext} ctx\n * @returns {boolean}\n */\n function ignoreValueOfActiveElement(possibleActiveElement, ctx) {\n return (\n !!ctx.ignoreActiveValue &&\n possibleActiveElement === document.activeElement &&\n possibleActiveElement !== document.body\n );\n }\n\n return morphNode;\n })();\n\n //=============================================================================\n // Head Management Functions\n //=============================================================================\n /**\n * @param {MorphContext} ctx\n * @param {Element} oldNode\n * @param {Element} newNode\n * @param {function} callback\n * @returns {Node[] | Promise}\n */\n function withHeadBlocking(ctx, oldNode, newNode, callback) {\n if (ctx.head.block) {\n const oldHead = oldNode.querySelector(\"head\");\n const newHead = newNode.querySelector(\"head\");\n if (oldHead && newHead) {\n const promises = handleHeadElement(oldHead, newHead, ctx);\n // when head promises resolve, proceed ignoring the head tag\n return Promise.all(promises).then(() => {\n const newCtx = Object.assign(ctx, {\n head: {\n block: false,\n ignore: true,\n },\n });\n return callback(newCtx);\n });\n }\n }\n // just proceed if we not head blocking\n return callback(ctx);\n }\n\n /**\n * The HEAD tag can be handled specially, either w/ a 'merge' or 'append' style\n *\n * @param {Element} oldHead\n * @param {Element} newHead\n * @param {MorphContext} ctx\n * @returns {Promise[]}\n */\n function handleHeadElement(oldHead, newHead, ctx) {\n let added = [];\n let removed = [];\n let preserved = [];\n let nodesToAppend = [];\n\n // put all new head elements into a Map, by their outerHTML\n let srcToNewHeadNodes = new Map();\n for (const newHeadChild of newHead.children) {\n srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);\n }\n\n // for each elt in the current head\n for (const currentHeadElt of oldHead.children) {\n // If the current head element is in the map\n let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);\n let isReAppended = ctx.head.shouldReAppend(currentHeadElt);\n let isPreserved = ctx.head.shouldPreserve(currentHeadElt);\n if (inNewContent || isPreserved) {\n if (isReAppended) {\n // remove the current version and let the new version replace it and re-execute\n removed.push(currentHeadElt);\n } else {\n // this element already exists and should not be re-appended, so remove it from\n // the new content map, preserving it in the DOM\n srcToNewHeadNodes.delete(currentHeadElt.outerHTML);\n preserved.push(currentHeadElt);\n }\n } else {\n if (ctx.head.style === \"append\") {\n // we are appending and this existing element is not new content\n // so if and only if it is marked for re-append do we do anything\n if (isReAppended) {\n removed.push(currentHeadElt);\n nodesToAppend.push(currentHeadElt);\n }\n } else {\n // if this is a merge, we remove this content since it is not in the new head\n if (ctx.head.shouldRemove(currentHeadElt) !== false) {\n removed.push(currentHeadElt);\n }\n }\n }\n }\n\n // Push the remaining new head elements in the Map into the\n // nodes to append to the head tag\n nodesToAppend.push(...srcToNewHeadNodes.values());\n\n let promises = [];\n for (const newNode of nodesToAppend) {\n // TODO: This could theoretically be null, based on type\n let newElt = /** @type {ChildNode} */ (\n document.createRange().createContextualFragment(newNode.outerHTML)\n .firstChild\n );\n if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {\n if (\n (\"href\" in newElt && newElt.href) ||\n (\"src\" in newElt && newElt.src)\n ) {\n /** @type {(result?: any) => void} */ let resolve;\n let promise = new Promise(function (_resolve) {\n resolve = _resolve;\n });\n newElt.addEventListener(\"load\", function () {\n resolve();\n });\n promises.push(promise);\n }\n oldHead.appendChild(newElt);\n ctx.callbacks.afterNodeAdded(newElt);\n added.push(newElt);\n }\n }\n\n // remove all removed elements, after we have appended the new elements to avoid\n // additional network requests for things like style sheets\n for (const removedElement of removed) {\n if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {\n oldHead.removeChild(removedElement);\n ctx.callbacks.afterNodeRemoved(removedElement);\n }\n }\n\n ctx.head.afterHeadMorphed(oldHead, {\n added: added,\n kept: preserved,\n removed: removed,\n });\n return promises;\n }\n\n //=============================================================================\n // Create Morph Context Functions\n //=============================================================================\n const createMorphContext = (function () {\n /**\n *\n * @param {Element} oldNode\n * @param {Element} newContent\n * @param {Config} config\n * @returns {MorphContext}\n */\n function createMorphContext(oldNode, newContent, config) {\n const { persistentIds, idMap } = createIdMaps(oldNode, newContent);\n\n const mergedConfig = mergeDefaults(config);\n const morphStyle = mergedConfig.morphStyle || \"outerHTML\";\n if (![\"innerHTML\", \"outerHTML\"].includes(morphStyle)) {\n throw `Do not understand how to morph style ${morphStyle}`;\n }\n\n return {\n target: oldNode,\n newContent: newContent,\n config: mergedConfig,\n morphStyle: morphStyle,\n ignoreActive: mergedConfig.ignoreActive,\n ignoreActiveValue: mergedConfig.ignoreActiveValue,\n restoreFocus: mergedConfig.restoreFocus,\n idMap: idMap,\n persistentIds: persistentIds,\n pantry: createPantry(),\n callbacks: mergedConfig.callbacks,\n head: mergedConfig.head,\n };\n }\n\n /**\n * Deep merges the config object and the Idiomorph.defaults object to\n * produce a final configuration object\n * @param {Config} config\n * @returns {ConfigInternal}\n */\n function mergeDefaults(config) {\n let finalConfig = Object.assign({}, defaults);\n\n // copy top level stuff into final config\n Object.assign(finalConfig, config);\n\n // copy callbacks into final config (do this to deep merge the callbacks)\n finalConfig.callbacks = Object.assign(\n {},\n defaults.callbacks,\n config.callbacks,\n );\n\n // copy head config into final config (do this to deep merge the head)\n finalConfig.head = Object.assign({}, defaults.head, config.head);\n\n return finalConfig;\n }\n\n /**\n * @returns {HTMLDivElement}\n */\n function createPantry() {\n const pantry = document.createElement(\"div\");\n pantry.hidden = true;\n document.body.insertAdjacentElement(\"afterend\", pantry);\n return pantry;\n }\n\n /**\n * Returns all elements with an ID contained within the root element and its descendants\n *\n * @param {Element} root\n * @returns {Element[]}\n */\n function findIdElements(root) {\n let elements = Array.from(root.querySelectorAll(\"[id]\"));\n if (root.id) {\n elements.push(root);\n }\n return elements;\n }\n\n /**\n * A bottom-up algorithm that populates a map of Element -> IdSet.\n * The idSet for a given element is the set of all IDs contained within its subtree.\n * As an optimzation, we filter these IDs through the given list of persistent IDs,\n * because we don't need to bother considering IDed elements that won't be in the new content.\n *\n * @param {Map>} idMap\n * @param {Set} persistentIds\n * @param {Element} root\n * @param {Element[]} elements\n */\n function populateIdMapWithTree(idMap, persistentIds, root, elements) {\n for (const elt of elements) {\n if (persistentIds.has(elt.id)) {\n /** @type {Element|null} */\n let current = elt;\n // walk up the parent hierarchy of that element, adding the id\n // of element to the parent's id set\n while (current) {\n let idSet = idMap.get(current);\n // if the id set doesn't exist, create it and insert it in the map\n if (idSet == null) {\n idSet = new Set();\n idMap.set(current, idSet);\n }\n idSet.add(elt.id);\n\n if (current === root) break;\n current = current.parentElement;\n }\n }\n }\n }\n\n /**\n * This function computes a map of nodes to all ids contained within that node (inclusive of the\n * node). This map can be used to ask if two nodes have intersecting sets of ids, which allows\n * for a looser definition of \"matching\" than tradition id matching, and allows child nodes\n * to contribute to a parent nodes matching.\n *\n * @param {Element} oldContent the old content that will be morphed\n * @param {Element} newContent the new content to morph to\n * @returns {IdSets}\n */\n function createIdMaps(oldContent, newContent) {\n const oldIdElements = findIdElements(oldContent);\n const newIdElements = findIdElements(newContent);\n\n const persistentIds = createPersistentIds(oldIdElements, newIdElements);\n\n /** @type {Map>} */\n let idMap = new Map();\n populateIdMapWithTree(idMap, persistentIds, oldContent, oldIdElements);\n\n /** @ts-ignore - if newContent is a duck-typed parent, pass its single child node as the root to halt upwards iteration */\n const newRoot = newContent.__idiomorphRoot || newContent;\n populateIdMapWithTree(idMap, persistentIds, newRoot, newIdElements);\n\n return { persistentIds, idMap };\n }\n\n /**\n * This function computes the set of ids that persist between the two contents excluding duplicates\n *\n * @param {Element[]} oldIdElements\n * @param {Element[]} newIdElements\n * @returns {Set}\n */\n function createPersistentIds(oldIdElements, newIdElements) {\n let duplicateIds = new Set();\n\n /** @type {Map} */\n let oldIdTagNameMap = new Map();\n for (const { id, tagName } of oldIdElements) {\n if (oldIdTagNameMap.has(id)) {\n duplicateIds.add(id);\n } else {\n oldIdTagNameMap.set(id, tagName);\n }\n }\n\n let persistentIds = new Set();\n for (const { id, tagName } of newIdElements) {\n if (persistentIds.has(id)) {\n duplicateIds.add(id);\n } else if (oldIdTagNameMap.get(id) === tagName) {\n persistentIds.add(id);\n }\n // skip if tag types mismatch because its not possible to morph one tag into another\n }\n\n for (const id of duplicateIds) {\n persistentIds.delete(id);\n }\n return persistentIds;\n }\n\n return createMorphContext;\n })();\n\n //=============================================================================\n // HTML Normalization Functions\n //=============================================================================\n const { normalizeElement, normalizeParent } = (function () {\n /** @type {WeakSet} */\n const generatedByIdiomorph = new WeakSet();\n\n /**\n *\n * @param {Element | Document} content\n * @returns {Element}\n */\n function normalizeElement(content) {\n if (content instanceof Document) {\n return content.documentElement;\n } else {\n return content;\n }\n }\n\n /**\n *\n * @param {null | string | Node | HTMLCollection | Node[] | Document & {generatedByIdiomorph:boolean}} newContent\n * @returns {Element}\n */\n function normalizeParent(newContent) {\n if (newContent == null) {\n return document.createElement(\"div\"); // dummy parent element\n } else if (typeof newContent === \"string\") {\n return normalizeParent(parseContent(newContent));\n } else if (\n generatedByIdiomorph.has(/** @type {Element} */ (newContent))\n ) {\n // the template tag created by idiomorph parsing can serve as a dummy parent\n return /** @type {Element} */ (newContent);\n } else if (newContent instanceof Node) {\n if (newContent.parentNode) {\n // we can't use the parent directly because newContent may have siblings\n // that we don't want in the morph, and reparenting might be expensive (TODO is it?),\n // so we create a duck-typed parent node instead.\n return createDuckTypedParent(newContent);\n } else {\n // a single node is added as a child to a dummy parent\n const dummyParent = document.createElement(\"div\");\n dummyParent.append(newContent);\n return dummyParent;\n }\n } else {\n // all nodes in the array or HTMLElement collection are consolidated under\n // a single dummy parent element\n const dummyParent = document.createElement(\"div\");\n for (const elt of [...newContent]) {\n dummyParent.append(elt);\n }\n return dummyParent;\n }\n }\n\n /**\n * Creates a fake duck-typed parent element to wrap a single node, without actually reparenting it.\n * \"If it walks like a duck, and quacks like a duck, then it must be a duck!\" -- James Whitcomb Riley (1849\u20131916)\n *\n * @param {Node} newContent\n * @returns {Element}\n */\n function createDuckTypedParent(newContent) {\n return /** @type {Element} */ (\n /** @type {unknown} */ ({\n childNodes: [newContent],\n /** @ts-ignore - cover your eyes for a minute, tsc */\n querySelectorAll: (s) => {\n /** @ts-ignore */\n const elements = newContent.querySelectorAll(s);\n /** @ts-ignore */\n return newContent.matches(s) ? [newContent, ...elements] : elements;\n },\n /** @ts-ignore */\n insertBefore: (n, r) => newContent.parentNode.insertBefore(n, r),\n /** @ts-ignore */\n moveBefore: (n, r) => newContent.parentNode.moveBefore(n, r),\n // for later use with populateIdMapWithTree to halt upwards iteration\n get __idiomorphRoot() {\n return newContent;\n },\n })\n );\n }\n\n /**\n *\n * @param {string} newContent\n * @returns {Node | null | DocumentFragment}\n */\n function parseContent(newContent) {\n let parser = new DOMParser();\n\n // remove svgs to avoid false-positive matches on head, etc.\n let contentWithSvgsRemoved = newContent.replace(\n /