@hyperframes/core 0.8.93 → 0.8.95

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.
@@ -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