@pie-players/pie-players-shared 0.3.67 → 0.3.69

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 (87) hide show
  1. package/README.md +59 -0
  2. package/dist/formative/index.d.ts +16 -0
  3. package/dist/formative/index.js +5 -0
  4. package/dist/formative/mastery.d.ts +16 -0
  5. package/dist/formative/mastery.js +43 -0
  6. package/dist/formative/outcome.d.ts +19 -0
  7. package/dist/formative/outcome.js +90 -0
  8. package/dist/formative/policy.d.ts +37 -0
  9. package/dist/formative/policy.js +86 -0
  10. package/dist/formative/session.d.ts +19 -0
  11. package/dist/formative/session.js +113 -0
  12. package/dist/formative/state.d.ts +60 -0
  13. package/dist/formative/state.js +140 -0
  14. package/dist/formative/types.d.ts +164 -0
  15. package/dist/formative/types.js +13 -0
  16. package/dist/i18n/catalogs.d.ts +38 -0
  17. package/dist/i18n/catalogs.js +47 -0
  18. package/dist/i18n/index.d.ts +28 -4
  19. package/dist/i18n/index.js +34 -3
  20. package/dist/i18n/language-tags.d.ts +51 -0
  21. package/dist/i18n/language-tags.js +84 -0
  22. package/dist/i18n/messages/en-US.d.ts +601 -0
  23. package/dist/i18n/messages/en-US.js +617 -0
  24. package/dist/i18n/messages/nl-NL.d.ts +516 -0
  25. package/dist/i18n/messages/nl-NL.js +516 -0
  26. package/dist/i18n/provider.d.ts +168 -0
  27. package/dist/i18n/provider.js +472 -0
  28. package/dist/i18n/types.d.ts +177 -28
  29. package/dist/i18n/types.js +6 -2
  30. package/dist/index.d.ts +3 -1
  31. package/dist/index.js +2 -0
  32. package/dist/pie/custom-element-define.d.ts +24 -0
  33. package/dist/pie/custom-element-define.js +36 -6
  34. package/dist/pie/index.d.ts +1 -1
  35. package/dist/pie/index.js +1 -1
  36. package/dist/pie/versioned-tag.js +3 -2
  37. package/dist/security/sanitize-forbidden-lists.d.ts +8 -0
  38. package/dist/security/sanitize-forbidden-lists.js +41 -0
  39. package/dist/security/sanitize-item-markup.js +3 -36
  40. package/dist/security/sanitize-svg-icon.js +3 -25
  41. package/dist/security/wrap-overwide-images.d.ts +5 -10
  42. package/dist/security/wrap-overwide-images.js +13 -62
  43. package/dist/security/wrap-overwide-tables.d.ts +5 -10
  44. package/dist/security/wrap-overwide-tables.js +13 -62
  45. package/dist/security/wrap-overwide.d.ts +51 -0
  46. package/dist/security/wrap-overwide.js +91 -0
  47. package/dist/server/npm-auth-env.d.ts +13 -0
  48. package/dist/server/npm-auth-env.js +64 -0
  49. package/dist/timed-media/index.d.ts +17 -0
  50. package/dist/timed-media/index.js +16 -0
  51. package/dist/timed-media/media-element-source.d.ts +36 -0
  52. package/dist/timed-media/media-element-source.js +121 -0
  53. package/dist/timed-media/policy.d.ts +36 -0
  54. package/dist/timed-media/policy.js +297 -0
  55. package/dist/timed-media/session.d.ts +21 -0
  56. package/dist/timed-media/session.js +85 -0
  57. package/dist/timed-media/state.d.ts +66 -0
  58. package/dist/timed-media/state.js +357 -0
  59. package/dist/timed-media/types.d.ts +343 -0
  60. package/dist/timed-media/types.js +12 -0
  61. package/dist/tools/term-lookup.d.ts +189 -0
  62. package/dist/tools/term-lookup.js +284 -0
  63. package/dist/types/index.d.ts +105 -1
  64. package/dist/ui/attribute-coercion.d.ts +1 -0
  65. package/dist/ui/attribute-coercion.js +19 -0
  66. package/dist/ui/first-focusable.d.ts +22 -0
  67. package/dist/ui/first-focusable.js +47 -0
  68. package/dist/ui/focus-trap.js +7 -2
  69. package/dist/ui/pointer-drag.d.ts +30 -0
  70. package/dist/ui/pointer-drag.js +34 -0
  71. package/package.json +38 -6
  72. package/dist/i18n/loader.d.ts +0 -35
  73. package/dist/i18n/loader.js +0 -132
  74. package/dist/i18n/simple-i18n.d.ts +0 -68
  75. package/dist/i18n/simple-i18n.js +0 -198
  76. package/dist/i18n/translations/ar/common.json +0 -36
  77. package/dist/i18n/translations/ar/toolkit.json +0 -48
  78. package/dist/i18n/translations/ar/tools.json +0 -103
  79. package/dist/i18n/translations/en/common.json +0 -36
  80. package/dist/i18n/translations/en/toolkit.json +0 -48
  81. package/dist/i18n/translations/en/tools.json +0 -103
  82. package/dist/i18n/translations/es/common.json +0 -36
  83. package/dist/i18n/translations/es/toolkit.json +0 -48
  84. package/dist/i18n/translations/es/tools.json +0 -103
  85. package/dist/i18n/translations/zh/common.json +0 -36
  86. package/dist/i18n/translations/zh/toolkit.json +0 -48
  87. package/dist/i18n/translations/zh/tools.json +0 -103
@@ -22,59 +22,30 @@
22
22
  * PIE element paints into its own light DOM (e.g. a `pie-passage`'s
23
23
  * model-driven content) get the same scrollable affordance even though
24
24
  * they never appeared in the authored markup string.
25
+ *
26
+ * The wrapping itself lives in `./wrap-overwide.js`, shared with the table
27
+ * wrapper: only the four values below and the accessible name differ.
25
28
  */
26
- const SCROLL_WRAPPER_CLASS = "pie-image-scroll";
27
- const PIE_CUSTOM_ELEMENT_TAG_REGEX = /^pie-/i;
28
- function isInsidePieCustomElement(image, root) {
29
- let ancestor = image.parentElement;
30
- while (ancestor && ancestor !== root) {
31
- if (PIE_CUSTOM_ELEMENT_TAG_REGEX.test(ancestor.tagName)) {
32
- return true;
33
- }
34
- ancestor = ancestor.parentElement;
35
- }
36
- return false;
37
- }
29
+ import { wrapOverwideInElement, wrapOverwideMarkup, } from "./wrap-overwide.js";
38
30
  function buildAriaLabel(image) {
39
31
  const alt = image.getAttribute("alt");
40
32
  const trimmed = alt ? alt.trim() : "";
41
33
  return trimmed ? `Scrollable image: ${trimmed}` : "Scrollable image";
42
34
  }
35
+ const IMAGE_SPEC = {
36
+ selector: "img",
37
+ wrapperTag: "span",
38
+ wrapperClass: "pie-image-scroll",
39
+ markupProbe: /<img\b/i,
40
+ buildAriaLabel,
41
+ };
43
42
  /**
44
43
  * Wrap every unwrapped `<img>` descendant of `root` with the shared
45
44
  * horizontal-scroll span. Returns the number of newly-wrapped images so
46
45
  * callers can short-circuit when nothing changed. Idempotent.
47
46
  */
48
47
  export function wrapOverwideImagesInElement(root, options = {}) {
49
- const { skipPieDescendants = false } = options;
50
- const images = Array.from(root.querySelectorAll("img"));
51
- if (images.length === 0)
52
- return 0;
53
- const ownerDocument = root.ownerDocument;
54
- if (!ownerDocument)
55
- return 0;
56
- let wrapped = 0;
57
- for (const image of images) {
58
- const parent = image.parentElement;
59
- if (!parent)
60
- continue;
61
- // Idempotency — already wrapped.
62
- if (parent.classList && parent.classList.contains(SCROLL_WRAPPER_CLASS)) {
63
- continue;
64
- }
65
- // Authored-markup pass: leave PIE custom-element internals alone.
66
- if (skipPieDescendants && isInsidePieCustomElement(image, root))
67
- continue;
68
- const wrapper = ownerDocument.createElement("span");
69
- wrapper.className = SCROLL_WRAPPER_CLASS;
70
- wrapper.setAttribute("tabindex", "0");
71
- wrapper.setAttribute("role", "region");
72
- wrapper.setAttribute("aria-label", buildAriaLabel(image));
73
- parent.insertBefore(wrapper, image);
74
- wrapper.appendChild(image);
75
- wrapped += 1;
76
- }
77
- return wrapped;
48
+ return wrapOverwideInElement(root, IMAGE_SPEC, options);
78
49
  }
79
50
  /**
80
51
  * Wrap `<img>` elements in `markup` with a horizontal-scroll container.
@@ -91,25 +62,5 @@ export function wrapOverwideImagesInElement(root, options = {}) {
91
62
  * to wrap element-rendered images.
92
63
  */
93
64
  export function wrapOverwideImages(markup) {
94
- if (!markup)
95
- return "";
96
- // Fast path: avoid the DOM round-trip entirely when the markup carries no
97
- // images. Keeps the sanitize pipeline cheap for the common case.
98
- if (!/<img\b/i.test(markup))
99
- return markup;
100
- if (typeof window === "undefined" || !window.document)
101
- return markup;
102
- const ParserCtor = typeof DOMParser !== "undefined"
103
- ? DOMParser
104
- : window.DOMParser;
105
- if (!ParserCtor)
106
- return markup;
107
- const doc = new ParserCtor().parseFromString(`<!DOCTYPE html><html><body>${markup}</body></html>`, "text/html");
108
- const body = doc.body;
109
- if (!body)
110
- return markup;
111
- const wrapped = wrapOverwideImagesInElement(body, {
112
- skipPieDescendants: true,
113
- });
114
- return wrapped > 0 ? body.innerHTML : markup;
65
+ return wrapOverwideMarkup(markup, IMAGE_SPEC);
115
66
  }
@@ -22,17 +22,12 @@
22
22
  * PIE element paints into its own light DOM (e.g. a `pie-passage`'s
23
23
  * model-driven content) get the same scrollable affordance even though
24
24
  * they never appeared in the authored markup string.
25
+ *
26
+ * The wrapping itself lives in `./wrap-overwide.js`, shared with the image
27
+ * wrapper: only the four values below and the accessible name differ.
25
28
  */
26
- export interface WrapOverwideTablesInElementOptions {
27
- /**
28
- * When `true`, tables whose nearest `pie-*` ancestor is *strictly between*
29
- * the table and `root` are left alone. Used by the string pipeline so the
30
- * authored-markup pass doesn't restructure a PIE element's own template.
31
- * Defaults to `false` so the live-DOM pass *does* wrap element-rendered
32
- * tables.
33
- */
34
- skipPieDescendants?: boolean;
35
- }
29
+ import { type WrapOverwideOptions } from "./wrap-overwide.js";
30
+ export type WrapOverwideTablesInElementOptions = WrapOverwideOptions;
36
31
  /**
37
32
  * Wrap every unwrapped `<table>` descendant of `root` with the shared
38
33
  * horizontal-scroll div. Returns the number of newly-wrapped tables so
@@ -22,19 +22,11 @@
22
22
  * PIE element paints into its own light DOM (e.g. a `pie-passage`'s
23
23
  * model-driven content) get the same scrollable affordance even though
24
24
  * they never appeared in the authored markup string.
25
+ *
26
+ * The wrapping itself lives in `./wrap-overwide.js`, shared with the image
27
+ * wrapper: only the four values below and the accessible name differ.
25
28
  */
26
- const SCROLL_WRAPPER_CLASS = "pie-table-scroll";
27
- const PIE_CUSTOM_ELEMENT_TAG_REGEX = /^pie-/i;
28
- function isInsidePieCustomElement(table, root) {
29
- let ancestor = table.parentElement;
30
- while (ancestor && ancestor !== root) {
31
- if (PIE_CUSTOM_ELEMENT_TAG_REGEX.test(ancestor.tagName)) {
32
- return true;
33
- }
34
- ancestor = ancestor.parentElement;
35
- }
36
- return false;
37
- }
29
+ import { wrapOverwideInElement, wrapOverwideMarkup, } from "./wrap-overwide.js";
38
30
  function buildAriaLabel(table) {
39
31
  // Authors commonly label tables via <caption>, aria-label, or aria-labelledby.
40
32
  // Prefer the most explicit signal and fall back to the generic label so
@@ -65,62 +57,21 @@ function buildAriaLabel(table) {
65
57
  }
66
58
  return "Scrollable table";
67
59
  }
60
+ const TABLE_SPEC = {
61
+ selector: "table",
62
+ wrapperTag: "div",
63
+ wrapperClass: "pie-table-scroll",
64
+ markupProbe: /<table\b/i,
65
+ buildAriaLabel,
66
+ };
68
67
  /**
69
68
  * Wrap every unwrapped `<table>` descendant of `root` with the shared
70
69
  * horizontal-scroll div. Returns the number of newly-wrapped tables so
71
70
  * callers can short-circuit when nothing changed. Idempotent.
72
71
  */
73
72
  export function wrapOverwideTablesInElement(root, options = {}) {
74
- const { skipPieDescendants = false } = options;
75
- const tables = Array.from(root.querySelectorAll("table"));
76
- if (tables.length === 0)
77
- return 0;
78
- const ownerDocument = root.ownerDocument;
79
- if (!ownerDocument)
80
- return 0;
81
- let wrapped = 0;
82
- for (const table of tables) {
83
- const parent = table.parentElement;
84
- if (!parent)
85
- continue;
86
- // Idempotency — already wrapped.
87
- if (parent.classList && parent.classList.contains(SCROLL_WRAPPER_CLASS)) {
88
- continue;
89
- }
90
- // Authored-markup pass: leave PIE custom-element internals alone.
91
- if (skipPieDescendants && isInsidePieCustomElement(table, root))
92
- continue;
93
- const wrapper = ownerDocument.createElement("div");
94
- wrapper.className = SCROLL_WRAPPER_CLASS;
95
- wrapper.setAttribute("tabindex", "0");
96
- wrapper.setAttribute("role", "region");
97
- wrapper.setAttribute("aria-label", buildAriaLabel(table));
98
- parent.insertBefore(wrapper, table);
99
- wrapper.appendChild(table);
100
- wrapped += 1;
101
- }
102
- return wrapped;
73
+ return wrapOverwideInElement(root, TABLE_SPEC, options);
103
74
  }
104
75
  export function wrapOverwideTables(markup) {
105
- if (!markup)
106
- return "";
107
- // Fast path: avoid the DOM round-trip entirely when the markup carries no
108
- // tables. Keeps the sanitize pipeline cheap for the common case.
109
- if (!/<table\b/i.test(markup))
110
- return markup;
111
- if (typeof window === "undefined" || !window.document)
112
- return markup;
113
- const ParserCtor = typeof DOMParser !== "undefined"
114
- ? DOMParser
115
- : window.DOMParser;
116
- if (!ParserCtor)
117
- return markup;
118
- const doc = new ParserCtor().parseFromString(`<!DOCTYPE html><html><body>${markup}</body></html>`, "text/html");
119
- const body = doc.body;
120
- if (!body)
121
- return markup;
122
- const wrapped = wrapOverwideTablesInElement(body, {
123
- skipPieDescendants: true,
124
- });
125
- return wrapped > 0 ? body.innerHTML : markup;
76
+ return wrapOverwideMarkup(markup, TABLE_SPEC);
126
77
  }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The wrapping engine behind `wrapOverwideImages` and `wrapOverwideTables`.
3
+ *
4
+ * Both exist for the same reason — content wider than its column must surface a
5
+ * scrollbar rather than be clipped by an ancestor `overflow-x: hidden` (PIE-94,
6
+ * WCAG 1.4.10 Reflow at 400% zoom) — and both wrap the same way: find the
7
+ * unwrapped nodes, skip a PIE element's own template on the authored-markup pass,
8
+ * and insert a focusable `role="region"` wrapper carrying the CSS from
9
+ * `@pie-players/pie-theme`.
10
+ *
11
+ * What genuinely differs per content kind is declared in {@link OverwideWrapSpec}
12
+ * and nothing else: the selector, the wrapper's tag and class, the fast-path probe
13
+ * on the markup string, and how a region announces itself. Accessible naming is
14
+ * the real divergence — an image has `alt`, a table has `<caption>`,
15
+ * `aria-label` and `aria-labelledby` — which is why it is a function rather than
16
+ * a template.
17
+ */
18
+ /** What one content kind needs in order to be wrapped. */
19
+ export interface OverwideWrapSpec {
20
+ /** Selector for the nodes to wrap, e.g. `"img"`. */
21
+ selector: string;
22
+ /** Tag of the inserted wrapper — inline for images, block for tables. */
23
+ wrapperTag: string;
24
+ /** Class the theme package styles, e.g. `"pie-image-scroll"`. */
25
+ wrapperClass: string;
26
+ /** Cheap test for "this markup could contain the nodes at all". */
27
+ markupProbe: RegExp;
28
+ /** The wrapper's accessible name for one node. */
29
+ buildAriaLabel: (node: Element) => string;
30
+ }
31
+ export interface WrapOverwideOptions {
32
+ /**
33
+ * When `true`, nodes whose nearest `pie-*` ancestor is *strictly between* the
34
+ * node and `root` are left alone. Used by the string pipeline so the
35
+ * authored-markup pass doesn't restructure a PIE element's own template.
36
+ * Defaults to `false` so the live-DOM pass *does* wrap element-rendered nodes.
37
+ */
38
+ skipPieDescendants?: boolean;
39
+ }
40
+ /**
41
+ * Wrap every unwrapped match under `root`. Returns the number newly wrapped so
42
+ * callers can short-circuit when nothing changed. Idempotent.
43
+ */
44
+ export declare function wrapOverwideInElement(root: Element, spec: OverwideWrapSpec, options?: WrapOverwideOptions): number;
45
+ /**
46
+ * String-in / string-out wrapping, for the post-sanitization step.
47
+ *
48
+ * No-ops on empty input, and during SSR (no `window` / `DOMParser`) — the markup
49
+ * is returned unchanged and the browser re-run on hydrate performs the wrap.
50
+ */
51
+ export declare function wrapOverwideMarkup(markup: string, spec: OverwideWrapSpec): string;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The wrapping engine behind `wrapOverwideImages` and `wrapOverwideTables`.
3
+ *
4
+ * Both exist for the same reason — content wider than its column must surface a
5
+ * scrollbar rather than be clipped by an ancestor `overflow-x: hidden` (PIE-94,
6
+ * WCAG 1.4.10 Reflow at 400% zoom) — and both wrap the same way: find the
7
+ * unwrapped nodes, skip a PIE element's own template on the authored-markup pass,
8
+ * and insert a focusable `role="region"` wrapper carrying the CSS from
9
+ * `@pie-players/pie-theme`.
10
+ *
11
+ * What genuinely differs per content kind is declared in {@link OverwideWrapSpec}
12
+ * and nothing else: the selector, the wrapper's tag and class, the fast-path probe
13
+ * on the markup string, and how a region announces itself. Accessible naming is
14
+ * the real divergence — an image has `alt`, a table has `<caption>`,
15
+ * `aria-label` and `aria-labelledby` — which is why it is a function rather than
16
+ * a template.
17
+ */
18
+ const PIE_CUSTOM_ELEMENT_TAG_REGEX = /^pie-/i;
19
+ function isInsidePieCustomElement(node, root) {
20
+ let ancestor = node.parentElement;
21
+ while (ancestor && ancestor !== root) {
22
+ if (PIE_CUSTOM_ELEMENT_TAG_REGEX.test(ancestor.tagName)) {
23
+ return true;
24
+ }
25
+ ancestor = ancestor.parentElement;
26
+ }
27
+ return false;
28
+ }
29
+ /**
30
+ * Wrap every unwrapped match under `root`. Returns the number newly wrapped so
31
+ * callers can short-circuit when nothing changed. Idempotent.
32
+ */
33
+ export function wrapOverwideInElement(root, spec, options = {}) {
34
+ const { skipPieDescendants = false } = options;
35
+ const nodes = Array.from(root.querySelectorAll(spec.selector));
36
+ if (nodes.length === 0)
37
+ return 0;
38
+ const ownerDocument = root.ownerDocument;
39
+ if (!ownerDocument)
40
+ return 0;
41
+ let wrapped = 0;
42
+ for (const node of nodes) {
43
+ const parent = node.parentElement;
44
+ if (!parent)
45
+ continue;
46
+ // Idempotency — already wrapped.
47
+ if (parent.classList?.contains(spec.wrapperClass))
48
+ continue;
49
+ // Authored-markup pass: leave PIE custom-element internals alone.
50
+ if (skipPieDescendants && isInsidePieCustomElement(node, root))
51
+ continue;
52
+ const wrapper = ownerDocument.createElement(spec.wrapperTag);
53
+ wrapper.className = spec.wrapperClass;
54
+ wrapper.setAttribute("tabindex", "0");
55
+ wrapper.setAttribute("role", "region");
56
+ wrapper.setAttribute("aria-label", spec.buildAriaLabel(node));
57
+ parent.insertBefore(wrapper, node);
58
+ wrapper.appendChild(node);
59
+ wrapped += 1;
60
+ }
61
+ return wrapped;
62
+ }
63
+ /**
64
+ * String-in / string-out wrapping, for the post-sanitization step.
65
+ *
66
+ * No-ops on empty input, and during SSR (no `window` / `DOMParser`) — the markup
67
+ * is returned unchanged and the browser re-run on hydrate performs the wrap.
68
+ */
69
+ export function wrapOverwideMarkup(markup, spec) {
70
+ if (!markup)
71
+ return "";
72
+ // Fast path: avoid the DOM round-trip entirely when the markup carries none of
73
+ // these nodes. Keeps the sanitize pipeline cheap for the common case.
74
+ if (!spec.markupProbe.test(markup))
75
+ return markup;
76
+ if (typeof window === "undefined" || !window.document)
77
+ return markup;
78
+ const ParserCtor = typeof DOMParser !== "undefined"
79
+ ? DOMParser
80
+ : window.DOMParser;
81
+ if (!ParserCtor)
82
+ return markup;
83
+ const doc = new ParserCtor().parseFromString(`<!DOCTYPE html><html><body>${markup}</body></html>`, "text/html");
84
+ const body = doc.body;
85
+ if (!body)
86
+ return markup;
87
+ const wrapped = wrapOverwideInElement(body, spec, {
88
+ skipPieDescendants: true,
89
+ });
90
+ return wrapped > 0 ? body.innerHTML : markup;
91
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Write a temp `.npmrc` carrying a scoped `_authToken` and return an env
3
+ * object pointing `NPM_CONFIG_USERCONFIG` at it, so a publish subprocess
4
+ * authenticates without a separate `npm login`. The token comes from
5
+ * `NPM_TOKEN`/`NODE_AUTH_TOKEN` in `baseEnv` first, falling back to parsing
6
+ * an `.env`-style file at `envPath` for either key.
7
+ *
8
+ * Call `cleanup()` once the subprocess using `env` has exited.
9
+ */
10
+ export declare const createNpmAuthEnvironment: (envPath?: string, baseEnv?: NodeJS.ProcessEnv) => {
11
+ env: NodeJS.ProcessEnv;
12
+ cleanup: () => void;
13
+ };
@@ -0,0 +1,64 @@
1
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ if (typeof window !== "undefined") {
5
+ throw new Error("[npm-auth-env] @pie-players/pie-players-shared/server/npm-auth-env is server-only and cannot be imported in browser code");
6
+ }
7
+ const REGISTRY = "https://registry.npmjs.org/";
8
+ const parseDotEnvToken = (envPath, key) => {
9
+ try {
10
+ const content = readFileSync(envPath, "utf8");
11
+ const pattern = new RegExp(`^\\s*(?:export\\s+)?${key}\\s*=\\s*(.*)\\s*$`, "m");
12
+ const match = content.match(pattern);
13
+ if (!match?.[1])
14
+ return "";
15
+ const rawValue = match[1].trim();
16
+ const isWrappedInQuotes = (rawValue.startsWith('"') && rawValue.endsWith('"')) ||
17
+ (rawValue.startsWith("'") && rawValue.endsWith("'"));
18
+ return (isWrappedInQuotes ? rawValue.slice(1, -1) : rawValue).trim();
19
+ }
20
+ catch {
21
+ return "";
22
+ }
23
+ };
24
+ const resolveToken = (envPath, baseEnv) => {
25
+ const envToken = String(baseEnv.NPM_TOKEN || baseEnv.NODE_AUTH_TOKEN || "").trim();
26
+ if (envToken)
27
+ return envToken;
28
+ if (!envPath)
29
+ return "";
30
+ return (parseDotEnvToken(envPath, "NPM_TOKEN") ||
31
+ parseDotEnvToken(envPath, "NODE_AUTH_TOKEN"));
32
+ };
33
+ /**
34
+ * Write a temp `.npmrc` carrying a scoped `_authToken` and return an env
35
+ * object pointing `NPM_CONFIG_USERCONFIG` at it, so a publish subprocess
36
+ * authenticates without a separate `npm login`. The token comes from
37
+ * `NPM_TOKEN`/`NODE_AUTH_TOKEN` in `baseEnv` first, falling back to parsing
38
+ * an `.env`-style file at `envPath` for either key.
39
+ *
40
+ * Call `cleanup()` once the subprocess using `env` has exited.
41
+ */
42
+ export const createNpmAuthEnvironment = (envPath, baseEnv = process.env) => {
43
+ const token = resolveToken(envPath, baseEnv);
44
+ if (!token) {
45
+ return {
46
+ env: baseEnv,
47
+ cleanup: () => { },
48
+ };
49
+ }
50
+ const tempDir = mkdtempSync(join(tmpdir(), "pie-npm-auth-"));
51
+ const npmrcPath = join(tempDir, ".npmrc");
52
+ writeFileSync(npmrcPath, `registry=${REGISTRY}\n//registry.npmjs.org/:_authToken=${token}\n`, "utf8");
53
+ return {
54
+ env: {
55
+ ...baseEnv,
56
+ NPM_CONFIG_USERCONFIG: npmrcPath,
57
+ NODE_AUTH_TOKEN: String(baseEnv.NODE_AUTH_TOKEN || "").trim() || token,
58
+ NPM_TOKEN: String(baseEnv.NPM_TOKEN || "").trim() || token,
59
+ },
60
+ cleanup: () => {
61
+ rmSync(tempDir, { recursive: true, force: true });
62
+ },
63
+ };
64
+ };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Timed-media sections: cue policy, playback policy, and the Media Time Source
3
+ * port.
4
+ *
5
+ * Pure apart from one adapter factory that is inert until called, so the contract
6
+ * is testable without a browser and an adapter can import it without pulling in a
7
+ * player. Live cue state belongs to `SectionController` in
8
+ * `@pie-players/pie-section-player`, which is where the equivalent formative state
9
+ * lives — one rollup over one item set, in one package.
10
+ *
11
+ * Contract: `docs/prds/timed-media-section-contract.md`.
12
+ */
13
+ export type { MediaTimeRanges, MediaTimeSource, MediaTimeSourceCapabilities, MediaTimeSourceNotification, ResolvedTimedMediaCue, ResolvedTimedMediaSectionData, TimedMediaCue, TimedMediaCueActivation, TimedMediaCuePolicy, TimedMediaDegradation, TimedMediaDeliveryState, TimedMediaEffects, TimedMediaEnforcement, TimedMediaGateCondition, TimedMediaGateView, TimedMediaInput, TimedMediaItemTryBudget, TimedMediaPlaybackPolicy, TimedMediaReduction, TimedMediaScoringPolicy, TimedMediaSectionData, TimedMediaSectionProjection, TimedMediaSectionSessionSlice, TimedMediaUnknownCorrectness, TimedMediaValidationError, TimedMediaValidationResult, } from "./types.js";
14
+ export { TIMED_MEDIA_PLAYBACK_POLICY_DEFAULTS, normalizeTimedMediaSectionData, } from "./policy.js";
15
+ export { TIMED_MEDIA_SLICE_VERSION, createTimedMediaState, reduceTimedMediaState, resolveTimedMediaEnforcement, resolveTimedMediaProjection, timedMediaProjectionSignature, } from "./state.js";
16
+ export { normalizeTimedMediaSectionSlice, toTimedMediaSectionSlice, } from "./session.js";
17
+ export { NATIVE_MEDIA_CAPABILITIES, createMediaElementTimeSource, findMediaElement, type MediaElementTimeSourceOptions, } from "./media-element-source.js";
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Timed-media sections: cue policy, playback policy, and the Media Time Source
3
+ * port.
4
+ *
5
+ * Pure apart from one adapter factory that is inert until called, so the contract
6
+ * is testable without a browser and an adapter can import it without pulling in a
7
+ * player. Live cue state belongs to `SectionController` in
8
+ * `@pie-players/pie-section-player`, which is where the equivalent formative state
9
+ * lives — one rollup over one item set, in one package.
10
+ *
11
+ * Contract: `docs/prds/timed-media-section-contract.md`.
12
+ */
13
+ export { TIMED_MEDIA_PLAYBACK_POLICY_DEFAULTS, normalizeTimedMediaSectionData, } from "./policy.js";
14
+ export { TIMED_MEDIA_SLICE_VERSION, createTimedMediaState, reduceTimedMediaState, resolveTimedMediaEnforcement, resolveTimedMediaProjection, timedMediaProjectionSignature, } from "./state.js";
15
+ export { normalizeTimedMediaSectionSlice, toTimedMediaSectionSlice, } from "./session.js";
16
+ export { NATIVE_MEDIA_CAPABILITIES, createMediaElementTimeSource, findMediaElement, } from "./media-element-source.js";
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The native `<video>` / `<audio>` adapter for the Media Time Source port.
3
+ *
4
+ * Short by design. The port is shaped after `HTMLMediaElement` precisely so that
5
+ * the browser's own media element satisfies it in a few lines, which is what
6
+ * makes the port testable and the eventual media-player dependency reversible
7
+ * rather than load-bearing.
8
+ *
9
+ * Touches no DOM until called, so this module stays importable in Node beside the
10
+ * rest of the pure timed-media contract.
11
+ */
12
+ import type { MediaTimeSource, MediaTimeSourceCapabilities } from "./types.js";
13
+ /**
14
+ * A native media element can be paused and its position can be written, so both
15
+ * capabilities hold. A host wrapping something that only *looks* native — an
16
+ * embed behind a facade, a remote-controlled player — overrides them rather than
17
+ * letting the section assume control it does not have.
18
+ */
19
+ export declare const NATIVE_MEDIA_CAPABILITIES: MediaTimeSourceCapabilities;
20
+ export interface MediaElementTimeSourceOptions {
21
+ capabilities?: Partial<MediaTimeSourceCapabilities>;
22
+ }
23
+ /** The subset of `HTMLMediaElement` this adapter uses. */
24
+ type MediaElementLike = Pick<HTMLMediaElement, "currentTime" | "duration" | "paused" | "seekable" | "play" | "pause" | "addEventListener" | "removeEventListener">;
25
+ export declare function createMediaElementTimeSource(element: MediaElementLike, options?: MediaElementTimeSourceOptions): MediaTimeSource;
26
+ /**
27
+ * Find the media element a stimulus renderable mounted.
28
+ *
29
+ * Deliberately a plain query rather than a contract on what the stimulus is: a
30
+ * passage config mounting `<pie-video-stimulus>`, and one carrying authored
31
+ * `<video>` markup, both end up with a media element in the card's subtree, and
32
+ * the section needs no way to tell them apart. An element that wants to supply its
33
+ * own port registers one instead.
34
+ */
35
+ export declare function findMediaElement(root: ParentNode | null): HTMLMediaElement | null;
36
+ export {};
@@ -0,0 +1,121 @@
1
+ /**
2
+ * The native `<video>` / `<audio>` adapter for the Media Time Source port.
3
+ *
4
+ * Short by design. The port is shaped after `HTMLMediaElement` precisely so that
5
+ * the browser's own media element satisfies it in a few lines, which is what
6
+ * makes the port testable and the eventual media-player dependency reversible
7
+ * rather than load-bearing.
8
+ *
9
+ * Touches no DOM until called, so this module stays importable in Node beside the
10
+ * rest of the pure timed-media contract.
11
+ */
12
+ /**
13
+ * A native media element can be paused and its position can be written, so both
14
+ * capabilities hold. A host wrapping something that only *looks* native — an
15
+ * embed behind a facade, a remote-controlled player — overrides them rather than
16
+ * letting the section assume control it does not have.
17
+ */
18
+ export const NATIVE_MEDIA_CAPABILITIES = {
19
+ canPause: true,
20
+ canRestrictSeeking: true,
21
+ };
22
+ const NOTIFYING_EVENTS = [
23
+ { name: "timeupdate", type: "time" },
24
+ // Both halves of a seek are reported. `seeking` is where a clamp can still
25
+ // land before the frame paints at the new position; `seeked` is the one that
26
+ // always fires. The reduction is idempotent for an already-legal position, so
27
+ // reporting twice costs nothing.
28
+ { name: "seeking", type: "seek" },
29
+ { name: "seeked", type: "seek" },
30
+ { name: "play", type: "play" },
31
+ { name: "pause", type: "pause" },
32
+ { name: "ended", type: "ended" },
33
+ ];
34
+ export function createMediaElementTimeSource(element, options = {}) {
35
+ const capabilities = {
36
+ ...NATIVE_MEDIA_CAPABILITIES,
37
+ ...(options.capabilities ?? {}),
38
+ };
39
+ const listeners = new Set();
40
+ function currentTime() {
41
+ const value = element.currentTime;
42
+ return Number.isFinite(value) && value >= 0 ? value : 0;
43
+ }
44
+ function notify(type) {
45
+ const notification = { type, currentTime: currentTime() };
46
+ for (const listener of Array.from(listeners)) {
47
+ try {
48
+ listener(notification);
49
+ }
50
+ catch {
51
+ // A subscriber that throws must not stop the others or break playback:
52
+ // this runs on every `timeupdate`.
53
+ }
54
+ }
55
+ }
56
+ const handlers = NOTIFYING_EVENTS.map((entry) => ({
57
+ name: entry.name,
58
+ handler: () => notify(entry.type),
59
+ }));
60
+ return {
61
+ get currentTime() {
62
+ return currentTime();
63
+ },
64
+ get duration() {
65
+ // `NaN` before metadata loads, as on the element itself. Callers that
66
+ // need a real duration wait for it rather than reading a zero that looks
67
+ // like a finished video.
68
+ return element.duration;
69
+ },
70
+ get paused() {
71
+ return element.paused;
72
+ },
73
+ get seekable() {
74
+ return element.seekable ?? null;
75
+ },
76
+ capabilities,
77
+ play() {
78
+ return element.play();
79
+ },
80
+ pause() {
81
+ element.pause();
82
+ },
83
+ seekTo(seconds) {
84
+ if (!Number.isFinite(seconds) || seconds < 0)
85
+ return;
86
+ element.currentTime = seconds;
87
+ },
88
+ subscribe(listener) {
89
+ listeners.add(listener);
90
+ if (listeners.size === 1) {
91
+ for (const entry of handlers) {
92
+ element.addEventListener(entry.name, entry.handler);
93
+ }
94
+ }
95
+ return () => {
96
+ if (!listeners.delete(listener))
97
+ return;
98
+ if (listeners.size > 0)
99
+ return;
100
+ for (const entry of handlers) {
101
+ element.removeEventListener(entry.name, entry.handler);
102
+ }
103
+ };
104
+ },
105
+ };
106
+ }
107
+ /**
108
+ * Find the media element a stimulus renderable mounted.
109
+ *
110
+ * Deliberately a plain query rather than a contract on what the stimulus is: a
111
+ * passage config mounting `<pie-video-stimulus>`, and one carrying authored
112
+ * `<video>` markup, both end up with a media element in the card's subtree, and
113
+ * the section needs no way to tell them apart. An element that wants to supply its
114
+ * own port registers one instead.
115
+ */
116
+ export function findMediaElement(root) {
117
+ if (!root)
118
+ return null;
119
+ const found = root.querySelector("video, audio");
120
+ return found ?? null;
121
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Validation for authored timed-media section data.
3
+ *
4
+ * Authored, wire-facing and untrusted, so every field is checked here and the
5
+ * caller never hand-parses. Errors are returned rather than thrown: the section
6
+ * reports them as a framework error and delivers as an ordinary section, which is
7
+ * loud without costing the learner the content.
8
+ */
9
+ import type { TimedMediaItemTryBudget, TimedMediaPlaybackPolicy, TimedMediaValidationResult } from "./types.js";
10
+ /**
11
+ * Applied when `playbackPolicy` is absent entirely. The restrictive values are
12
+ * the defaults because a timed-media section is authored to sequence: a section
13
+ * that forgot the policy block reads as one that wanted sequencing, and the
14
+ * permissive reading would silently deliver an unsequenced video.
15
+ */
16
+ export declare const TIMED_MEDIA_PLAYBACK_POLICY_DEFAULTS: TimedMediaPlaybackPolicy;
17
+ /**
18
+ * Validate authored `timedMedia` against the section it belongs to.
19
+ *
20
+ * `resolveStimulusRenderableId` is supplied by the caller because the mapping
21
+ * from an authored `stimulusRef` to a rendered renderable id belongs to the
22
+ * content service that assigned that id — deriving it twice is two
23
+ * implementations of one mapping.
24
+ */
25
+ export declare function normalizeTimedMediaSectionData(args: {
26
+ timedMedia: unknown;
27
+ /** Canonical identifiers of the section's item refs. */
28
+ itemIdentifiers: readonly string[];
29
+ resolveStimulusRenderableId: (stimulusRef: string) => string | undefined;
30
+ /**
31
+ * The Try budget each item's resolved formative policy leaves, so a gate on
32
+ * correctness can be refused where it would strand the learner. Supplied by the
33
+ * caller because formative policy resolution is that contract's, not this one's.
34
+ */
35
+ resolveItemTryBudget: (itemIdentifier: string) => TimedMediaItemTryBudget;
36
+ }): TimedMediaValidationResult;