@pie-players/pie-players-shared 0.3.67 → 0.3.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -0
- package/dist/formative/index.d.ts +16 -0
- package/dist/formative/index.js +5 -0
- package/dist/formative/mastery.d.ts +16 -0
- package/dist/formative/mastery.js +43 -0
- package/dist/formative/outcome.d.ts +19 -0
- package/dist/formative/outcome.js +90 -0
- package/dist/formative/policy.d.ts +37 -0
- package/dist/formative/policy.js +86 -0
- package/dist/formative/session.d.ts +19 -0
- package/dist/formative/session.js +113 -0
- package/dist/formative/state.d.ts +60 -0
- package/dist/formative/state.js +140 -0
- package/dist/formative/types.d.ts +164 -0
- package/dist/formative/types.js +13 -0
- package/dist/i18n/catalogs.d.ts +38 -0
- package/dist/i18n/catalogs.js +47 -0
- package/dist/i18n/index.d.ts +28 -4
- package/dist/i18n/index.js +34 -3
- package/dist/i18n/language-tags.d.ts +51 -0
- package/dist/i18n/language-tags.js +84 -0
- package/dist/i18n/messages/en-US.d.ts +601 -0
- package/dist/i18n/messages/en-US.js +617 -0
- package/dist/i18n/messages/nl-NL.d.ts +516 -0
- package/dist/i18n/messages/nl-NL.js +516 -0
- package/dist/i18n/provider.d.ts +168 -0
- package/dist/i18n/provider.js +472 -0
- package/dist/i18n/types.d.ts +177 -28
- package/dist/i18n/types.js +6 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/pie/custom-element-define.d.ts +24 -0
- package/dist/pie/custom-element-define.js +36 -6
- package/dist/pie/index.d.ts +1 -1
- package/dist/pie/index.js +1 -1
- package/dist/pie/versioned-tag.js +3 -2
- package/dist/security/sanitize-forbidden-lists.d.ts +8 -0
- package/dist/security/sanitize-forbidden-lists.js +41 -0
- package/dist/security/sanitize-item-markup.js +3 -36
- package/dist/security/sanitize-svg-icon.js +3 -25
- package/dist/security/wrap-overwide-images.d.ts +5 -10
- package/dist/security/wrap-overwide-images.js +13 -62
- package/dist/security/wrap-overwide-tables.d.ts +5 -10
- package/dist/security/wrap-overwide-tables.js +13 -62
- package/dist/security/wrap-overwide.d.ts +51 -0
- package/dist/security/wrap-overwide.js +91 -0
- package/dist/server/npm-auth-env.d.ts +13 -0
- package/dist/server/npm-auth-env.js +64 -0
- package/dist/timed-media/index.d.ts +17 -0
- package/dist/timed-media/index.js +16 -0
- package/dist/timed-media/media-element-source.d.ts +36 -0
- package/dist/timed-media/media-element-source.js +121 -0
- package/dist/timed-media/policy.d.ts +36 -0
- package/dist/timed-media/policy.js +297 -0
- package/dist/timed-media/session.d.ts +21 -0
- package/dist/timed-media/session.js +85 -0
- package/dist/timed-media/state.d.ts +66 -0
- package/dist/timed-media/state.js +357 -0
- package/dist/timed-media/types.d.ts +343 -0
- package/dist/timed-media/types.js +12 -0
- package/dist/tools/term-lookup.d.ts +189 -0
- package/dist/tools/term-lookup.js +284 -0
- package/dist/types/index.d.ts +105 -1
- package/dist/ui/attribute-coercion.d.ts +1 -0
- package/dist/ui/attribute-coercion.js +19 -0
- package/dist/ui/first-focusable.d.ts +22 -0
- package/dist/ui/first-focusable.js +47 -0
- package/dist/ui/focus-trap.js +7 -2
- package/dist/ui/pointer-drag.d.ts +30 -0
- package/dist/ui/pointer-drag.js +34 -0
- package/package.json +35 -3
- package/dist/i18n/loader.d.ts +0 -35
- package/dist/i18n/loader.js +0 -132
- package/dist/i18n/simple-i18n.d.ts +0 -68
- package/dist/i18n/simple-i18n.js +0 -198
- package/dist/i18n/translations/ar/common.json +0 -36
- package/dist/i18n/translations/ar/toolkit.json +0 -48
- package/dist/i18n/translations/ar/tools.json +0 -103
- package/dist/i18n/translations/en/common.json +0 -36
- package/dist/i18n/translations/en/toolkit.json +0 -48
- package/dist/i18n/translations/en/tools.json +0 -103
- package/dist/i18n/translations/es/common.json +0 -36
- package/dist/i18n/translations/es/toolkit.json +0 -48
- package/dist/i18n/translations/es/tools.json +0 -103
- package/dist/i18n/translations/zh/common.json +0 -36
- package/dist/i18n/translations/zh/toolkit.json +0 -48
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|