@hyperframes/core 0.8.92 → 0.8.94
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/compiler/compositionScoping.d.ts.map +1 -1
- package/dist/compiler/compositionScoping.js +91 -81
- package/dist/compiler/compositionScoping.js.map +1 -1
- package/dist/compiler/inlineSubCompositions.d.ts.map +1 -1
- package/dist/compiler/inlineSubCompositions.js +38 -0
- package/dist/compiler/inlineSubCompositions.js.map +1 -1
- package/dist/compiler/selectorIdTokens.d.ts +52 -0
- package/dist/compiler/selectorIdTokens.d.ts.map +1 -0
- package/dist/compiler/selectorIdTokens.js +195 -0
- package/dist/compiler/selectorIdTokens.js.map +1 -0
- package/dist/compiler/svgIdNamespacing.d.ts +78 -0
- package/dist/compiler/svgIdNamespacing.d.ts.map +1 -0
- package/dist/compiler/svgIdNamespacing.js +365 -0
- package/dist/compiler/svgIdNamespacing.js.map +1 -0
- package/dist/compositionReadiness.d.ts.map +1 -1
- package/dist/compositionReadiness.js +4 -1
- package/dist/compositionReadiness.js.map +1 -1
- package/dist/generated/runtime-inline.js +1 -1
- package/dist/generated/runtime-inline.js.map +1 -1
- package/dist/hyperframe.runtime.iife.js +147 -82
- package/dist/runtime/timeline.d.ts +6 -0
- package/dist/runtime/timeline.d.ts.map +1 -1
- package/dist/runtime/timeline.js +1 -1
- package/dist/runtime/timeline.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared low-level scanner: walk a CSS selector and replace whole-token
|
|
3
|
+
* `#id` occurrences that sit outside quotes and attribute-selector brackets.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from `compositionScoping.ts`'s authored-root-id rewrite (the
|
|
6
|
+
* original, single-id version of this scan) so `svgIdNamespacing.ts` can
|
|
7
|
+
* reuse the exact same quote/bracket-tracking logic for its many-id rewrite
|
|
8
|
+
* instead of a second, drifting copy of the same state machine.
|
|
9
|
+
*
|
|
10
|
+
* Split into two small passes rather than one branch-heavy loop: first mark
|
|
11
|
+
* which offsets are outside a quoted string or an attribute-selector
|
|
12
|
+
* bracket (`markUnguardedOffsets`), then walk the selector once more,
|
|
13
|
+
* consulting that mask, to actually splice in replacements
|
|
14
|
+
* (`replaceSelectorIdTokens`). Each pass stays simple enough to read at a
|
|
15
|
+
* glance instead of one function juggling both jobs.
|
|
16
|
+
*
|
|
17
|
+
* The `#id` token is read as a CSS identifier per CSS Syntax Level 3
|
|
18
|
+
* (`decodeCssIdentifierAt`): `\.`-style and `\HEX `-style escapes are
|
|
19
|
+
* decoded before the id is compared against the candidate list, so a
|
|
20
|
+
* stylesheet spelling `#fx\.1` matches the element whose raw `id` attribute
|
|
21
|
+
* is `fx.1`. Callers emit replacement selectors through
|
|
22
|
+
* `escapeCssIdentifier`, the inverse operation.
|
|
23
|
+
*/
|
|
24
|
+
/** Unescaped name code points: `[A-Za-z0-9_-]` plus any non-ASCII. */
|
|
25
|
+
function isNameChar(char) {
|
|
26
|
+
if (!char)
|
|
27
|
+
return false;
|
|
28
|
+
return /[\w-]/.test(char) || char.charCodeAt(0) >= 0x80;
|
|
29
|
+
}
|
|
30
|
+
/** CSS whitespace as defined by CSS Syntax Level 3 (`\r\n` handled by the caller). */
|
|
31
|
+
function isCssWhitespace(char) {
|
|
32
|
+
return char === " " || char === "\t" || char === "\n" || char === "\r" || char === "\f";
|
|
33
|
+
}
|
|
34
|
+
const HEX_ESCAPE_RE = /^[0-9a-fA-F]{1,6}/;
|
|
35
|
+
/** Out-of-range and surrogate code points decode to U+FFFD, as the spec requires. */
|
|
36
|
+
function sanitizeCodePoint(codePoint) {
|
|
37
|
+
const isSurrogate = codePoint >= 0xd800 && codePoint <= 0xdfff;
|
|
38
|
+
return codePoint === 0 || codePoint > 0x10ffff || isSurrogate ? 0xfffd : codePoint;
|
|
39
|
+
}
|
|
40
|
+
/** A hex escape swallows ONE following whitespace; `\r\n` counts as one. */
|
|
41
|
+
function skipEscapeWhitespace(text, index) {
|
|
42
|
+
if (text[index] === "\r" && text[index + 1] === "\n")
|
|
43
|
+
return index + 2;
|
|
44
|
+
return isCssWhitespace(text[index]) ? index + 1 : index;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* "Consume an escaped code point" (CSS Syntax Level 3) with `text[index]`
|
|
48
|
+
* being the backslash: 1-6 hex digits plus one optional trailing whitespace
|
|
49
|
+
* decode to that code point, any other non-newline character decodes to
|
|
50
|
+
* itself. Returns `null` for an invalid escape (trailing backslash or a
|
|
51
|
+
* backslash before a newline), which ends the identifier.
|
|
52
|
+
*/
|
|
53
|
+
function consumeCssEscape(text, index) {
|
|
54
|
+
const next = text[index + 1];
|
|
55
|
+
if (next === undefined || next === "\n")
|
|
56
|
+
return null;
|
|
57
|
+
const hex = HEX_ESCAPE_RE.exec(text.slice(index + 1, index + 7));
|
|
58
|
+
if (!hex)
|
|
59
|
+
return { value: next, end: index + 2 };
|
|
60
|
+
const codePoint = sanitizeCodePoint(Number.parseInt(hex[0], 16));
|
|
61
|
+
return {
|
|
62
|
+
value: String.fromCodePoint(codePoint),
|
|
63
|
+
end: skipEscapeWhitespace(text, index + 1 + hex[0].length),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Read one CSS identifier starting at `start` and return its decoded value
|
|
68
|
+
* plus the offset just past it, or `null` when no identifier starts there.
|
|
69
|
+
* Follows the "consume an ident sequence" algorithm of CSS Syntax Level 3.
|
|
70
|
+
*/
|
|
71
|
+
export function decodeCssIdentifierAt(text, start) {
|
|
72
|
+
let index = start;
|
|
73
|
+
let value = "";
|
|
74
|
+
while (index < text.length) {
|
|
75
|
+
const char = text[index];
|
|
76
|
+
if (char === "\\") {
|
|
77
|
+
const escape = consumeCssEscape(text, index);
|
|
78
|
+
if (!escape)
|
|
79
|
+
break;
|
|
80
|
+
value += escape.value;
|
|
81
|
+
index = escape.end;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!isNameChar(char))
|
|
85
|
+
break;
|
|
86
|
+
value += char;
|
|
87
|
+
index += 1;
|
|
88
|
+
}
|
|
89
|
+
return index === start ? null : { value, end: index };
|
|
90
|
+
}
|
|
91
|
+
const CODE_UNIT_HYPHEN = 0x2d;
|
|
92
|
+
function isDigitCodeUnit(codeUnit) {
|
|
93
|
+
return codeUnit >= 0x30 && codeUnit <= 0x39;
|
|
94
|
+
}
|
|
95
|
+
/** Code units `CSS.escape` copies through verbatim: `[A-Za-z0-9_-]` and non-ASCII. */
|
|
96
|
+
function isPlainNameCodeUnit(codeUnit) {
|
|
97
|
+
return (codeUnit >= 0x80 ||
|
|
98
|
+
codeUnit === CODE_UNIT_HYPHEN ||
|
|
99
|
+
codeUnit === 0x5f ||
|
|
100
|
+
isDigitCodeUnit(codeUnit) ||
|
|
101
|
+
(codeUnit >= 0x41 && codeUnit <= 0x5a) ||
|
|
102
|
+
(codeUnit >= 0x61 && codeUnit <= 0x7a));
|
|
103
|
+
}
|
|
104
|
+
/** Code units `CSS.escape` writes as a hex escape: control characters, and a
|
|
105
|
+
* digit in a position where it would otherwise start a number. */
|
|
106
|
+
function needsHexEscape(value, index) {
|
|
107
|
+
const codeUnit = value.charCodeAt(index);
|
|
108
|
+
if ((codeUnit >= 0x01 && codeUnit <= 0x1f) || codeUnit === 0x7f)
|
|
109
|
+
return true;
|
|
110
|
+
if (!isDigitCodeUnit(codeUnit))
|
|
111
|
+
return false;
|
|
112
|
+
return index === 0 || (index === 1 && value.charCodeAt(0) === CODE_UNIT_HYPHEN);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Serialize `value` as a CSS identifier — the `CSS.escape()` algorithm from
|
|
116
|
+
* CSSOM, implemented here because Node has no `CSS` global. Every output is
|
|
117
|
+
* a valid `#ident` selector body that `decodeCssIdentifierAt` round-trips
|
|
118
|
+
* back to `value`.
|
|
119
|
+
*/
|
|
120
|
+
export function escapeCssIdentifier(value) {
|
|
121
|
+
let result = "";
|
|
122
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
123
|
+
const codeUnit = value.charCodeAt(index);
|
|
124
|
+
const char = value[index];
|
|
125
|
+
if (codeUnit === 0)
|
|
126
|
+
result += "\uFFFD";
|
|
127
|
+
else if (needsHexEscape(value, index))
|
|
128
|
+
result += `\\${codeUnit.toString(16)} `;
|
|
129
|
+
else if (index === 0 && value.length === 1 && codeUnit === CODE_UNIT_HYPHEN)
|
|
130
|
+
result += `\\${char}`;
|
|
131
|
+
else if (isPlainNameCodeUnit(codeUnit))
|
|
132
|
+
result += char;
|
|
133
|
+
else
|
|
134
|
+
result += `\\${char}`;
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* A full attribute-selector bracket (`[data-x="a"]`, quotes optional, `]`
|
|
140
|
+
* inside a quoted value tolerated) or a bare quoted string. Either is a
|
|
141
|
+
* region where a literal `#` is never an id-selector prefix — the CSS parser
|
|
142
|
+
* itself never looks for one there — so `markUnguardedOffsets` below can
|
|
143
|
+
* find every such region with one pass of `matchAll` instead of a hand-rolled
|
|
144
|
+
* character-by-character state machine. Inside the bracket alternative, bare
|
|
145
|
+
* characters exclude the quote marks so each position matches exactly one
|
|
146
|
+
* branch (no ambiguous backtracking).
|
|
147
|
+
*/
|
|
148
|
+
const GUARDED_SELECTOR_SEGMENT_RE = /\\(?:[0-9a-fA-F]{1,6}(?:\r\n|[ \t\r\n\f])?|[\s\S])|"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\[(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^\]"'])*\]/g;
|
|
149
|
+
/**
|
|
150
|
+
* `mask[i]` is `true` when `selector[i]` sits outside both a quoted string
|
|
151
|
+
* and an attribute-selector bracket — the two places a literal `#` is never
|
|
152
|
+
* an id selector prefix.
|
|
153
|
+
*/
|
|
154
|
+
function markUnguardedOffsets(selector) {
|
|
155
|
+
const mask = new Array(selector.length).fill(true);
|
|
156
|
+
for (const match of selector.matchAll(GUARDED_SELECTOR_SEGMENT_RE)) {
|
|
157
|
+
const start = match.index;
|
|
158
|
+
mask.fill(false, start, start + match[0].length);
|
|
159
|
+
}
|
|
160
|
+
return mask;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Replace every whole-token `#id` in `selector` whose DECODED identifier is
|
|
164
|
+
* in `candidateIds`, skipping occurrences inside a quoted string or an
|
|
165
|
+
* attribute-selector bracket.
|
|
166
|
+
*
|
|
167
|
+
* Because the whole identifier is consumed before comparing, `#clip2` is
|
|
168
|
+
* never mistaken for `#clip`, and `#fx\.1` matches the candidate `fx.1`.
|
|
169
|
+
* `resolveReplacement` receives the matched (decoded) id and returns the
|
|
170
|
+
* full replacement text (including its own leading `#`, if any) to splice
|
|
171
|
+
* in.
|
|
172
|
+
*/
|
|
173
|
+
export function replaceSelectorIdTokens(selector, candidateIds, resolveReplacement) {
|
|
174
|
+
if (candidateIds.length === 0 || !selector.includes("#"))
|
|
175
|
+
return selector;
|
|
176
|
+
const candidates = new Set(candidateIds);
|
|
177
|
+
const unguarded = markUnguardedOffsets(selector);
|
|
178
|
+
let result = "";
|
|
179
|
+
let index = 0;
|
|
180
|
+
while (index < selector.length) {
|
|
181
|
+
const token = unguarded[index] && selector[index] === "#"
|
|
182
|
+
? decodeCssIdentifierAt(selector, index + 1)
|
|
183
|
+
: null;
|
|
184
|
+
if (token && candidates.has(token.value)) {
|
|
185
|
+
result += resolveReplacement(token.value);
|
|
186
|
+
index = token.end;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
result += selector[index];
|
|
190
|
+
index += 1;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=selectorIdTokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selectorIdTokens.js","sourceRoot":"","sources":["../../src/compiler/selectorIdTokens.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native SVG url()/href references resolve by document order, bypassing JS scoping.
|
|
3
|
+
* Rename only colliding, natively referenced SVG ids; leave JS-only ids reachable
|
|
4
|
+
* by libraries that bypass the scoped selector shim. The first unrenamable element
|
|
5
|
+
* keeps the authored id (otherwise the first element does). Renamed elements retain
|
|
6
|
+
* data-hf-authored-id for script lookup compatibility.
|
|
7
|
+
*/
|
|
8
|
+
/** Reused from `compositionScoping.ts`'s `AUTHORED_ROOT_ID_ATTR` in spirit —
|
|
9
|
+
* same purpose (let a renamed id still resolve by its authored name), now
|
|
10
|
+
* generalized from "the composition root" to any renamed descendant. */
|
|
11
|
+
export declare const SVG_AUTHORED_ID_ATTR = "data-hf-authored-id";
|
|
12
|
+
/** Structural shape both a linkedom/live-DOM `Element` and `Document`
|
|
13
|
+
* satisfy — mirrors the narrow-interface pattern `mediaRenderIds.ts` and
|
|
14
|
+
* `compositionAssembly.ts` already use so this module works unmodified
|
|
15
|
+
* across the preview bundler and the render compiler. */
|
|
16
|
+
interface SvgIdQueryable {
|
|
17
|
+
querySelectorAll(selector: string): Iterable<Element>;
|
|
18
|
+
}
|
|
19
|
+
/** One inlined composition instance, as seen in the ASSEMBLED document. */
|
|
20
|
+
export interface SvgIdScope {
|
|
21
|
+
/** The element whose subtree holds this instance's content (the host). */
|
|
22
|
+
root: Element;
|
|
23
|
+
/**
|
|
24
|
+
* Document-unique prefix for ids renamed in this scope — the instance's
|
|
25
|
+
* runtime composition id. An empty namespace makes the scope read-only:
|
|
26
|
+
* an anonymous host has no identity to prefix with, the same guard
|
|
27
|
+
* `scopeCssToComposition` and `wrapScopedCompositionScript` apply.
|
|
28
|
+
*/
|
|
29
|
+
namespace: string;
|
|
30
|
+
/**
|
|
31
|
+
* Nested composition hosts inside `root` whose content belongs to their
|
|
32
|
+
* OWN scope. Their subtrees are skipped both when collecting this scope's
|
|
33
|
+
* ids and when rewriting its references.
|
|
34
|
+
*/
|
|
35
|
+
exclude?: readonly Element[];
|
|
36
|
+
/**
|
|
37
|
+
* This instance's extracted `<style>` text (already removed from the DOM
|
|
38
|
+
* by the inline pipeline). Scanned for `url(#id)` references so a filter
|
|
39
|
+
* that is only applied from a stylesheet still counts as natively
|
|
40
|
+
* referenced. The caller rewrites these strings afterwards via
|
|
41
|
+
* `rewriteSvgIdReferencesInCss` with the returned map.
|
|
42
|
+
*/
|
|
43
|
+
cssTexts?: readonly string[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Rename every colliding, natively-referenced SVG id across the inlined
|
|
47
|
+
* composition instances in `scopes` and rewrite each instance's attribute
|
|
48
|
+
* references (`href`/`xlink:href`, any `url(#id)` funcref — including inside
|
|
49
|
+
* a `style` attribute) to match.
|
|
50
|
+
*
|
|
51
|
+
* Returns one old-id -> new-id map per scope, in the same order, so the
|
|
52
|
+
* caller can apply the identical substitution to that instance's separately
|
|
53
|
+
* extracted `<style>` text via `rewriteSvgIdReferencesInCss`, which this
|
|
54
|
+
* function never sees. Maps include inherited references whose original target
|
|
55
|
+
* was renamed elsewhere; scopes with local definitions keep their own mapping.
|
|
56
|
+
*
|
|
57
|
+
* `document` must be the ASSEMBLED document every scope root lives in: the
|
|
58
|
+
* collision census covers the whole thing, including ids the top-level
|
|
59
|
+
* document declares itself, since those collide with an inlined instance's
|
|
60
|
+
* ids just as two instances collide with each other.
|
|
61
|
+
*/
|
|
62
|
+
export declare function namespaceCollidingSvgIds(document: SvgIdQueryable, scopes: readonly SvgIdScope[]): Map<string, string>[];
|
|
63
|
+
/**
|
|
64
|
+
* Apply the same id substitution `namespaceCollidingSvgIds` computed to a
|
|
65
|
+
* composition's `<style>` text.
|
|
66
|
+
*
|
|
67
|
+
* `<style>` content is extracted from the DOM and carried around as a raw
|
|
68
|
+
* string by the inline pipeline (see `inlineSubCompositions`'s
|
|
69
|
+
* `scopeSubStyle`), so it is never visited by the attribute walk. Both a bare
|
|
70
|
+
* `#id` selector (`#clip rect { fill: red }`, already scoped to the right
|
|
71
|
+
* instance by #556's composition-box prefix, but still naming the PRE-rename
|
|
72
|
+
* id) and a `url(#id)` declaration value need rewriting here, or a
|
|
73
|
+
* same-composition stylesheet rule silently stops matching the element whose
|
|
74
|
+
* id this module just changed.
|
|
75
|
+
*/
|
|
76
|
+
export declare function rewriteSvgIdReferencesInCss(css: string, idMap: ReadonlyMap<string, string>): string;
|
|
77
|
+
export {};
|
|
78
|
+
//# sourceMappingURL=svgIdNamespacing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"svgIdNamespacing.d.ts","sourceRoot":"","sources":["../../src/compiler/svgIdNamespacing.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH;;yEAEyE;AACzE,eAAO,MAAM,oBAAoB,wBAAwB,CAAC;AAoE1D;;;0DAG0D;AAC1D,UAAU,cAAc;IACtB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;CACvD;AAED,2EAA2E;AAC3E,MAAM,WAAW,UAAU;IACzB,0EAA0E;IAC1E,IAAI,EAAE,OAAO,CAAC;IACd;;;;;OAKG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;IAC7B;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B;AA8LD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE,SAAS,UAAU,EAAE,GAC5B,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAiCvB;AAqBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,2BAA2B,CACzC,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC,MAAM,CAsCR"}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native SVG url()/href references resolve by document order, bypassing JS scoping.
|
|
3
|
+
* Rename only colliding, natively referenced SVG ids; leave JS-only ids reachable
|
|
4
|
+
* by libraries that bypass the scoped selector shim. The first unrenamable element
|
|
5
|
+
* keeps the authored id (otherwise the first element does). Renamed elements retain
|
|
6
|
+
* data-hf-authored-id for script lookup compatibility.
|
|
7
|
+
*/
|
|
8
|
+
import postcss from "postcss";
|
|
9
|
+
import { escapeCssIdentifier, replaceSelectorIdTokens } from "./selectorIdTokens.js";
|
|
10
|
+
const ID_ATTR = "id";
|
|
11
|
+
/** Reused from `compositionScoping.ts`'s `AUTHORED_ROOT_ID_ATTR` in spirit —
|
|
12
|
+
* same purpose (let a renamed id still resolve by its authored name), now
|
|
13
|
+
* generalized from "the composition root" to any renamed descendant. */
|
|
14
|
+
export const SVG_AUTHORED_ID_ATTR = "data-hf-authored-id";
|
|
15
|
+
/**
|
|
16
|
+
* Matches a `url(#id)` funcref value — quoted or bare — as used by
|
|
17
|
+
* `clip-path`, `filter`, `mask`, `fill`, `stroke`, `marker-start/mid/end`,
|
|
18
|
+
* `cursor`, `mask-image`, and equally by any of those written into an inline
|
|
19
|
+
* `style` attribute or a `<style>` declaration value. One pattern covers all
|
|
20
|
+
* of them because CSS only ever spells an id reference this way in a
|
|
21
|
+
* property VALUE — a bare `#id` (no `url()`) is exclusively a *selector*.
|
|
22
|
+
*
|
|
23
|
+
* The captured id is the literal fragment text. A fragment is a URL, not a
|
|
24
|
+
* CSS identifier, so it carries no CSS escapes; the replacement keeps the
|
|
25
|
+
* original quoting and only swaps the id, which stays a valid fragment
|
|
26
|
+
* because the namespace prefix is restricted to `[A-Za-z0-9_-]`.
|
|
27
|
+
*/
|
|
28
|
+
const URL_HASH_REF_RE = /(url\(\s*)(["']?)#([^"')\s]+)\2(\s*\))/gi;
|
|
29
|
+
/**
|
|
30
|
+
* Attributes carrying a bare `#id` fragment reference rather than a
|
|
31
|
+
* `url(#id)` funcref — SVG's `<use>`, `<a>`, `<pattern>`, `<textPath>`,
|
|
32
|
+
* `<feImage>`, `<mpath>`, and so on. Matching by suffix also covers
|
|
33
|
+
* namespaced `xlink:href`, however a given DOM implementation exposes it.
|
|
34
|
+
*/
|
|
35
|
+
function isHrefAttrName(name) {
|
|
36
|
+
return name === "href" || name.endsWith(":href");
|
|
37
|
+
}
|
|
38
|
+
function sanitizeNamespaceSegment(value) {
|
|
39
|
+
return value.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
40
|
+
}
|
|
41
|
+
function rewriteUrlHashRefs(value, idMap) {
|
|
42
|
+
if (!value || !value.toLowerCase().includes("url("))
|
|
43
|
+
return value;
|
|
44
|
+
return value.replace(URL_HASH_REF_RE, (full, pre, quote, id, post) => {
|
|
45
|
+
const mapped = idMap.get(id);
|
|
46
|
+
return mapped ? `${pre}${quote}#${mapped}${quote}${post}` : full;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function rewriteHrefValue(value, idMap) {
|
|
50
|
+
if (!value.startsWith("#"))
|
|
51
|
+
return null;
|
|
52
|
+
const mapped = idMap.get(value.slice(1));
|
|
53
|
+
return mapped ? `#${mapped}` : null;
|
|
54
|
+
}
|
|
55
|
+
/** Rewrite every id reference carried on one element's attributes. CSS text
|
|
56
|
+
* inside a `<style>` element is handled separately by
|
|
57
|
+
* `rewriteSvgIdReferencesInCss` — it is extracted and scoped as a raw
|
|
58
|
+
* string elsewhere in the inline pipeline, never visited by this walk. */
|
|
59
|
+
function rewriteElementIdReferences(el, idMap) {
|
|
60
|
+
const attrs = el.attributes ? Array.from(el.attributes) : [];
|
|
61
|
+
for (const attr of attrs) {
|
|
62
|
+
const { name, value } = attr;
|
|
63
|
+
if (!value)
|
|
64
|
+
continue;
|
|
65
|
+
if (isHrefAttrName(name)) {
|
|
66
|
+
const rewritten = rewriteHrefValue(value, idMap);
|
|
67
|
+
if (rewritten) {
|
|
68
|
+
el.setAttribute(name, rewritten);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (value.toLowerCase().includes("url(")) {
|
|
73
|
+
const rewritten = rewriteUrlHashRefs(value, idMap);
|
|
74
|
+
if (rewritten !== value)
|
|
75
|
+
el.setAttribute(name, rewritten);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function collectUrlHashRefsFromText(text, filter, out) {
|
|
80
|
+
let m;
|
|
81
|
+
URL_HASH_REF_RE.lastIndex = 0;
|
|
82
|
+
while ((m = URL_HASH_REF_RE.exec(text))) {
|
|
83
|
+
const refId = m[3];
|
|
84
|
+
if (filter.has(refId))
|
|
85
|
+
out.add(refId);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function collectHrefFragmentRef(attr, filter, out) {
|
|
89
|
+
if (isHrefAttrName(attr.name) && attr.value.startsWith("#")) {
|
|
90
|
+
const id = attr.value.slice(1);
|
|
91
|
+
if (filter.has(id))
|
|
92
|
+
out.add(id);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Ids referenced by native browser resolution — `url(#id)` funcrefs and
|
|
97
|
+
* bare `href="#id"` fragment refs — as opposed to JavaScript-only refs
|
|
98
|
+
* (e.g. GSAP's `tl.to("#cut-1")`). Global libraries access `document`
|
|
99
|
+
* directly and bypass the composition-scoped querySelector Proxy, so only
|
|
100
|
+
* natively-referenced ids are safe to rename.
|
|
101
|
+
*
|
|
102
|
+
* RESIDUAL, BY DESIGN: two instances that both animate the same JS-ONLY id
|
|
103
|
+
* (two catalog scenes each doing `tl.to("#cut-1")`) both keep `id="cut-1"`.
|
|
104
|
+
* Scripts wrapped by `wrapScopedCompositionScript` still find their own
|
|
105
|
+
* element (the scoped `document`/GSAP proxies filter to the instance root),
|
|
106
|
+
* but an UNSCOPED lookup — a third-party library reading `document` directly
|
|
107
|
+
* — binds to the first instance in document order, exactly as it did before
|
|
108
|
+
* this module existed. Broadening this pre-scan to JS-only ids would trade
|
|
109
|
+
* that residual for breaking every such library lookup outright (the
|
|
110
|
+
* regression that motivated the gate), so the residual stays.
|
|
111
|
+
*
|
|
112
|
+
* Also not pre-scanned: references a script injects at runtime
|
|
113
|
+
* (`el.setAttribute("clip-path", "url(#foo)")` as the ONLY reference to
|
|
114
|
+
* `#foo`). Static markup and stylesheets are the contract.
|
|
115
|
+
*/
|
|
116
|
+
function collectNativelyReferencedIds(elements, cssTexts, svgIds) {
|
|
117
|
+
const referenced = new Set();
|
|
118
|
+
for (const el of elements) {
|
|
119
|
+
for (const attr of el.attributes ? Array.from(el.attributes) : []) {
|
|
120
|
+
if (!attr.value)
|
|
121
|
+
continue;
|
|
122
|
+
collectHrefFragmentRef(attr, svgIds, referenced);
|
|
123
|
+
if (attr.value.toLowerCase().includes("url("))
|
|
124
|
+
collectUrlHashRefsFromText(attr.value, svgIds, referenced);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const text of cssTexts) {
|
|
128
|
+
if (text.toLowerCase().includes("url("))
|
|
129
|
+
collectUrlHashRefsFromText(text, svgIds, referenced);
|
|
130
|
+
}
|
|
131
|
+
return referenced;
|
|
132
|
+
}
|
|
133
|
+
function isExcluded(el, exclude) {
|
|
134
|
+
return exclude.some((excluded) => excluded === el || excluded.contains(el));
|
|
135
|
+
}
|
|
136
|
+
/** `root` plus every descendant that is not inside an excluded subtree. */
|
|
137
|
+
function collectScopeElements(scope) {
|
|
138
|
+
const exclude = scope.exclude ?? [];
|
|
139
|
+
const descendants = [...scope.root.querySelectorAll("*")];
|
|
140
|
+
const own = exclude.length ? descendants.filter((el) => !isExcluded(el, exclude)) : descendants;
|
|
141
|
+
return [scope.root, ...own];
|
|
142
|
+
}
|
|
143
|
+
/** Every `<svg>`-subtree element in the scope that carries an id. */
|
|
144
|
+
function collectSvgIdElements(scope) {
|
|
145
|
+
const exclude = scope.exclude ?? [];
|
|
146
|
+
const matches = [...scope.root.querySelectorAll("svg [id], svg[id]")];
|
|
147
|
+
return exclude.length ? matches.filter((el) => !isExcluded(el, exclude)) : matches;
|
|
148
|
+
}
|
|
149
|
+
function resolveScope(scope) {
|
|
150
|
+
const elements = collectScopeElements(scope);
|
|
151
|
+
const renamable = new Set();
|
|
152
|
+
if (!scope.namespace)
|
|
153
|
+
return { elements, renamable };
|
|
154
|
+
const svgIdElements = collectSvgIdElements(scope);
|
|
155
|
+
const svgIds = new Set();
|
|
156
|
+
for (const el of svgIdElements) {
|
|
157
|
+
const id = el.getAttribute(ID_ATTR);
|
|
158
|
+
if (id)
|
|
159
|
+
svgIds.add(id);
|
|
160
|
+
}
|
|
161
|
+
if (svgIds.size === 0)
|
|
162
|
+
return { elements, renamable };
|
|
163
|
+
const nativelyReferenced = collectNativelyReferencedIds(elements, scope.cssTexts ?? [], svgIds);
|
|
164
|
+
for (const el of svgIdElements) {
|
|
165
|
+
const id = el.getAttribute(ID_ATTR);
|
|
166
|
+
if (id && nativelyReferenced.has(id))
|
|
167
|
+
renamable.add(el);
|
|
168
|
+
}
|
|
169
|
+
return { elements, renamable };
|
|
170
|
+
}
|
|
171
|
+
function buildIdCensus(document) {
|
|
172
|
+
const elementsById = new Map();
|
|
173
|
+
const usedIds = new Set();
|
|
174
|
+
for (const el of document.querySelectorAll("[id]")) {
|
|
175
|
+
const id = el.getAttribute(ID_ATTR);
|
|
176
|
+
if (!id)
|
|
177
|
+
continue;
|
|
178
|
+
usedIds.add(id);
|
|
179
|
+
const list = elementsById.get(id);
|
|
180
|
+
if (list)
|
|
181
|
+
list.push(el);
|
|
182
|
+
else
|
|
183
|
+
elementsById.set(id, [el]);
|
|
184
|
+
}
|
|
185
|
+
return { elementsById, usedIds };
|
|
186
|
+
}
|
|
187
|
+
function hasAnyCollision(census) {
|
|
188
|
+
for (const elements of census.elementsById.values()) {
|
|
189
|
+
if (elements.length > 1)
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
/** A namespaced id that is not already taken anywhere in the document. */
|
|
195
|
+
function mintNamespacedId(namespace, originalId, usedIds) {
|
|
196
|
+
const base = `${sanitizeNamespaceSegment(namespace)}--${originalId}`;
|
|
197
|
+
let candidate = base;
|
|
198
|
+
for (let n = 2; usedIds.has(candidate); n += 1)
|
|
199
|
+
candidate = `${base}-${n}`;
|
|
200
|
+
usedIds.add(candidate);
|
|
201
|
+
return candidate;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Decide, per scope, which ids get renamed and to what. For every colliding
|
|
205
|
+
* id the keeper is the first element in document order that no scope can
|
|
206
|
+
* rename, else simply the first element; every other renamable element's
|
|
207
|
+
* scope receives a mapping for that id.
|
|
208
|
+
*/
|
|
209
|
+
function planRenames(census, scopes, scopeIndexByRenamable) {
|
|
210
|
+
const idMaps = scopes.map(() => new Map());
|
|
211
|
+
for (const [id, elements] of census.elementsById) {
|
|
212
|
+
if (elements.length < 2)
|
|
213
|
+
continue;
|
|
214
|
+
const keeper = elements.find((el) => !scopeIndexByRenamable.has(el)) ?? elements[0];
|
|
215
|
+
for (const el of elements) {
|
|
216
|
+
if (el === keeper)
|
|
217
|
+
continue;
|
|
218
|
+
const scopeIndex = scopeIndexByRenamable.get(el);
|
|
219
|
+
if (scopeIndex === undefined)
|
|
220
|
+
continue;
|
|
221
|
+
const idMap = idMaps[scopeIndex];
|
|
222
|
+
if (!idMap.has(id)) {
|
|
223
|
+
idMap.set(id, mintNamespacedId(scopes[scopeIndex].namespace, id, census.usedIds));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return idMaps;
|
|
228
|
+
}
|
|
229
|
+
/** Rename the planned elements in one scope and rewrite every reference in
|
|
230
|
+
* that scope's attributes to match. */
|
|
231
|
+
function applyRenames(resolved, idMap) {
|
|
232
|
+
for (const el of resolved.elements) {
|
|
233
|
+
const currentId = el.getAttribute(ID_ATTR);
|
|
234
|
+
if (currentId && resolved.renamable.has(el) && idMap.has(currentId)) {
|
|
235
|
+
if (!el.hasAttribute(SVG_AUTHORED_ID_ATTR))
|
|
236
|
+
el.setAttribute(SVG_AUTHORED_ID_ATTR, currentId);
|
|
237
|
+
el.setAttribute(ID_ATTR, idMap.get(currentId));
|
|
238
|
+
}
|
|
239
|
+
rewriteElementIdReferences(el, idMap);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Rename every colliding, natively-referenced SVG id across the inlined
|
|
244
|
+
* composition instances in `scopes` and rewrite each instance's attribute
|
|
245
|
+
* references (`href`/`xlink:href`, any `url(#id)` funcref — including inside
|
|
246
|
+
* a `style` attribute) to match.
|
|
247
|
+
*
|
|
248
|
+
* Returns one old-id -> new-id map per scope, in the same order, so the
|
|
249
|
+
* caller can apply the identical substitution to that instance's separately
|
|
250
|
+
* extracted `<style>` text via `rewriteSvgIdReferencesInCss`, which this
|
|
251
|
+
* function never sees. Maps include inherited references whose original target
|
|
252
|
+
* was renamed elsewhere; scopes with local definitions keep their own mapping.
|
|
253
|
+
*
|
|
254
|
+
* `document` must be the ASSEMBLED document every scope root lives in: the
|
|
255
|
+
* collision census covers the whole thing, including ids the top-level
|
|
256
|
+
* document declares itself, since those collide with an inlined instance's
|
|
257
|
+
* ids just as two instances collide with each other.
|
|
258
|
+
*/
|
|
259
|
+
export function namespaceCollidingSvgIds(document, scopes) {
|
|
260
|
+
const untouched = scopes.map(() => new Map());
|
|
261
|
+
if (scopes.length === 0)
|
|
262
|
+
return untouched;
|
|
263
|
+
const census = buildIdCensus(document);
|
|
264
|
+
if (!hasAnyCollision(census))
|
|
265
|
+
return untouched;
|
|
266
|
+
const resolved = scopes.map(resolveScope);
|
|
267
|
+
const scopeIndexByRenamable = new Map();
|
|
268
|
+
resolved.forEach(({ renamable }, index) => {
|
|
269
|
+
for (const el of renamable)
|
|
270
|
+
scopeIndexByRenamable.set(el, index);
|
|
271
|
+
});
|
|
272
|
+
if (scopeIndexByRenamable.size === 0)
|
|
273
|
+
return untouched;
|
|
274
|
+
const idMaps = planRenames(census, scopes, scopeIndexByRenamable);
|
|
275
|
+
// A reference without a local definition keeps its original document-order target,
|
|
276
|
+
// even if that target was renamed in an ancestor or another composition.
|
|
277
|
+
const referenceMaps = resolved.map((scope, index) => {
|
|
278
|
+
const references = new Map(idMaps[index]);
|
|
279
|
+
const localIds = new Set(scope.elements.map((el) => el.getAttribute(ID_ATTR)));
|
|
280
|
+
for (const [id, elements] of census.elementsById) {
|
|
281
|
+
if (localIds.has(id))
|
|
282
|
+
continue;
|
|
283
|
+
const targetScope = scopeIndexByRenamable.get(elements[0]);
|
|
284
|
+
if (targetScope === undefined)
|
|
285
|
+
continue;
|
|
286
|
+
const mapped = idMaps[targetScope].get(id);
|
|
287
|
+
if (mapped)
|
|
288
|
+
references.set(id, mapped);
|
|
289
|
+
}
|
|
290
|
+
return references;
|
|
291
|
+
});
|
|
292
|
+
referenceMaps.forEach((idMap, index) => {
|
|
293
|
+
if (idMap.size > 0)
|
|
294
|
+
applyRenames(resolved[index], idMap);
|
|
295
|
+
});
|
|
296
|
+
return referenceMaps;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Whole-token `#id` replacement inside a CSS selector, generalized to many
|
|
300
|
+
* ids at once and to a literal `#newId` swap instead of an attribute-selector
|
|
301
|
+
* expansion (a plain descendant/id selector already resolves correctly once
|
|
302
|
+
* the id itself is unique — no extra scoping needed). Shares its quote- and
|
|
303
|
+
* bracket-aware scan with `compositionScoping.ts`'s
|
|
304
|
+
* `replaceAuthoredRootIdSelectors` via `selectorIdTokens.ts`, rather than a
|
|
305
|
+
* second copy of the same state machine. The scanner decodes CSS identifier
|
|
306
|
+
* escapes before matching, so `#fx\.1` matches the raw id `fx.1`, and the
|
|
307
|
+
* replacement is re-escaped so the emitted selector stays valid.
|
|
308
|
+
*/
|
|
309
|
+
function renameIdTokensInSelector(selector, idMap) {
|
|
310
|
+
return replaceSelectorIdTokens(selector, [...idMap.keys()], (matchedId) => `#${escapeCssIdentifier(idMap.get(matchedId))}`);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Apply the same id substitution `namespaceCollidingSvgIds` computed to a
|
|
314
|
+
* composition's `<style>` text.
|
|
315
|
+
*
|
|
316
|
+
* `<style>` content is extracted from the DOM and carried around as a raw
|
|
317
|
+
* string by the inline pipeline (see `inlineSubCompositions`'s
|
|
318
|
+
* `scopeSubStyle`), so it is never visited by the attribute walk. Both a bare
|
|
319
|
+
* `#id` selector (`#clip rect { fill: red }`, already scoped to the right
|
|
320
|
+
* instance by #556's composition-box prefix, but still naming the PRE-rename
|
|
321
|
+
* id) and a `url(#id)` declaration value need rewriting here, or a
|
|
322
|
+
* same-composition stylesheet rule silently stops matching the element whose
|
|
323
|
+
* id this module just changed.
|
|
324
|
+
*/
|
|
325
|
+
export function rewriteSvgIdReferencesInCss(css, idMap) {
|
|
326
|
+
if (!css || idMap.size === 0)
|
|
327
|
+
return css;
|
|
328
|
+
if (!css.includes("#") && !css.toLowerCase().includes("url("))
|
|
329
|
+
return css;
|
|
330
|
+
let root;
|
|
331
|
+
try {
|
|
332
|
+
root = postcss.parse(css);
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
// Unparseable CSS is the caller's problem to report; leaving it untouched
|
|
336
|
+
// is strictly better than dropping the whole stylesheet here.
|
|
337
|
+
return css;
|
|
338
|
+
}
|
|
339
|
+
let mutated = false;
|
|
340
|
+
root.walkRules((rule) => {
|
|
341
|
+
const rewritten = rule.selectors.map((selector) => renameIdTokensInSelector(selector, idMap));
|
|
342
|
+
if (rewritten.some((selector, index) => selector !== rule.selectors[index])) {
|
|
343
|
+
rule.selectors = rewritten;
|
|
344
|
+
mutated = true;
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
root.walkDecls((decl) => {
|
|
348
|
+
const rewritten = rewriteUrlHashRefs(decl.value, idMap);
|
|
349
|
+
if (rewritten !== decl.value) {
|
|
350
|
+
decl.value = rewritten;
|
|
351
|
+
mutated = true;
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
root.walkAtRules((atRule) => {
|
|
355
|
+
if (!atRule.params)
|
|
356
|
+
return;
|
|
357
|
+
const rewritten = rewriteUrlHashRefs(atRule.params, idMap);
|
|
358
|
+
if (rewritten !== atRule.params) {
|
|
359
|
+
atRule.params = rewritten;
|
|
360
|
+
mutated = true;
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
return mutated ? root.toResult({ map: false }).css : css;
|
|
364
|
+
}
|
|
365
|
+
//# sourceMappingURL=svgIdNamespacing.js.map
|