dalila 1.10.4 → 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 +29 -12
- package/dist/core/signal.d.ts +5 -0
- package/dist/core/signal.js +20 -14
- package/dist/runtime/bind.js +36 -17
- package/dist/runtime/fromHtml.js +20 -3
- package/dist/runtime/html-sinks.js +19 -3
- package/package.json +1 -1
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
88
|
+
if (!attr.value.includes(tokenSpec.prefix))
|
|
75
89
|
continue;
|
|
76
|
-
|
|
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
|
-
|
|
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 += `${
|
|
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;
|
package/dist/core/signal.d.ts
CHANGED
|
@@ -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. */
|
package/dist/core/signal.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
package/dist/runtime/bind.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
195
|
-
|
|
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
|
|
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(
|
|
327
|
+
const boundary = root.closest(INTERNAL_BOUND_SELECTOR);
|
|
310
328
|
return out.filter(el => {
|
|
311
|
-
const bound = el.closest(
|
|
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
|
|
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
|
|
1583
|
+
const bound = resolveBoundary(parent);
|
|
1565
1584
|
if (bound !== textBoundary)
|
|
1566
1585
|
continue;
|
|
1567
1586
|
}
|
package/dist/runtime/fromHtml.js
CHANGED
|
@@ -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
|
|
19
|
-
container
|
|
20
|
-
|
|
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
|
+
}
|
|
@@ -78,9 +78,25 @@ function hasExecutableHtmlScriptTag(value) {
|
|
|
78
78
|
return false;
|
|
79
79
|
}
|
|
80
80
|
function hasExecutableProtocol(value) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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';
|
|
84
100
|
}
|
|
85
101
|
function hasExecutableHtmlEventAttribute(value) {
|
|
86
102
|
let index = 0;
|