@pie-players/pie-players-shared 0.3.60 → 0.3.62

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.
Files changed (147) hide show
  1. package/dist/config/profile.js +0 -1
  2. package/dist/i18n/index.js +0 -1
  3. package/dist/i18n/loader.js +0 -1
  4. package/dist/i18n/simple-i18n.js +0 -1
  5. package/dist/i18n/types.js +0 -1
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +2 -1
  8. package/dist/instrumentation/debug-panel-stream.js +0 -1
  9. package/dist/instrumentation/index.js +0 -1
  10. package/dist/instrumentation/provider-guards.js +0 -1
  11. package/dist/instrumentation/providers/BaseInstrumentationProvider.js +0 -1
  12. package/dist/instrumentation/providers/CompositeInstrumentationProvider.js +0 -1
  13. package/dist/instrumentation/providers/ConsoleInstrumentationProvider.js +0 -1
  14. package/dist/instrumentation/providers/DebugPanelInstrumentationProvider.js +0 -1
  15. package/dist/instrumentation/providers/NewRelicInstrumentationProvider.js +0 -1
  16. package/dist/instrumentation/providers/index.js +0 -1
  17. package/dist/instrumentation/types.js +0 -1
  18. package/dist/loader-config.js +0 -1
  19. package/dist/loaders/ElementLoader.js +0 -1
  20. package/dist/loaders/element-loader-types.js +0 -1
  21. package/dist/loaders/element-loader.js +0 -1
  22. package/dist/loaders/element-package-policy.js +0 -1
  23. package/dist/loaders/esm-adapter.js +0 -1
  24. package/dist/loaders/iife-adapter.js +0 -1
  25. package/dist/loaders/index.js +0 -1
  26. package/dist/object/index.js +0 -1
  27. package/dist/pie/asset-handler.js +0 -1
  28. package/dist/pie/authoring-tag.js +0 -1
  29. package/dist/pie/authoring.js +0 -1
  30. package/dist/pie/component-context.js +0 -1
  31. package/dist/pie/config.js +0 -1
  32. package/dist/pie/configure-initialization.js +0 -1
  33. package/dist/pie/correct-response-env.js +0 -1
  34. package/dist/pie/custom-element-define.js +0 -1
  35. package/dist/pie/index.js +0 -1
  36. package/dist/pie/initialization.js +0 -1
  37. package/dist/pie/instrumentation-event-bridge.js +0 -1
  38. package/dist/pie/instrumentation-event-map.js +0 -1
  39. package/dist/pie/instrumentation-provider-resolution.js +0 -1
  40. package/dist/pie/item-controller-storage.js +0 -1
  41. package/dist/pie/item-controller.js +0 -1
  42. package/dist/pie/item-session-contract.js +0 -1
  43. package/dist/pie/logger.js +0 -1
  44. package/dist/pie/math-rendering.js +0 -1
  45. package/dist/pie/overrides.js +0 -1
  46. package/dist/pie/player-initializer.js +0 -1
  47. package/dist/pie/registry.js +0 -1
  48. package/dist/pie/resource-monitor.js +0 -1
  49. package/dist/pie/scoring.js +0 -1
  50. package/dist/pie/stage-tracker.js +0 -1
  51. package/dist/pie/stages.js +0 -1
  52. package/dist/pie/tag-names.js +0 -1
  53. package/dist/pie/types.js +0 -1
  54. package/dist/pie/updates.js +0 -1
  55. package/dist/pie/utils.js +0 -1
  56. package/dist/pie/versioned-tag.js +0 -1
  57. package/dist/player-strategy.js +0 -1
  58. package/dist/security/index.js +0 -1
  59. package/dist/security/sanitize-item-markup.d.ts +11 -0
  60. package/dist/security/sanitize-item-markup.js +5 -3
  61. package/dist/security/sanitize-svg-icon.js +0 -1
  62. package/dist/security/validate-style-url.js +0 -1
  63. package/dist/security/wrap-model-rich-content.js +0 -1
  64. package/dist/security/wrap-overwide-images.js +0 -1
  65. package/dist/security/wrap-overwide-tables.js +0 -1
  66. package/dist/server/npm-registry.js +0 -1
  67. package/dist/types/index.js +0 -1
  68. package/dist/ui/content-styles.d.ts +73 -0
  69. package/dist/ui/content-styles.js +204 -0
  70. package/dist/ui/debug-panel-persistence.js +0 -1
  71. package/dist/ui/first-focusable.js +0 -1
  72. package/dist/ui/focus-trap.js +0 -1
  73. package/dist/ui/safe-storage.js +0 -1
  74. package/dist/ui/scope-css.d.ts +56 -0
  75. package/dist/ui/scope-css.js +310 -0
  76. package/dist/ui/zoom-compensation.js +0 -1
  77. package/package.json +9 -5
  78. package/dist/config/profile.js.map +0 -1
  79. package/dist/i18n/index.js.map +0 -1
  80. package/dist/i18n/loader.js.map +0 -1
  81. package/dist/i18n/simple-i18n.js.map +0 -1
  82. package/dist/i18n/types.js.map +0 -1
  83. package/dist/index.js.map +0 -1
  84. package/dist/instrumentation/debug-panel-stream.js.map +0 -1
  85. package/dist/instrumentation/index.js.map +0 -1
  86. package/dist/instrumentation/provider-guards.js.map +0 -1
  87. package/dist/instrumentation/providers/BaseInstrumentationProvider.js.map +0 -1
  88. package/dist/instrumentation/providers/CompositeInstrumentationProvider.js.map +0 -1
  89. package/dist/instrumentation/providers/ConsoleInstrumentationProvider.js.map +0 -1
  90. package/dist/instrumentation/providers/DebugPanelInstrumentationProvider.js.map +0 -1
  91. package/dist/instrumentation/providers/NewRelicInstrumentationProvider.js.map +0 -1
  92. package/dist/instrumentation/providers/index.js.map +0 -1
  93. package/dist/instrumentation/types.js.map +0 -1
  94. package/dist/loader-config.js.map +0 -1
  95. package/dist/loaders/ElementLoader.js.map +0 -1
  96. package/dist/loaders/element-loader-types.js.map +0 -1
  97. package/dist/loaders/element-loader.js.map +0 -1
  98. package/dist/loaders/element-package-policy.js.map +0 -1
  99. package/dist/loaders/esm-adapter.js.map +0 -1
  100. package/dist/loaders/iife-adapter.js.map +0 -1
  101. package/dist/loaders/index.js.map +0 -1
  102. package/dist/object/index.js.map +0 -1
  103. package/dist/pie/asset-handler.js.map +0 -1
  104. package/dist/pie/authoring-tag.js.map +0 -1
  105. package/dist/pie/authoring.js.map +0 -1
  106. package/dist/pie/component-context.js.map +0 -1
  107. package/dist/pie/config.js.map +0 -1
  108. package/dist/pie/configure-initialization.js.map +0 -1
  109. package/dist/pie/correct-response-env.js.map +0 -1
  110. package/dist/pie/custom-element-define.js.map +0 -1
  111. package/dist/pie/index.js.map +0 -1
  112. package/dist/pie/initialization.js.map +0 -1
  113. package/dist/pie/instrumentation-event-bridge.js.map +0 -1
  114. package/dist/pie/instrumentation-event-map.js.map +0 -1
  115. package/dist/pie/instrumentation-provider-resolution.js.map +0 -1
  116. package/dist/pie/item-controller-storage.js.map +0 -1
  117. package/dist/pie/item-controller.js.map +0 -1
  118. package/dist/pie/item-session-contract.js.map +0 -1
  119. package/dist/pie/logger.js.map +0 -1
  120. package/dist/pie/math-rendering.js.map +0 -1
  121. package/dist/pie/overrides.js.map +0 -1
  122. package/dist/pie/player-initializer.js.map +0 -1
  123. package/dist/pie/registry.js.map +0 -1
  124. package/dist/pie/resource-monitor.js.map +0 -1
  125. package/dist/pie/scoring.js.map +0 -1
  126. package/dist/pie/stage-tracker.js.map +0 -1
  127. package/dist/pie/stages.js.map +0 -1
  128. package/dist/pie/tag-names.js.map +0 -1
  129. package/dist/pie/types.js.map +0 -1
  130. package/dist/pie/updates.js.map +0 -1
  131. package/dist/pie/utils.js.map +0 -1
  132. package/dist/pie/versioned-tag.js.map +0 -1
  133. package/dist/player-strategy.js.map +0 -1
  134. package/dist/security/index.js.map +0 -1
  135. package/dist/security/sanitize-item-markup.js.map +0 -1
  136. package/dist/security/sanitize-svg-icon.js.map +0 -1
  137. package/dist/security/validate-style-url.js.map +0 -1
  138. package/dist/security/wrap-model-rich-content.js.map +0 -1
  139. package/dist/security/wrap-overwide-images.js.map +0 -1
  140. package/dist/security/wrap-overwide-tables.js.map +0 -1
  141. package/dist/server/npm-registry.js.map +0 -1
  142. package/dist/types/index.js.map +0 -1
  143. package/dist/ui/debug-panel-persistence.js.map +0 -1
  144. package/dist/ui/first-focusable.js.map +0 -1
  145. package/dist/ui/focus-trap.js.map +0 -1
  146. package/dist/ui/safe-storage.js.map +0 -1
  147. package/dist/ui/zoom-compensation.js.map +0 -1
@@ -0,0 +1,204 @@
1
+ /**
2
+ * Installs the shared PIE content stylesheet
3
+ * (`@pie-players/pie-theme/components.css`) into the host document.
4
+ *
5
+ * Authored assessment content depends on classes that belong to no single
6
+ * component: passage markup (`.numbered-paragraph`, `.p-number`,
7
+ * `div.passage-title`), the legacy `kds-*` families, the `@media print`
8
+ * `.noprint` rules, and the answer-eliminator classes. Players render into
9
+ * light DOM, so those rules have to exist as a document-level stylesheet —
10
+ * there is no shadow root to scope them to.
11
+ *
12
+ * Hosts used to be required to import that stylesheet themselves. Nothing
13
+ * enforced it and nothing failed loudly when they didn't: the item rendered,
14
+ * the passage was simply unstyled, and it surfaced as a visual bug reported by
15
+ * hand days later. Players install the stylesheet themselves instead, which
16
+ * keeps the host contract at "import the player".
17
+ *
18
+ * The CSS text is passed in rather than imported here: this package builds with
19
+ * plain `tsc`, so it cannot inline a stylesheet. Bundler-built player packages
20
+ * import it with Vite's `?inline` and hand the text over.
21
+ */
22
+ /** Marks a `<style>` element this module owns, and keeps installs idempotent. */
23
+ const MARKER_ATTRIBUTE = "data-pie-content-styles";
24
+ /**
25
+ * Declared by `components.css` itself, so it is observable no matter how the
26
+ * stylesheet arrived — our injection or a host import. Used only to tell a host
27
+ * that opted out but then shipped nothing.
28
+ */
29
+ const SENTINEL_PROPERTY = "--pie-content-styles";
30
+ /** `<html data-pie-content-styles="host">` opts a host out of installation. */
31
+ const OPT_OUT_ATTRIBUTE = "data-pie-content-styles";
32
+ const OPT_OUT_VALUE = "host";
33
+ const isBrowser = () => typeof document !== "undefined" && !!document.head;
34
+ /**
35
+ * True when the host declared `<html data-pie-content-styles="host">`, i.e. it
36
+ * takes ownership of loading the stylesheet.
37
+ */
38
+ export function contentStylesOptedOut() {
39
+ if (!isBrowser())
40
+ return false;
41
+ return (document.documentElement.getAttribute(OPT_OUT_ATTRIBUTE) === OPT_OUT_VALUE);
42
+ }
43
+ /**
44
+ * True when `components.css` is applied to the document, by any route. Reads the
45
+ * sentinel custom property the stylesheet declares on `:root`.
46
+ *
47
+ * Only meaningful once the document's stylesheets have been applied — a host
48
+ * that loads CSS via an async `<link>` reads as missing until it lands.
49
+ */
50
+ export function contentStylesPresent() {
51
+ if (!isBrowser())
52
+ return false;
53
+ const value = getComputedStyle(document.documentElement)
54
+ .getPropertyValue(SENTINEL_PROPERTY)
55
+ .trim();
56
+ return value !== "";
57
+ }
58
+ /**
59
+ * Installs `cssText` as a document-level stylesheet, once per document.
60
+ *
61
+ * The stylesheet is **prepended** to `<head>` and deliberately left out of a
62
+ * cascade layer. Prepending reproduces the placement hosts were told to use by
63
+ * hand — first in the entry, before app CSS — so an app rule and a content rule
64
+ * of equal specificity resolve in the host's favour, exactly as before.
65
+ *
66
+ * A cascade layer looks tempting here and is the wrong tool: unlayered author
67
+ * declarations beat *all* layered ones regardless of specificity, so a host
68
+ * reset as broad as `p { margin: 0 }` would silently outrank
69
+ * `.numbered-paragraph { margin-left: 36px }`. That trades a visible missing
70
+ * stylesheet for a subtler override bug, so ordinary specificity wins instead.
71
+ *
72
+ * @param cssText Contents of `@pie-players/pie-theme/components.css`.
73
+ * @param source Package installing the styles, for diagnostics.
74
+ */
75
+ export function installContentStyles(cssText, source) {
76
+ if (!isBrowser())
77
+ return "no-document";
78
+ if (contentStylesOptedOut())
79
+ return "opted-out";
80
+ if (document.querySelector(`style[${MARKER_ATTRIBUTE}]`)) {
81
+ // Another player package, or a second copy of this one, already installed
82
+ // the same stylesheet. Duplicating it would be harmless but pointless.
83
+ return "already-installed";
84
+ }
85
+ if (!cssText)
86
+ return "no-document";
87
+ const style = document.createElement("style");
88
+ style.setAttribute(MARKER_ATTRIBUTE, source);
89
+ style.textContent = cssText;
90
+ document.head.prepend(style);
91
+ return "installed";
92
+ }
93
+ /**
94
+ * Counts content stylesheets in the document that this module did not install —
95
+ * i.e. copies the host loaded itself. Detected by the sentinel property rather
96
+ * than by URL, so a copy arriving as a `<link>`, a bundler-injected `<style>`, or
97
+ * anything else all count the same.
98
+ *
99
+ * Cross-origin sheets throw on `cssRules` access and are skipped; a host copy
100
+ * served from another origin therefore reads as absent. That only costs a
101
+ * diagnostic, never correctness.
102
+ */
103
+ const countHostContentStyleSheets = () => {
104
+ // Walks the owning elements rather than document.styleSheets: the marker
105
+ // attribute lives on the element, and CSSStyleSheet.ownerNode is not
106
+ // universally implemented (happy-dom omits it), which would make our own
107
+ // installed copy look like a host copy.
108
+ const nodes = document.querySelectorAll(`style:not([${MARKER_ATTRIBUTE}]), link[rel~="stylesheet"]:not([${MARKER_ATTRIBUTE}])`);
109
+ let count = 0;
110
+ for (const node of Array.from(nodes)) {
111
+ let rules;
112
+ try {
113
+ rules = node.sheet?.cssRules;
114
+ }
115
+ catch {
116
+ continue;
117
+ }
118
+ if (!rules)
119
+ continue;
120
+ for (const rule of Array.from(rules)) {
121
+ if (rule.style?.getPropertyValue(SENTINEL_PROPERTY)) {
122
+ count += 1;
123
+ break;
124
+ }
125
+ }
126
+ }
127
+ return count;
128
+ };
129
+ let auditWarningIssued = false;
130
+ const pendingChecks = [];
131
+ /**
132
+ * Reports, once per page, the two ways content styling can go wrong. Neither is
133
+ * detectable synchronously — a host copy may still be in flight as an async
134
+ * `<link>` — so the check is deferred.
135
+ *
136
+ * - **Missing**: the host opted out and then loaded nothing, so authored content
137
+ * renders unstyled. This is the failure the old host-import contract produced
138
+ * silently.
139
+ * - **Duplicated**: the host still imports `components.css` itself *and* the
140
+ * player installed a copy. Rendering is correct while the two agree, but the
141
+ * host's copy loads later and therefore wins ties at equal specificity — so a
142
+ * host copy pinned to an older `@pie-players/pie-theme` silently overrides the
143
+ * player's newer rules. Harmless today, a confusing override tomorrow.
144
+ */
145
+ export function auditContentStyles(source) {
146
+ if (!isBrowser() || auditWarningIssued)
147
+ return;
148
+ const check = () => {
149
+ if (auditWarningIssued)
150
+ return;
151
+ if (contentStylesOptedOut()) {
152
+ if (contentStylesPresent())
153
+ return;
154
+ auditWarningIssued = true;
155
+ console.warn(`[${source}] No PIE content stylesheet found. This document sets ` +
156
+ `${OPT_OUT_ATTRIBUTE}="${OPT_OUT_VALUE}", so ${source} did not install ` +
157
+ `one. Authored content that relies on shared classes ` +
158
+ `(.numbered-paragraph, .p-number, div.passage-title, the kds-* ` +
159
+ `families, answer-eliminator styles) will render unstyled. Either ` +
160
+ `import "@pie-players/pie-theme/components.css" in the host app, or ` +
161
+ `drop the ${OPT_OUT_ATTRIBUTE} attribute to let the player install it.`);
162
+ return;
163
+ }
164
+ if (countHostContentStyleSheets() > 0) {
165
+ auditWarningIssued = true;
166
+ console.warn(`[${source}] The PIE content stylesheet is loaded twice: ${source} ` +
167
+ `installs it, and this host also imports ` +
168
+ `"@pie-players/pie-theme/components.css". Rendering is unaffected ` +
169
+ `while both copies match, but the host copy loads later and wins ` +
170
+ `ties at equal specificity, so a copy pinned to an older ` +
171
+ `@pie-players/pie-theme will silently override newer player styles. ` +
172
+ `Remove the host import, or set ` +
173
+ `${OPT_OUT_ATTRIBUTE}="${OPT_OUT_VALUE}" on <html> to own the ` +
174
+ `stylesheet deliberately.`);
175
+ }
176
+ };
177
+ // A host stylesheet can land well after the module graph evaluates, and a
178
+ // bundler-injected <link> is not necessarily parsed by the time `load` fires.
179
+ // This is advisory output, so it is checked at a few widening points rather
180
+ // than raced: whichever one first sees a settled document wins, and the latch
181
+ // keeps the rest quiet. Missing the window costs a diagnostic, not
182
+ // correctness.
183
+ if (typeof requestAnimationFrame !== "function") {
184
+ check();
185
+ return;
186
+ }
187
+ requestAnimationFrame(() => requestAnimationFrame(check));
188
+ if (typeof window !== "undefined" && document.readyState !== "complete") {
189
+ window.addEventListener("load", () => check(), { once: true });
190
+ }
191
+ if (typeof setTimeout === "function") {
192
+ pendingChecks.push(setTimeout(check, 1000));
193
+ }
194
+ }
195
+ /**
196
+ * Test-only: clears the once-per-page warning latch and cancels any pending
197
+ * deferred check, so a timer scheduled by one test cannot warn during the next.
198
+ */
199
+ export function resetContentStylesWarningForTesting() {
200
+ auditWarningIssued = false;
201
+ for (const handle of pendingChecks)
202
+ clearTimeout(handle);
203
+ pendingChecks.length = 0;
204
+ }
@@ -35,4 +35,3 @@ export function clearDebugPanelState(key) {
35
35
  // ignore
36
36
  }
37
37
  }
38
- //# sourceMappingURL=debug-panel-persistence.js.map
@@ -24,4 +24,3 @@ export function isProgrammaticFocusTarget(el) {
24
24
  return false;
25
25
  return el.offsetParent !== null || el.getClientRects().length > 0;
26
26
  }
27
- //# sourceMappingURL=first-focusable.js.map
@@ -81,4 +81,3 @@ export function createFocusTrap(container, options = {}) {
81
81
  }
82
82
  };
83
83
  }
84
- //# sourceMappingURL=focus-trap.js.map
@@ -18,4 +18,3 @@ export function safeLocalStorageSet(key, value) {
18
18
  // ignore
19
19
  }
20
20
  }
21
- //# sourceMappingURL=safe-storage.js.map
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Scope an external stylesheet's rules to a single player instance, so a
3
+ * partner-authored stylesheet loaded via `<pie-item-player
4
+ * external-style-urls="...">` or `itemConfig.resources.stylesheets[*].url`
5
+ * styles that player's subtree instead of the whole host document.
6
+ *
7
+ * This replaces a single-regex implementation that prefixed every
8
+ * selector-like fragment. That is correct for flat selector rules and wrong for
9
+ * everything else: `@media screen { ... }` became
10
+ * `.scope @media screen { ... }`, an invalid selector, so the browser dropped
11
+ * the whole block and every rule inside it; `@font-face` and `@keyframes` were
12
+ * corrupted the same way; and `:root` became `.scope :root`, which can never
13
+ * match, because `:root` is `<html>` and is not a descendant of the player.
14
+ * At-rules and `:root` custom properties therefore never applied at all.
15
+ *
16
+ * The QTI player's `scopeCssRules` (`pie-qti` -
17
+ * `packages/item-player/src/components/utils/stylesheetRender.ts`) is not a
18
+ * drop-in either. Its `:root` handling is the behaviour we want and is adopted
19
+ * here, but it excludes `@` from its selector pattern rather than understanding
20
+ * at-rules, so `@media` and `@supports` lose their condition and their inner
21
+ * rules are hoisted and applied unconditionally — a mobile-only rule then
22
+ * applies at every viewport width, which is a subtler failure than dropping it.
23
+ *
24
+ * So this walks the stylesheet brace-by-brace instead of pattern-matching it.
25
+ * A real CSS parser would be the textbook answer and is deliberately not used:
26
+ * `@pie-players/pie-item-player` ships with two workspace dependencies, this
27
+ * runs in the delivery path, and scoping needs to know about rule boundaries
28
+ * only — not about property grammar.
29
+ *
30
+ * Known limits, all preserving existing behaviour rather than adding policy:
31
+ *
32
+ * - `@import` is passed through untouched, exactly as the old regex left it. It
33
+ * pulls in an unscoped stylesheet, so it defeats scoping, but blocking it is
34
+ * a security policy decision and not this function's job. Callers gate
35
+ * stylesheet URLs with `validateExternalStyleUrl` before fetching.
36
+ * - Declaration blocks are emitted verbatim, so `url(...)` references are left
37
+ * as authored and resolve against the stylesheet's own URL as before.
38
+ * - A style rule's block is never rewritten, which is also what native CSS
39
+ * nesting needs: nested selectors are relative to a parent that has already
40
+ * been scoped.
41
+ */
42
+ /**
43
+ * Rewrite `cssText` so every selector it contains is confined to
44
+ * `scopeSelector`.
45
+ *
46
+ * Returns `cssText` unchanged when there is no scope selector to apply, and
47
+ * `""` for empty or non-string input.
48
+ */
49
+ export declare function scopeStylesheetCss(cssText: unknown, scopeSelector: string): string;
50
+ /**
51
+ * Confine a single selector to `scopeSelector`.
52
+ *
53
+ * Exported for tests and for callers that scope a selector they built
54
+ * themselves; `scopeStylesheetCss` is the entry point for stylesheet text.
55
+ */
56
+ export declare function scopeSelector(selector: string, scopeSelector: string): string;
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Scope an external stylesheet's rules to a single player instance, so a
3
+ * partner-authored stylesheet loaded via `<pie-item-player
4
+ * external-style-urls="...">` or `itemConfig.resources.stylesheets[*].url`
5
+ * styles that player's subtree instead of the whole host document.
6
+ *
7
+ * This replaces a single-regex implementation that prefixed every
8
+ * selector-like fragment. That is correct for flat selector rules and wrong for
9
+ * everything else: `@media screen { ... }` became
10
+ * `.scope @media screen { ... }`, an invalid selector, so the browser dropped
11
+ * the whole block and every rule inside it; `@font-face` and `@keyframes` were
12
+ * corrupted the same way; and `:root` became `.scope :root`, which can never
13
+ * match, because `:root` is `<html>` and is not a descendant of the player.
14
+ * At-rules and `:root` custom properties therefore never applied at all.
15
+ *
16
+ * The QTI player's `scopeCssRules` (`pie-qti` -
17
+ * `packages/item-player/src/components/utils/stylesheetRender.ts`) is not a
18
+ * drop-in either. Its `:root` handling is the behaviour we want and is adopted
19
+ * here, but it excludes `@` from its selector pattern rather than understanding
20
+ * at-rules, so `@media` and `@supports` lose their condition and their inner
21
+ * rules are hoisted and applied unconditionally — a mobile-only rule then
22
+ * applies at every viewport width, which is a subtler failure than dropping it.
23
+ *
24
+ * So this walks the stylesheet brace-by-brace instead of pattern-matching it.
25
+ * A real CSS parser would be the textbook answer and is deliberately not used:
26
+ * `@pie-players/pie-item-player` ships with two workspace dependencies, this
27
+ * runs in the delivery path, and scoping needs to know about rule boundaries
28
+ * only — not about property grammar.
29
+ *
30
+ * Known limits, all preserving existing behaviour rather than adding policy:
31
+ *
32
+ * - `@import` is passed through untouched, exactly as the old regex left it. It
33
+ * pulls in an unscoped stylesheet, so it defeats scoping, but blocking it is
34
+ * a security policy decision and not this function's job. Callers gate
35
+ * stylesheet URLs with `validateExternalStyleUrl` before fetching.
36
+ * - Declaration blocks are emitted verbatim, so `url(...)` references are left
37
+ * as authored and resolve against the stylesheet's own URL as before.
38
+ * - A style rule's block is never rewritten, which is also what native CSS
39
+ * nesting needs: nested selectors are relative to a parent that has already
40
+ * been scoped.
41
+ */
42
+ /**
43
+ * At-rules whose block contains nested *rules*, so scoping has to recurse into
44
+ * the block while the prelude is preserved verbatim.
45
+ *
46
+ * Everything not listed here is passed through untouched. That is the safe
47
+ * default in both directions: at-rules whose block holds declarations rather
48
+ * than rules (`@font-face`, `@page`, `@property`, `@counter-style`) or
49
+ * non-selector keys (`@keyframes` percentages) must not be scoped, and an
50
+ * at-rule this list has never heard of is more safely left alone than guessed
51
+ * at.
52
+ */
53
+ const NESTED_RULE_AT_RULES = new Set([
54
+ "media",
55
+ "supports",
56
+ "container",
57
+ "layer",
58
+ "scope",
59
+ ]);
60
+ /** Matches a leading `:root`, `html` or `body` not followed by more name characters. */
61
+ const DOCUMENT_ROOT_PREFIX = /^(?::root|html|body)(?![\w-])/;
62
+ /** Matches an at-rule name, with any vendor prefix, at the start of a prelude. */
63
+ const AT_RULE_NAME = /^@(?:-[a-z]+-)?([\w-]+)/i;
64
+ /**
65
+ * Rewrite `cssText` so every selector it contains is confined to
66
+ * `scopeSelector`.
67
+ *
68
+ * Returns `cssText` unchanged when there is no scope selector to apply, and
69
+ * `""` for empty or non-string input.
70
+ */
71
+ export function scopeStylesheetCss(cssText, scopeSelector) {
72
+ if (typeof cssText !== "string" || cssText.length === 0)
73
+ return "";
74
+ const scope = scopeSelector?.trim();
75
+ if (!scope)
76
+ return cssText;
77
+ return scopeRuleList(stripComments(cssText), scope);
78
+ }
79
+ /**
80
+ * Confine a single selector to `scopeSelector`.
81
+ *
82
+ * Exported for tests and for callers that scope a selector they built
83
+ * themselves; `scopeStylesheetCss` is the entry point for stylesheet text.
84
+ */
85
+ export function scopeSelector(selector, scopeSelector) {
86
+ const sel = selector.trim();
87
+ const scope = scopeSelector.trim();
88
+ if (!sel || !scope)
89
+ return "";
90
+ // Idempotent: re-scoping already-scoped CSS must not nest the scope twice.
91
+ // Deliberately narrow — a bare `startsWith` would also skip an authored
92
+ // selector that merely shares the scope's leading characters.
93
+ if (sel === scope || sel.startsWith(`${scope} `))
94
+ return sel;
95
+ // `:root`/`html`/`body` are replaced rather than prefixed. Prefixing yields
96
+ // `.scope :root`, a selector that cannot match, which is how external
97
+ // `:root { --var: ... }` custom properties used to disappear. Any compound
98
+ // or combinator that followed is preserved: `html.dark .a` -> `.scope.dark .a`.
99
+ const rootPrefix = DOCUMENT_ROOT_PREFIX.exec(sel);
100
+ if (rootPrefix)
101
+ return `${scope}${sel.slice(rootPrefix[0].length)}`;
102
+ // Everything else becomes a descendant, including a leading pseudo-class or
103
+ // pseudo-element. `pie-qti`'s scoper attaches those to the scope instead,
104
+ // which is not the same selector: `:is(.a, .b) .c` authored at stylesheet
105
+ // level means "some element matching .a or .b", so attaching it demands that
106
+ // the *player root* carry the partner's class, and it never does. The same
107
+ // holds for `:hover` and `::selection` — the author meant an element in the
108
+ // content, not the player's own box. Root selectors are the one case where
109
+ // attaching is right, and they are handled above.
110
+ return `${scope} ${sel}`;
111
+ }
112
+ function scopeRuleList(css, scope) {
113
+ let out = "";
114
+ let index = 0;
115
+ while (index < css.length) {
116
+ const node = readNode(css, index);
117
+ if (node.end <= index)
118
+ break;
119
+ out += renderNode(node, scope);
120
+ index = node.end;
121
+ }
122
+ return out;
123
+ }
124
+ function renderNode(node, scope) {
125
+ if (node.kind === "statement") {
126
+ const text = node.text.trim();
127
+ // A stray `;` at rule-list level carries nothing worth re-emitting.
128
+ return text === ";" ? "" : `${text}\n`;
129
+ }
130
+ if (node.kind === "trailing") {
131
+ // Only whitespace is expected here; anything else is malformed CSS that
132
+ // is better passed through than dropped.
133
+ return node.text.trim() ? `${node.text.trim()}\n` : "";
134
+ }
135
+ const prelude = node.prelude.trim();
136
+ if (!prelude)
137
+ return "";
138
+ if (prelude.startsWith("@")) {
139
+ const name = AT_RULE_NAME.exec(prelude)?.[1]?.toLowerCase();
140
+ if (name && NESTED_RULE_AT_RULES.has(name)) {
141
+ const inner = scopeRuleList(node.block, scope);
142
+ if (!inner.trim())
143
+ return "";
144
+ return `${prelude} {\n${inner}}\n`;
145
+ }
146
+ return `${prelude} {${node.block}}\n`;
147
+ }
148
+ const selectors = splitTopLevel(prelude, ",")
149
+ .map((selector) => scopeSelector(selector, scope))
150
+ .filter(Boolean);
151
+ if (selectors.length === 0)
152
+ return "";
153
+ return `${selectors.join(", ")} {${node.block}}\n`;
154
+ }
155
+ /**
156
+ * Read one node starting at `start`, tracking strings and parentheses so a `{`
157
+ * inside `content: "{"` or a `,` inside `:is(a, b)` is not mistaken for
158
+ * structure.
159
+ */
160
+ function readNode(css, start) {
161
+ let index = start;
162
+ let quote = null;
163
+ let parenDepth = 0;
164
+ while (index < css.length) {
165
+ const char = css[index];
166
+ if (quote) {
167
+ if (char === "\\")
168
+ index += 1;
169
+ else if (char === quote)
170
+ quote = null;
171
+ }
172
+ else if (char === '"' || char === "'") {
173
+ quote = char;
174
+ }
175
+ else if (char === "(") {
176
+ parenDepth += 1;
177
+ }
178
+ else if (char === ")") {
179
+ if (parenDepth > 0)
180
+ parenDepth -= 1;
181
+ }
182
+ else if (parenDepth === 0 && char === ";") {
183
+ return {
184
+ kind: "statement",
185
+ text: css.slice(start, index + 1),
186
+ end: index + 1,
187
+ };
188
+ }
189
+ else if (parenDepth === 0 && char === "{") {
190
+ const blockEnd = findBlockEnd(css, index);
191
+ return {
192
+ kind: "rule",
193
+ prelude: css.slice(start, index),
194
+ block: css.slice(index + 1, blockEnd.contentEnd),
195
+ end: blockEnd.end,
196
+ };
197
+ }
198
+ index += 1;
199
+ }
200
+ return { kind: "trailing", text: css.slice(start), end: css.length };
201
+ }
202
+ /**
203
+ * Find the `}` matching the `{` at `openIndex`. Unbalanced input (a truncated
204
+ * stylesheet) is treated as running to the end rather than throwing.
205
+ */
206
+ function findBlockEnd(css, openIndex) {
207
+ let depth = 0;
208
+ let quote = null;
209
+ for (let index = openIndex; index < css.length; index += 1) {
210
+ const char = css[index];
211
+ if (quote) {
212
+ if (char === "\\")
213
+ index += 1;
214
+ else if (char === quote)
215
+ quote = null;
216
+ continue;
217
+ }
218
+ if (char === '"' || char === "'") {
219
+ quote = char;
220
+ continue;
221
+ }
222
+ if (char === "{")
223
+ depth += 1;
224
+ else if (char === "}") {
225
+ depth -= 1;
226
+ if (depth === 0)
227
+ return { contentEnd: index, end: index + 1 };
228
+ }
229
+ }
230
+ return { contentEnd: css.length, end: css.length };
231
+ }
232
+ /** Split on `separator` at paren depth zero and outside strings. */
233
+ function splitTopLevel(value, separator) {
234
+ const parts = [];
235
+ let current = "";
236
+ let quote = null;
237
+ let parenDepth = 0;
238
+ for (let index = 0; index < value.length; index += 1) {
239
+ const char = value[index];
240
+ if (quote) {
241
+ current += char;
242
+ if (char === "\\" && index + 1 < value.length) {
243
+ current += value[index + 1];
244
+ index += 1;
245
+ }
246
+ else if (char === quote) {
247
+ quote = null;
248
+ }
249
+ continue;
250
+ }
251
+ if (char === '"' || char === "'") {
252
+ quote = char;
253
+ current += char;
254
+ continue;
255
+ }
256
+ if (char === "(")
257
+ parenDepth += 1;
258
+ else if (char === ")" && parenDepth > 0)
259
+ parenDepth -= 1;
260
+ if (char === separator && parenDepth === 0) {
261
+ parts.push(current);
262
+ current = "";
263
+ continue;
264
+ }
265
+ current += char;
266
+ }
267
+ parts.push(current);
268
+ return parts;
269
+ }
270
+ /**
271
+ * Remove comments before walking, so a comment sitting between two rules is not
272
+ * absorbed into the next rule's selector list. String-aware, so a literal
273
+ * `content: "/*"` survives.
274
+ */
275
+ function stripComments(css) {
276
+ let out = "";
277
+ let quote = null;
278
+ let index = 0;
279
+ while (index < css.length) {
280
+ const char = css[index];
281
+ if (quote) {
282
+ out += char;
283
+ if (char === "\\" && index + 1 < css.length) {
284
+ out += css[index + 1];
285
+ index += 2;
286
+ continue;
287
+ }
288
+ if (char === quote)
289
+ quote = null;
290
+ index += 1;
291
+ continue;
292
+ }
293
+ if (char === '"' || char === "'") {
294
+ quote = char;
295
+ out += char;
296
+ index += 1;
297
+ continue;
298
+ }
299
+ if (char === "/" && css[index + 1] === "*") {
300
+ const close = css.indexOf("*/", index + 2);
301
+ index = close === -1 ? css.length : close + 2;
302
+ // Keep a space so `a/**/b` does not become the single token `ab`.
303
+ out += " ";
304
+ continue;
305
+ }
306
+ out += char;
307
+ index += 1;
308
+ }
309
+ return out;
310
+ }
@@ -41,4 +41,3 @@ export function approximateZoomFromWidths(outerWidth, innerWidth) {
41
41
  export function computeZoomCompensation(zoom, maxZoom, minCompensation) {
42
42
  return Math.max(minCompensation, Math.min(1, maxZoom / zoom));
43
43
  }
44
- //# sourceMappingURL=zoom-compensation.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pie-players/pie-players-shared",
3
- "version": "0.3.60",
3
+ "version": "0.3.62",
4
4
  "type": "module",
5
5
  "description": "Shared runtime + UI utilities for PIE players",
6
6
  "license": "MIT",
@@ -64,6 +64,10 @@
64
64
  "./ui/zoom-compensation": {
65
65
  "types": "./dist/ui/zoom-compensation.d.ts",
66
66
  "import": "./dist/ui/zoom-compensation.js"
67
+ },
68
+ "./ui/content-styles": {
69
+ "types": "./dist/ui/content-styles.d.ts",
70
+ "import": "./dist/ui/content-styles.js"
67
71
  }
68
72
  },
69
73
  "files": [
@@ -72,7 +76,7 @@
72
76
  "dependencies": {
73
77
  "@pie-lib/math-rendering-module": "5.1.0",
74
78
  "dompurify": "^3.4.0",
75
- "semver": "^7.7.4"
79
+ "semver": "^7.8.5"
76
80
  },
77
81
  "scripts": {
78
82
  "build": "rm -rf dist tsconfig.tsbuildinfo && tsc -p tsconfig.json && mkdir -p dist/components && cp -R src/components/vendor dist/components/vendor",
@@ -85,11 +89,11 @@
85
89
  "scan-hardcoded": "bun run src/i18n/scripts/scan-hardcoded.ts"
86
90
  },
87
91
  "devDependencies": {
88
- "@biomejs/biome": "^2.4.12",
89
- "@happy-dom/global-registrator": "^20.9.0",
92
+ "@biomejs/biome": "^2.5.6",
93
+ "@happy-dom/global-registrator": "^20.11.1",
90
94
  "@types/semver": "^7.7.1",
91
95
  "glob": "^13.0.0",
92
- "svelte": "^5.55.4",
96
+ "svelte": "^5.56.8",
93
97
  "typescript": "^5.9.3"
94
98
  },
95
99
  "homepage": "https://github.com/pie-framework/pie-players/tree/master/packages/players-shared#readme",
@@ -1 +0,0 @@
1
- {"version":3,"file":"profile.js","sourceRoot":"","sources":["../../src/config/profile.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,8EAA8E;AAC9E,4EAA4E;AAC5E,MAAM,0BAA0B,GAAG,2BAA2B,CAAC;AAC/D,MAAM,0BAA0B,GAAG,GAAG,0BAA0B,WAAW,CAAC;AAS5E,SAAS,aAAa,CAAC,GAAW;IACjC,IAAI,CAAC;QACJ,sBAAsB;QACtB,MAAM,CAAC,GAAI,MAAM,CAAC,IAAY,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,CAAC,CAAC;IACrD,CAAC;IAAC,MAAM,CAAC,CAAA,CAAC;IACV,OAAO,SAAS,CAAC;AAClB,CAAC;AAED,MAAM,CAAC,MAAM,kBAAkB,GAC9B,CAAC,OAAO,MAAM,KAAK,WAAW,IAAI,MAAM,CAAC,sBAAsB,CAAC;IAChE,aAAa,CAAC,2BAA2B,CAAC;IAC1C,0BAA0B,CAAC;AAE5B,MAAM,CAAC,MAAM,kBAAkB,GAC9B,CAAC,OAAO,MAAM,KAAK,WAAW,IAAI,MAAM,CAAC,sBAAsB,CAAC;IAChE,aAAa,CAAC,2BAA2B,CAAC;IAC1C,0BAA0B,CAAC","sourcesContent":["/**\n * Minimal, frontend-safe defaults for PIE build service URLs.\n *\n * In PIEOneer, these come from `$env/dynamic/public`. In `pie-players` we avoid\n * SvelteKit env dependencies so this can be consumed by any bundler.\n */\n\n// Default public bundle host. Keep this aligned with the IIFE loader default.\n// (We prefer the proxy endpoint, which is stable/cached for browser usage.)\nconst DEFAULT_BUILDER_ORIGIN_URL = \"https://proxy.pie-api.com\";\nconst DEFAULT_BUILDER_BUNDLE_URL = `${DEFAULT_BUILDER_ORIGIN_URL}/bundles/`;\n\ndeclare global {\n\tinterface Window {\n\t\tPIE_BUILDER_ORIGIN_URL?: string;\n\t\tPIE_BUILDER_BUNDLE_URL?: string;\n\t}\n}\n\nfunction readPublicEnv(key: string): string | undefined {\n\ttry {\n\t\t// Vite/SvelteKit-like\n\t\tconst v = (import.meta as any)?.env?.[key];\n\t\tif (typeof v === \"string\" && v.length > 0) return v;\n\t} catch {}\n\treturn undefined;\n}\n\nexport const BUILDER_ORIGIN_URL =\n\t(typeof window !== \"undefined\" && window.PIE_BUILDER_ORIGIN_URL) ||\n\treadPublicEnv(\"PUBLIC_BUILDER_ORIGIN_URL\") ||\n\tDEFAULT_BUILDER_ORIGIN_URL;\n\nexport const BUILDER_BUNDLE_URL =\n\t(typeof window !== \"undefined\" && window.PIE_BUILDER_BUNDLE_URL) ||\n\treadPublicEnv(\"PUBLIC_BUILDER_BUNDLE_URL\") ||\n\tDEFAULT_BUILDER_BUNDLE_URL;\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/i18n/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EACN,oBAAoB,EACpB,mBAAmB,EACnB,WAAW,EACX,gBAAgB,GAChB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC","sourcesContent":["/**\n * I18n Module\n *\n * Internationalization system for PIE Players.\n *\n * @module @pie-players/pie-players-shared/i18n\n */\n\nexport {\n\tBUNDLED_TRANSLATIONS,\n\tgetAvailableLocales,\n\tisRTLLocale,\n\tloadTranslations,\n} from \"./loader.js\";\nexport { SimpleI18n } from \"./simple-i18n.js\";\nexport type {\n\tI18nConfig,\n\tI18nServiceApi,\n\tPluralTranslation,\n\tTranslationBundle,\n} from \"./types.js\";\n"]}