dalila 1.10.3 → 1.10.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core/html.js CHANGED
@@ -1,8 +1,21 @@
1
1
  // Placeholder tokens injected into raw HTML so that dynamic values can be
2
2
  // located and replaced after the browser parses the markup.
3
- const TOKEN_PREFIX = '__DALILA_SLOT_';
4
3
  const TOKEN_SUFFIX = '__';
5
- const TOKEN_REGEX = /__DALILA_SLOT_(\d+)__/g;
4
+ let tokenNamespaceCounter = 0;
5
+ function escapeRegExp(text) {
6
+ return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7
+ }
8
+ function createTokenSpec(strings) {
9
+ const source = strings.join('');
10
+ let prefix = '';
11
+ do {
12
+ prefix = `__DALILA_SLOT_${(tokenNamespaceCounter++).toString(36)}_`;
13
+ } while (source.includes(prefix));
14
+ return {
15
+ prefix,
16
+ regex: new RegExp(`${escapeRegExp(prefix)}(\\d+)${escapeRegExp(TOKEN_SUFFIX)}`, 'g'),
17
+ };
18
+ }
6
19
  function isNode(value) {
7
20
  return typeof Node !== 'undefined' && value instanceof Node;
8
21
  }
@@ -35,14 +48,15 @@ function toAttributeValue(value) {
35
48
  return String(value);
36
49
  }
37
50
  /** Walk a text node, replacing placeholder tokens with their corresponding values. */
38
- function replaceTextTokens(node, values) {
51
+ function replaceTextTokens(node, values, tokenSpec) {
39
52
  const text = node.nodeValue ?? '';
40
- if (!text.includes(TOKEN_PREFIX))
53
+ if (!text.includes(tokenSpec.prefix))
41
54
  return;
42
55
  const fragment = document.createDocumentFragment();
43
56
  let lastIndex = 0;
44
57
  let match = null;
45
- while ((match = TOKEN_REGEX.exec(text))) {
58
+ tokenSpec.regex.lastIndex = 0;
59
+ while ((match = tokenSpec.regex.exec(text))) {
46
60
  const matchIndex = match.index;
47
61
  const tokenLength = match[0].length;
48
62
  if (matchIndex > lastIndex) {
@@ -69,11 +83,12 @@ function replaceTextTokens(node, values) {
69
83
  * Single-token attributes set to null/undefined/false are removed entirely
70
84
  * (useful for conditional boolean attributes like `disabled`).
71
85
  */
72
- function replaceAttributeTokens(element, values) {
86
+ function replaceAttributeTokens(element, values, tokenSpec) {
73
87
  for (const attr of Array.from(element.attributes)) {
74
- if (!attr.value.includes(TOKEN_PREFIX))
88
+ if (!attr.value.includes(tokenSpec.prefix))
75
89
  continue;
76
- const tokenMatches = Array.from(attr.value.matchAll(TOKEN_REGEX));
90
+ tokenSpec.regex.lastIndex = 0;
91
+ const tokenMatches = Array.from(attr.value.matchAll(tokenSpec.regex));
77
92
  const singleTokenMatch = tokenMatches.length === 1 && attr.value.trim() === tokenMatches[0][0];
78
93
  if (singleTokenMatch) {
79
94
  const value = values[Number(tokenMatches[0][1])];
@@ -82,7 +97,8 @@ function replaceAttributeTokens(element, values) {
82
97
  continue;
83
98
  }
84
99
  }
85
- const nextValue = attr.value.replace(TOKEN_REGEX, (_, index) => {
100
+ tokenSpec.regex.lastIndex = 0;
101
+ const nextValue = attr.value.replace(tokenSpec.regex, (_, index) => {
86
102
  const value = values[Number(index)];
87
103
  return toAttributeValue(value);
88
104
  });
@@ -108,11 +124,12 @@ function replaceAttributeTokens(element, values) {
108
124
  * ```
109
125
  */
110
126
  export function html(strings, ...values) {
127
+ const tokenSpec = createTokenSpec(strings);
111
128
  let markup = '';
112
129
  for (let i = 0; i < strings.length; i += 1) {
113
130
  markup += strings[i];
114
131
  if (i < values.length) {
115
- markup += `${TOKEN_PREFIX}${i}${TOKEN_SUFFIX}`;
132
+ markup += `${tokenSpec.prefix}${i}${TOKEN_SUFFIX}`;
116
133
  }
117
134
  }
118
135
  const template = document.createElement('template');
@@ -125,11 +142,11 @@ export function html(strings, ...values) {
125
142
  }
126
143
  for (const node of nodes) {
127
144
  if (node.nodeType === Node.TEXT_NODE) {
128
- replaceTextTokens(node, values);
145
+ replaceTextTokens(node, values, tokenSpec);
129
146
  continue;
130
147
  }
131
148
  if (node.nodeType === Node.ELEMENT_NODE) {
132
- replaceAttributeTokens(node, values);
149
+ replaceAttributeTokens(node, values, tokenSpec);
133
150
  }
134
151
  }
135
152
  return fragment;
@@ -492,6 +492,9 @@ function escapeInlineScriptContent(script) {
492
492
  }
493
493
  });
494
494
  }
495
+ function stringifyInlineScriptLiteral(value) {
496
+ return escapeInlineScriptContent(JSON.stringify(value));
497
+ }
495
498
  /**
496
499
  * Generate a minimal inline script to prevent FOUC.
497
500
  *
@@ -499,13 +502,14 @@ function escapeInlineScriptContent(script) {
499
502
  */
500
503
  export function createPreloadScript(options) {
501
504
  const { storageKey, defaultValue, target = 'documentElement', attribute = 'data-theme', storageType = 'localStorage', } = options;
502
- // Use JSON.stringify to safely embed strings (avoid breaking quotes / injection)
503
- const k = JSON.stringify(storageKey);
504
- const d = JSON.stringify(defaultValue);
505
- const a = JSON.stringify(attribute);
505
+ const safeTarget = target === 'body' ? 'body' : 'documentElement';
506
+ const safeStorageType = storageType === 'sessionStorage' ? 'sessionStorage' : 'localStorage';
507
+ const k = stringifyInlineScriptLiteral(storageKey);
508
+ const d = stringifyInlineScriptLiteral(defaultValue);
509
+ const a = stringifyInlineScriptLiteral(attribute);
506
510
  // Still minified
507
- const script = `(function(){try{var s=${storageType}.getItem(${k});var v=s==null?${d}:JSON.parse(s);document.${target}.setAttribute(${a},v)}catch(e){document.${target}.setAttribute(${a},${d})}})();`;
508
- return escapeInlineScriptContent(script);
511
+ const script = `(function(){try{var s=${safeStorageType}.getItem(${k});var v=s==null?${d}:JSON.parse(s);document.${safeTarget}.setAttribute(${a},v)}catch(e){document.${safeTarget}.setAttribute(${a},${d})}})();`;
512
+ return script;
509
513
  }
510
514
  export function createThemeScript(storageKey, defaultTheme = 'light') {
511
515
  return createPreloadScript({
@@ -28,6 +28,7 @@ export declare function setEffectErrorHandler(handler: EffectErrorHandler | null
28
28
  * User-provided handlers registered via `setEffectErrorHandler()` always win.
29
29
  */
30
30
  export declare function setDefaultEffectErrorHandler(handler: EffectErrorHandler | null): void;
31
+ export declare const WRITABLE_SIGNAL_MARKER: unique symbol;
31
32
  export interface Signal<T> {
32
33
  /** Read the current value (with dependency tracking if inside an effect). */
33
34
  (): T;
@@ -39,6 +40,8 @@ export interface Signal<T> {
39
40
  peek(): T;
40
41
  /** Subscribe to value changes manually (outside of effects). Returns unsubscribe function. */
41
42
  on(callback: (value: T) => void): () => void;
43
+ /** Internal nominal marker used by the runtime to detect writable signals quickly. */
44
+ readonly [WRITABLE_SIGNAL_MARKER]?: true;
42
45
  }
43
46
  export interface ReadonlySignal<T> {
44
47
  /** Read the current value (with dependency tracking if inside an effect). */
@@ -47,6 +50,8 @@ export interface ReadonlySignal<T> {
47
50
  peek(): T;
48
51
  /** Subscribe to value changes manually (outside of effects). Returns unsubscribe function. */
49
52
  on(callback: (value: T) => void): () => void;
53
+ /** Internal nominal marker used by the runtime to detect signal mutability quickly. */
54
+ readonly [WRITABLE_SIGNAL_MARKER]?: boolean;
50
55
  }
51
56
  export interface ComputedSignal<T> extends ReadonlySignal<T> {
52
57
  /** Nominal marker to improve IntelliSense/readability in docs and types. */
@@ -58,6 +58,7 @@ function reportEffectError(error, source) {
58
58
  }
59
59
  reportEffectErrorWithHandlers(err, source);
60
60
  }
61
+ export const WRITABLE_SIGNAL_MARKER = Symbol('dalila.writable-signal');
61
62
  /**
62
63
  * Currently executing effect for dependency collection.
63
64
  * Any signal read while this is set subscribes this effect.
@@ -220,6 +221,12 @@ export function signal(initialValue) {
220
221
  subscriber.deps?.delete(subscribers);
221
222
  };
222
223
  };
224
+ Object.defineProperty(read, WRITABLE_SIGNAL_MARKER, {
225
+ value: true,
226
+ configurable: false,
227
+ enumerable: false,
228
+ writable: false,
229
+ });
223
230
  signalRef = read;
224
231
  registerSignal(signalRef, 'signal', {
225
232
  scopeRef: owningScope,
@@ -257,6 +264,12 @@ export function readonly(source) {
257
264
  enumerable: false,
258
265
  writable: false,
259
266
  });
267
+ Object.defineProperty(read, WRITABLE_SIGNAL_MARKER, {
268
+ value: false,
269
+ configurable: false,
270
+ enumerable: false,
271
+ writable: false,
272
+ });
260
273
  return read;
261
274
  }
262
275
  function assertTimingOptions(waitMs, leading, trailing, kind) {
@@ -452,8 +465,6 @@ export function computed(fn) {
452
465
  let dirty = true;
453
466
  const subscribers = new Set();
454
467
  let signalRef;
455
- // Dep sets that `markDirty` is currently registered in (so we can unsubscribe on recompute).
456
- let trackedDeps = new Set();
457
468
  /**
458
469
  * Internal invalidator.
459
470
  * Runs synchronously when any dependency changes.
@@ -467,20 +478,11 @@ export function computed(fn) {
467
478
  });
468
479
  markDirty.disposed = false;
469
480
  markDirty.sync = true;
470
- const cleanupDeps = () => {
471
- for (const depSet of trackedDeps) {
472
- depSet.delete(markDirty);
473
- untrackDependencyBySet(depSet, markDirty);
474
- }
475
- trackedDeps.clear();
476
- if (markDirty.deps)
477
- markDirty.deps.clear();
478
- };
479
481
  const recomputeIfDirty = () => {
480
482
  if (!dirty)
481
483
  return;
482
484
  trackComputedRunStart(signalRef);
483
- cleanupDeps();
485
+ cleanupEffectDeps(markDirty);
484
486
  const prevEffect = activeEffect;
485
487
  const prevScope = activeScope;
486
488
  // Collect deps into markDirty.
@@ -489,8 +491,6 @@ export function computed(fn) {
489
491
  try {
490
492
  value = fn();
491
493
  dirty = false;
492
- if (markDirty.deps)
493
- trackedDeps = new Set(markDirty.deps);
494
494
  }
495
495
  finally {
496
496
  trackComputedRunEnd(signalRef);
@@ -534,6 +534,12 @@ export function computed(fn) {
534
534
  subscriber.deps?.delete(subscribers);
535
535
  };
536
536
  };
537
+ Object.defineProperty(read, WRITABLE_SIGNAL_MARKER, {
538
+ value: false,
539
+ configurable: false,
540
+ enumerable: false,
541
+ writable: false,
542
+ });
537
543
  signalRef = read;
538
544
  registerSignal(signalRef, 'computed', {
539
545
  scopeRef: owningScope,
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { isInDevMode } from '../core/dev.js';
10
10
  import { createScope, getCurrentScope, withScope } from '../core/scope.js';
11
- import { effect, FatalEffectError, signal } from '../core/signal.js';
11
+ import { effect, FatalEffectError, signal, WRITABLE_SIGNAL_MARKER, } from '../core/signal.js';
12
12
  import { withSchedulerPriority } from '../core/scheduler.js';
13
13
  import { WRAPPED_HANDLER } from '../form/form.js';
14
14
  import { linkScopeToDom, withDevtoolsDomTarget } from '../core/devtools.js';
@@ -34,8 +34,14 @@ function isSignal(value) {
34
34
  function isWritableSignal(value) {
35
35
  if (!isSignal(value))
36
36
  return false;
37
+ const marker = value[WRITABLE_SIGNAL_MARKER];
38
+ if (marker === true)
39
+ return true;
40
+ if (marker === false)
41
+ return false;
37
42
  // `computed()` exposes set/update that always throw. Probe with a no-op write
38
43
  // (same value) to detect read-only signals without mutating state.
44
+ // Keep the fallback for signal-like values that do not carry Dalila's marker.
39
45
  try {
40
46
  const current = value.peek();
41
47
  value.set(current);
@@ -105,6 +111,19 @@ function isWarnAsErrorEnabledForActiveScope() {
105
111
  }
106
112
  const RAW_BLOCK_SELECTORS = '[d-pre], [d-raw], d-pre, d-raw, [data-dalila-raw]';
107
113
  const RAW_BLOCK_STYLE_ID = 'dalila-raw-block-default-styles';
114
+ const INTERNAL_BOUND_SELECTOR = '[data-dalila-internal-bound]';
115
+ function createNearestBoundaryResolver() {
116
+ const cache = new WeakMap();
117
+ return (el) => {
118
+ if (!el)
119
+ return null;
120
+ if (cache.has(el))
121
+ return cache.get(el) ?? null;
122
+ const nearest = el.closest(INTERNAL_BOUND_SELECTOR);
123
+ cache.set(el, nearest);
124
+ return nearest;
125
+ };
126
+ }
108
127
  function ensureRawBlockDefaultStyles() {
109
128
  if (typeof document === 'undefined')
110
129
  return;
@@ -132,14 +151,15 @@ function elementDepth(el) {
132
151
  return depth;
133
152
  }
134
153
  function collectRawBlocks(root) {
135
- const boundary = root.closest('[data-dalila-internal-bound]');
154
+ const resolveBoundary = createNearestBoundaryResolver();
155
+ const boundary = resolveBoundary(root);
136
156
  const matches = [];
137
157
  if (root.matches(RAW_BLOCK_SELECTORS)) {
138
158
  matches.push(root);
139
159
  }
140
160
  matches.push(...Array.from(root.querySelectorAll(RAW_BLOCK_SELECTORS)));
141
161
  const inScope = matches.filter((el) => {
142
- const bound = el.closest('[data-dalila-internal-bound]');
162
+ const bound = resolveBoundary(el);
143
163
  return bound === boundary;
144
164
  });
145
165
  inScope.sort((a, b) => elementDepth(a) - elementDepth(b));
@@ -171,7 +191,8 @@ function isInsideRawBlock(el, boundary) {
171
191
  return rawRoot.closest('[data-dalila-internal-bound]') === boundary;
172
192
  }
173
193
  function createBindScanPlan(root) {
174
- const boundary = root.closest('[data-dalila-internal-bound]');
194
+ const resolveBoundary = createNearestBoundaryResolver();
195
+ const boundary = resolveBoundary(root);
175
196
  const elements = [];
176
197
  const attrIndex = new Map();
177
198
  const tagIndex = new Map();
@@ -191,16 +212,13 @@ function createBindScanPlan(root) {
191
212
  }
192
213
  };
193
214
  if (root.matches('*')) {
194
- const rootBoundary = root.closest('[data-dalila-internal-bound]');
195
- if (rootBoundary === boundary) {
196
- elements.push(root);
197
- indexElement(root);
198
- }
215
+ elements.push(root);
216
+ indexElement(root);
199
217
  }
200
218
  const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
201
219
  while (walker.nextNode()) {
202
220
  const el = walker.currentNode;
203
- const nearestBound = el.closest('[data-dalila-internal-bound]');
221
+ const nearestBound = resolveBoundary(el);
204
222
  if (nearestBound !== boundary)
205
223
  continue;
206
224
  elements.push(el);
@@ -208,6 +226,7 @@ function createBindScanPlan(root) {
208
226
  }
209
227
  return {
210
228
  root,
229
+ boundary,
211
230
  elements,
212
231
  attrIndex,
213
232
  tagIndex,
@@ -275,19 +294,18 @@ function resolveSelectorFromIndex(plan, selector) {
275
294
  return null;
276
295
  }
277
296
  function qsaFromPlan(plan, selector) {
278
- const boundary = plan.root.closest('[data-dalila-internal-bound]');
279
297
  const cacheable = !selector.includes('[');
280
298
  if (cacheable) {
281
299
  const cached = plan.selectorCache.get(selector);
282
300
  if (cached) {
283
301
  return cached.filter((el) => (el === plan.root || plan.root.contains(el))
284
- && !isInsideRawBlock(el, boundary));
302
+ && !isInsideRawBlock(el, plan.boundary));
285
303
  }
286
304
  }
287
305
  const indexed = resolveSelectorFromIndex(plan, selector);
288
306
  const source = indexed ?? plan.elements.filter(el => el.matches(selector));
289
307
  const matches = source.filter((el) => (el === plan.root || plan.root.contains(el))
290
- && !isInsideRawBlock(el, boundary));
308
+ && !isInsideRawBlock(el, plan.boundary));
291
309
  if (cacheable)
292
310
  plan.selectorCache.set(selector, matches);
293
311
  return matches;
@@ -306,9 +324,9 @@ function qsaIncludingRoot(root, selector) {
306
324
  // scope; anything deeper was already bound by a nested bind() call.
307
325
  // This also handles manual bind() calls on elements inside a clone:
308
326
  // root won't have the marker, but root.closest() will find the clone.
309
- const boundary = root.closest('[data-dalila-internal-bound]');
327
+ const boundary = root.closest(INTERNAL_BOUND_SELECTOR);
310
328
  return out.filter(el => {
311
- const bound = el.closest('[data-dalila-internal-bound]');
329
+ const bound = el.closest(INTERNAL_BOUND_SELECTOR);
312
330
  if (bound !== boundary)
313
331
  return false;
314
332
  return !isInsideRawBlock(el, boundary);
@@ -1552,7 +1570,8 @@ function createInterpolationTemplatePlan(root, rawTextSelectors) {
1552
1570
  const bindings = [];
1553
1571
  let totalExpressions = 0;
1554
1572
  let fastPathExpressions = 0;
1555
- const textBoundary = root.closest('[data-dalila-internal-bound]');
1573
+ const resolveBoundary = createNearestBoundaryResolver();
1574
+ const textBoundary = resolveBoundary(root);
1556
1575
  while (walker.nextNode()) {
1557
1576
  const node = walker.currentNode;
1558
1577
  const parent = node.parentElement;
@@ -1561,7 +1580,7 @@ function createInterpolationTemplatePlan(root, rawTextSelectors) {
1561
1580
  if (parent && parent.closest(RAW_BLOCK_SELECTORS))
1562
1581
  continue;
1563
1582
  if (parent) {
1564
- const bound = parent.closest('[data-dalila-internal-bound]');
1583
+ const bound = resolveBoundary(parent);
1565
1584
  if (bound !== textBoundary)
1566
1585
  continue;
1567
1586
  }
@@ -15,9 +15,12 @@ export function fromHtml(html, options = {}) {
15
15
  const resolvedSecurity = resolveConfiguredRuntimeSecurityOptions(security);
16
16
  const template = document.createElement('template');
17
17
  setTemplateInnerHTML(template, html, resolvedSecurity);
18
- const container = document.createElement('div');
19
- container.style.display = 'contents';
20
- container.appendChild(template.content);
18
+ const singleRoot = resolveSingleRootElement(template.content);
19
+ const container = singleRoot ?? document.createElement('div');
20
+ if (!singleRoot) {
21
+ container.style.display = 'contents';
22
+ container.appendChild(template.content);
23
+ }
21
24
  // Bind BEFORE inserting children so the layout's bind() only processes
22
25
  // the layout's own HTML — children are already bound by their own fromHtml() call.
23
26
  const dispose = bind(container, data ?? {}, { _internal: true, sanitizeHtml, security });
@@ -37,3 +40,17 @@ export function fromHtml(html, options = {}) {
37
40
  }
38
41
  return container;
39
42
  }
43
+ function resolveSingleRootElement(fragment) {
44
+ let root = null;
45
+ for (const node of Array.from(fragment.childNodes)) {
46
+ if (node.nodeType === Node.TEXT_NODE && !(node.textContent ?? '').trim()) {
47
+ continue;
48
+ }
49
+ if (node.nodeType === Node.ELEMENT_NODE && node instanceof HTMLElement && !root) {
50
+ root = node;
51
+ continue;
52
+ }
53
+ return null;
54
+ }
55
+ return root;
56
+ }
@@ -1,6 +1,5 @@
1
1
  const TRUSTED_POLICY_CACHE_KEY = Symbol.for('dalila.runtime.trustedTypesPolicies');
2
2
  const TRUSTED_POLICY_PARSE_SUFFIX = '--dalila-parse';
3
- const EXECUTABLE_HTML_EVENT_ATTR_PATTERN = /<[^>]+\son[a-z0-9:_-]+\s*=/i;
4
3
  const EXECUTABLE_HTML_URL_ATTR_NAMES = new Set([
5
4
  'href',
6
5
  'src',
@@ -9,7 +8,6 @@ const EXECUTABLE_HTML_URL_ATTR_NAMES = new Set([
9
8
  'action',
10
9
  'poster',
11
10
  ]);
12
- const EXECUTABLE_DATA_URL_PATTERN = /^data:(?:text\/html|application\/xhtml\+xml|image\/svg\+xml)\b/i;
13
11
  function getTrustedPolicyCache() {
14
12
  const host = globalThis;
15
13
  if (host[TRUSTED_POLICY_CACHE_KEY] instanceof Map) {
@@ -42,7 +40,7 @@ function isHtmlAttributeNameChar(code) {
42
40
  && code !== 0x60;
43
41
  }
44
42
  function isTagBoundaryChar(char) {
45
- return !char || /[\s/>]/.test(char);
43
+ return !char || char === '/' || char === '>' || isHtmlWhitespaceCode(char.charCodeAt(0));
46
44
  }
47
45
  function getPreviousNonWhitespaceChar(value, end, start = 0) {
48
46
  for (let index = end - 1; index >= start; index -= 1) {
@@ -80,9 +78,101 @@ function hasExecutableHtmlScriptTag(value) {
80
78
  return false;
81
79
  }
82
80
  function hasExecutableProtocol(value) {
83
- return value.startsWith('javascript:')
84
- || value.startsWith('vbscript:')
85
- || EXECUTABLE_DATA_URL_PATTERN.test(value);
81
+ if (value.startsWith('javascript:') || value.startsWith('vbscript:')) {
82
+ return true;
83
+ }
84
+ if (!value.startsWith('data:')) {
85
+ return false;
86
+ }
87
+ return hasExecutableDataProtocol(value);
88
+ }
89
+ function hasExecutableDataProtocol(value) {
90
+ if (!value.startsWith('data:'))
91
+ return false;
92
+ const metadataStart = 'data:'.length;
93
+ const metadataEnd = value.indexOf(',', metadataStart);
94
+ const metadata = (metadataEnd === -1 ? value.slice(metadataStart) : value.slice(metadataStart, metadataEnd))
95
+ .toLowerCase();
96
+ const mediaType = metadata.split(';', 1)[0].trim();
97
+ return mediaType === 'text/html'
98
+ || mediaType === 'application/xhtml+xml'
99
+ || mediaType === 'image/svg+xml';
100
+ }
101
+ function hasExecutableHtmlEventAttribute(value) {
102
+ let index = 0;
103
+ while (index < value.length) {
104
+ const tagStart = value.indexOf('<', index);
105
+ if (tagStart === -1)
106
+ return false;
107
+ let cursor = tagStart + 1;
108
+ const firstCode = value.charCodeAt(cursor);
109
+ if (Number.isNaN(firstCode)
110
+ || value[cursor] === '/'
111
+ || value[cursor] === '!'
112
+ || value[cursor] === '?') {
113
+ index = cursor;
114
+ continue;
115
+ }
116
+ while (cursor < value.length
117
+ && !isHtmlWhitespaceCode(value.charCodeAt(cursor))
118
+ && value[cursor] !== '>') {
119
+ cursor += 1;
120
+ }
121
+ while (cursor < value.length && value[cursor] !== '>') {
122
+ while (cursor < value.length && isHtmlWhitespaceCode(value.charCodeAt(cursor))) {
123
+ cursor += 1;
124
+ }
125
+ if (cursor >= value.length || value[cursor] === '>') {
126
+ break;
127
+ }
128
+ if (value[cursor] === '/') {
129
+ cursor += 1;
130
+ continue;
131
+ }
132
+ const nameStart = cursor;
133
+ while (cursor < value.length && isHtmlAttributeNameChar(value.charCodeAt(cursor))) {
134
+ cursor += 1;
135
+ }
136
+ if (cursor === nameStart) {
137
+ cursor += 1;
138
+ continue;
139
+ }
140
+ const attrName = value.slice(nameStart, cursor).toLowerCase();
141
+ while (cursor < value.length && isHtmlWhitespaceCode(value.charCodeAt(cursor))) {
142
+ cursor += 1;
143
+ }
144
+ if (value[cursor] !== '=') {
145
+ continue;
146
+ }
147
+ if (attrName.startsWith('on')) {
148
+ return true;
149
+ }
150
+ cursor += 1;
151
+ while (cursor < value.length && isHtmlWhitespaceCode(value.charCodeAt(cursor))) {
152
+ cursor += 1;
153
+ }
154
+ if (cursor >= value.length) {
155
+ break;
156
+ }
157
+ const quote = value[cursor];
158
+ if (quote === '"' || quote === '\'') {
159
+ cursor += 1;
160
+ const closingQuoteIndex = value.indexOf(quote, cursor);
161
+ if (closingQuoteIndex === -1) {
162
+ break;
163
+ }
164
+ cursor = closingQuoteIndex + 1;
165
+ continue;
166
+ }
167
+ while (cursor < value.length
168
+ && !isHtmlWhitespaceCode(value.charCodeAt(cursor))
169
+ && value[cursor] !== '>') {
170
+ cursor += 1;
171
+ }
172
+ }
173
+ index = cursor + 1;
174
+ }
175
+ return false;
86
176
  }
87
177
  function hasExecutableHtmlUrlAttribute(value) {
88
178
  let index = 0;
@@ -194,7 +284,7 @@ export function hasExecutableHtmlSinkPattern(value) {
194
284
  if (!value)
195
285
  return false;
196
286
  return hasExecutableHtmlScriptTag(value)
197
- || EXECUTABLE_HTML_EVENT_ATTR_PATTERN.test(value)
287
+ || hasExecutableHtmlEventAttribute(value)
198
288
  || hasExecutableHtmlUrlAttribute(value);
199
289
  }
200
290
  function getTrustedTypesApi() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dalila",
3
- "version": "1.10.3",
3
+ "version": "1.10.5",
4
4
  "description": "DOM-first reactive framework based on signals",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -116,19 +116,27 @@ function createForbiddenPathError() {
116
116
  return error;
117
117
  }
118
118
 
119
- function isPathInsideRoot(candidatePath) {
119
+ function toServedRelativePath(candidatePath) {
120
+ if (typeof candidatePath !== 'string' || candidatePath.length === 0) {
121
+ return null;
122
+ }
123
+
120
124
  const normalizedPath = path.resolve(candidatePath);
121
125
  const relativePath = path.relative(rootDirAbs, normalizedPath);
122
- return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
123
- }
126
+ if (relativePath === '') {
127
+ return '';
128
+ }
124
129
 
125
- function normalizeServedPath(candidatePath) {
126
- if (typeof candidatePath !== 'string' || candidatePath.length === 0) {
130
+ if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
127
131
  return null;
128
132
  }
129
133
 
130
- const normalizedPath = path.resolve(candidatePath);
131
- return isPathInsideRoot(normalizedPath) ? normalizedPath : null;
134
+ return relativePath;
135
+ }
136
+
137
+ function normalizeServedPath(candidatePath) {
138
+ const relativePath = toServedRelativePath(candidatePath);
139
+ return relativePath == null ? null : path.join(rootDirAbs, relativePath);
132
140
  }
133
141
 
134
142
  function statServedPath(targetPath) {
@@ -219,6 +227,10 @@ function stringifyInlineScriptPayload(value, indent = 0) {
219
227
  .join('\n');
220
228
  }
221
229
 
230
+ function stringifyInlineScriptLiteral(value) {
231
+ return escapeInlineScriptContent(JSON.stringify(value));
232
+ }
233
+
222
234
  function normalizePreloadStorageType(storageType) {
223
235
  return storageType === 'sessionStorage' ? 'sessionStorage' : 'localStorage';
224
236
  }
@@ -763,14 +775,14 @@ function findTypeScriptFiles(dir, files = []) {
763
775
  */
764
776
  function generatePreloadScript(name, defaultValue, storageType = 'localStorage') {
765
777
  const safeStorageType = normalizePreloadStorageType(storageType);
766
- const payload = JSON.stringify({
778
+ const payload = stringifyInlineScriptLiteral({
767
779
  key: name,
768
780
  defaultValue,
769
781
  storageType: safeStorageType,
770
782
  });
771
- const fallbackValue = JSON.stringify(defaultValue);
783
+ const fallbackValue = stringifyInlineScriptLiteral(defaultValue);
772
784
  const script = `(function(){try{var p=${payload};var s=window[p.storageType];var v=s.getItem(p.key);document.documentElement.setAttribute('data-theme',v==null?p.defaultValue:JSON.parse(v))}catch(e){document.documentElement.setAttribute('data-theme',${fallbackValue})}})();`;
773
- return escapeInlineScriptContent(script);
785
+ return script;
774
786
  }
775
787
 
776
788
  function renderPreloadScriptTags(baseDir) {