@pie-players/pie-players-shared 0.3.74 → 1.0.0-beta.0
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 +88 -75
- package/dist/components/vendor/nds/README.md +4 -5
- package/dist/config/profile.d.ts +1 -1
- package/dist/config/profile.js +1 -1
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.js +1 -1
- package/dist/i18n/language-tags.d.ts +9 -1
- package/dist/i18n/language-tags.js +11 -1
- package/dist/i18n/messages/en-US.d.ts +33 -28
- package/dist/i18n/messages/en-US.js +34 -29
- package/dist/i18n/messages/nl-NL.d.ts +33 -28
- package/dist/i18n/messages/nl-NL.js +34 -29
- package/dist/i18n/provider.d.ts +14 -7
- package/dist/i18n/provider.js +26 -29
- package/dist/i18n/types.d.ts +4 -5
- package/dist/index.d.ts +1 -1
- package/dist/index.js +0 -1
- package/dist/instrumentation/providers/BaseInstrumentationProvider.js +0 -14
- package/dist/loader-config.d.ts +0 -2
- package/dist/loader-config.js +0 -2
- package/dist/loaders/element-loader.js +13 -6
- package/dist/loaders/esm-adapter.d.ts +11 -0
- package/dist/loaders/esm-adapter.js +74 -45
- package/dist/loaders/iife-adapter.d.ts +3 -3
- package/dist/loaders/iife-adapter.js +36 -35
- package/dist/loaders/index.d.ts +4 -2
- package/dist/loaders/index.js +2 -1
- package/dist/loaders/mathjax-events.d.ts +22 -0
- package/dist/loaders/mathjax-events.js +65 -0
- package/dist/loaders/preloaded-registration.d.ts +18 -2
- package/dist/loaders/preloaded-registration.js +14 -2
- package/dist/media/index.d.ts +4 -6
- package/dist/media/index.js +6 -8
- package/dist/object/index.d.ts +8 -3
- package/dist/object/index.js +8 -6
- package/dist/pie/asset-handler.d.ts +0 -17
- package/dist/pie/asset-handler.js +1 -1
- package/dist/pie/authoring.js +5 -7
- package/dist/pie/elements-rendered.d.ts +23 -0
- package/dist/pie/elements-rendered.js +96 -0
- package/dist/pie/index.d.ts +4 -6
- package/dist/pie/index.js +4 -7
- package/dist/pie/initialization.d.ts +13 -14
- package/dist/pie/initialization.js +61 -75
- package/dist/pie/instrumentation-event-bridge.js +23 -1
- package/dist/pie/instrumentation-event-map.js +11 -18
- package/dist/pie/item-controller.js +4 -0
- package/dist/pie/item-session-contract.js +7 -9
- package/dist/pie/logger.d.ts +6 -0
- package/dist/pie/logger.js +10 -0
- package/dist/pie/math-assets.d.ts +48 -0
- package/dist/pie/math-assets.js +81 -0
- package/dist/pie/resource-monitor.d.ts +34 -32
- package/dist/pie/resource-monitor.js +152 -231
- package/dist/pie/scoring.d.ts +1 -6
- package/dist/pie/scoring.js +3 -8
- package/dist/pie/session-commit.js +1 -1
- package/dist/pie/stages.d.ts +11 -47
- package/dist/pie/stages.js +11 -47
- package/dist/pie/updates.js +5 -4
- package/dist/pie/utils.d.ts +0 -2
- package/dist/pie/utils.js +0 -2
- package/dist/security/sanitize-item-markup.d.ts +8 -4
- package/dist/security/sanitize-item-markup.js +52 -6
- package/dist/security/sanitize-svg-icon.js +2 -2
- package/dist/security/wrap-overwide-images.d.ts +1 -1
- package/dist/security/wrap-overwide-images.js +20 -1
- package/dist/security/wrap-overwide.d.ts +8 -0
- package/dist/security/wrap-overwide.js +47 -3
- package/dist/timed-media/media-element-source.js +1 -2
- package/dist/types/index.d.ts +88 -153
- package/dist/types/index.js +2 -2
- package/dist/ui/attribute-coercion.d.ts +5 -0
- package/dist/ui/attribute-coercion.js +6 -4
- package/dist/ui/content-styles.d.ts +6 -10
- package/dist/ui/content-styles.js +8 -13
- package/dist/ui/first-focusable.d.ts +0 -1
- package/dist/ui/first-focusable.js +0 -1
- package/dist/ui/focus-trap.js +2 -3
- package/dist/ui/scope-css.d.ts +23 -26
- package/dist/ui/scope-css.js +23 -26
- package/package.json +6 -4
- package/dist/loaders/mathjax-version-conflict.d.ts +0 -17
- package/dist/loaders/mathjax-version-conflict.js +0 -51
- package/dist/pie/stage-tracker.d.ts +0 -50
- package/dist/pie/stage-tracker.js +0 -105
package/dist/pie/scoring.d.ts
CHANGED
|
@@ -20,14 +20,9 @@ export type ScorePieItemOptions = {
|
|
|
20
20
|
container?: Pick<ParentNode, "querySelector">;
|
|
21
21
|
env?: Partial<Env>;
|
|
22
22
|
models?: PieModel[];
|
|
23
|
-
/**
|
|
24
|
-
* Existing callers keep the historical scorePieItem controller call shape.
|
|
25
|
-
* provideScore() opts into the item-player score result shape explicitly.
|
|
26
|
-
*/
|
|
27
|
-
outcomeArguments?: "session-env" | "model-session-env";
|
|
28
23
|
/**
|
|
29
24
|
* Preserve one result slot per model, including undefined for missing element/controller.
|
|
30
|
-
*
|
|
25
|
+
* Missing slots are filtered out by default.
|
|
31
26
|
*/
|
|
32
27
|
includeMissingResults?: boolean;
|
|
33
28
|
/** The scoring player's bundle type; `player.js` scores nothing locally. */
|
package/dist/pie/scoring.js
CHANGED
|
@@ -62,13 +62,9 @@ const findRenderedPieElement = (root, modelId) => {
|
|
|
62
62
|
return (root.querySelector(`[id="${escapedId}"]`) ||
|
|
63
63
|
root.querySelector(`[pie-id="${escapedId}"]`));
|
|
64
64
|
};
|
|
65
|
-
const invokeOutcome = async (controller, model, session, env
|
|
66
|
-
if (argumentShape === "model-session-env") {
|
|
67
|
-
const outcome = controller.outcome;
|
|
68
|
-
return outcome.call(controller, model, session, env);
|
|
69
|
-
}
|
|
65
|
+
const invokeOutcome = async (controller, model, session, env) => {
|
|
70
66
|
const outcome = controller.outcome;
|
|
71
|
-
return outcome.call(controller, session, env);
|
|
67
|
+
return outcome.call(controller, model, session, env);
|
|
72
68
|
};
|
|
73
69
|
/**
|
|
74
70
|
* Score a PIE item by calling outcome on all PIE elements
|
|
@@ -76,7 +72,6 @@ const invokeOutcome = async (controller, model, session, env, argumentShape) =>
|
|
|
76
72
|
export async function scorePieItem(config, sessionData, options = {}) {
|
|
77
73
|
const root = options.container ?? document;
|
|
78
74
|
const models = options.models ?? config.models ?? [];
|
|
79
|
-
const outcomeArguments = options.outcomeArguments ?? "session-env";
|
|
80
75
|
const scoringEnv = {
|
|
81
76
|
...options.env,
|
|
82
77
|
mode: "evaluate",
|
|
@@ -91,7 +86,7 @@ export async function scorePieItem(config, sessionData, options = {}) {
|
|
|
91
86
|
if (controller?.outcome) {
|
|
92
87
|
return {
|
|
93
88
|
...session,
|
|
94
|
-
...(await invokeOutcome(controller, model, session, scoringEnv
|
|
89
|
+
...(await invokeOutcome(controller, model, session, scoringEnv)),
|
|
95
90
|
};
|
|
96
91
|
}
|
|
97
92
|
else {
|
|
@@ -93,7 +93,7 @@ function assignedElementsOf(element) {
|
|
|
93
93
|
* elements assigned to each `<slot>`.
|
|
94
94
|
*
|
|
95
95
|
* `querySelectorAll("*")` sees none of the last two, and both are load-bearing
|
|
96
|
-
* here. The section player mounts each item inside `<pie-item-
|
|
96
|
+
* here. The section player mounts each item inside `<pie-item-scope>`'s shadow
|
|
97
97
|
* root, and projects its item pane through a slot on the assessment toolkit -
|
|
98
98
|
* a walk that stops at `<slot>` finds every delivery element in a section
|
|
99
99
|
* unreachable.
|
package/dist/pie/stages.d.ts
CHANGED
|
@@ -1,56 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Stage vocabulary of the section player's `pie-stage-change` event.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* stage
|
|
9
|
-
*
|
|
4
|
+
* A cohort (one section and attempt) moves through `composed`,
|
|
5
|
+
* `engine-ready`, `interactive` and `disposed`, in that order. The section
|
|
6
|
+
* runtime engine in `@pie-players/pie-assessment-toolkit` is the only emitter;
|
|
7
|
+
* it dispatches the event on the layout element. `pie-loading-complete` is
|
|
8
|
+
* not a stage; it fires once per cohort when the section controller is ready
|
|
9
|
+
* and the element pre-warm has resolved. `readiness.mode = "strict"` only
|
|
10
|
+
* delays `interactive`.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `runtime-bound`, and `ui-rendered` stages. The retro keeps the same
|
|
15
|
-
* shape for both the toolkit CE and the layout CEs — every CE iterates
|
|
16
|
-
* through the same list so subscribers don't need to special-case shape.
|
|
17
|
-
*
|
|
18
|
-
* Locked decisions (see `m6_ready_vocab_canonical_followup.plan.md`):
|
|
19
|
-
* - **A1** Stage machine model with one canonical event family.
|
|
20
|
-
* - **B1** "All items loaded" is reclassified as `pie-loading-complete`,
|
|
21
|
-
* not a stage. (The previous `ready` DOM event alias was removed in
|
|
22
|
-
* the broad architecture review compat sweep.)
|
|
23
|
-
* - **C1** `readiness.mode = "strict"` only delays the `interactive`
|
|
24
|
-
* transition; every earlier stage fires identically across modes.
|
|
25
|
-
* - **D1** One DOM event family `pie-stage-change` with the stage in the
|
|
26
|
-
* payload, mirroring the M3 `framework-error` idiom.
|
|
27
|
-
*
|
|
28
|
-
* Failure semantics deliberately stay in the M3 framework-error contract —
|
|
29
|
-
* `status: "failed"` records the position; the framework error records the
|
|
30
|
-
* cause.
|
|
12
|
+
* `status` is `entered`, or `failed` for the stage a non-recoverable
|
|
13
|
+
* framework error stopped the chain at, with `skipped` for each later stage
|
|
14
|
+
* up to `interactive`. The `framework-error` event carries the cause.
|
|
31
15
|
*/
|
|
32
16
|
export declare const STAGES: readonly ["composed", "engine-ready", "interactive", "disposed"];
|
|
33
17
|
export type Stage = (typeof STAGES)[number];
|
|
34
18
|
export type StageStatus = "entered" | "skipped" | "failed";
|
|
35
|
-
/**
|
|
36
|
-
* Subset of the source CE shape used by stage subscribers. Both shapes
|
|
37
|
-
* apply every canonical stage post-retro — the distinction stays in the
|
|
38
|
-
* type so future shape-specific stages (e.g. layout-only `paginated`) can
|
|
39
|
-
* be added without churn at the call sites.
|
|
40
|
-
*/
|
|
41
|
-
export type StageSourceCe = "toolkit" | "layout";
|
|
42
|
-
/**
|
|
43
|
-
* Stages that apply to a given CE shape. Post-retro the toolkit and the
|
|
44
|
-
* layout CEs share the same canonical list; `applicableStages` is kept as
|
|
45
|
-
* an indirection so the tracker, telemetry, and tests don't have to be
|
|
46
|
-
* rewritten when a future stage diverges between shapes.
|
|
47
|
-
*/
|
|
48
|
-
export declare function applicableStages(_sourceCe: StageSourceCe): readonly Stage[];
|
|
49
|
-
/**
|
|
50
|
-
* Strict ordering index used by the tracker to detect monotonic
|
|
51
|
-
* violations. `disposed` is last; `composed` is first.
|
|
52
|
-
*/
|
|
53
|
-
export declare function stageOrdinal(stage: Stage): number;
|
|
54
19
|
export type StageChangeDetail = {
|
|
55
20
|
stage: Stage;
|
|
56
21
|
status: StageStatus;
|
|
@@ -68,7 +33,6 @@ export type LoadingCompleteDetail = {
|
|
|
68
33
|
sectionId: string;
|
|
69
34
|
attemptId?: string;
|
|
70
35
|
itemCount: number;
|
|
71
|
-
loadedCount: number;
|
|
72
36
|
timestamp: string;
|
|
73
37
|
sourceCe: string;
|
|
74
38
|
};
|
package/dist/pie/stages.js
CHANGED
|
@@ -1,33 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Stage vocabulary of the section player's `pie-stage-change` event.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* stage
|
|
9
|
-
*
|
|
4
|
+
* A cohort (one section and attempt) moves through `composed`,
|
|
5
|
+
* `engine-ready`, `interactive` and `disposed`, in that order. The section
|
|
6
|
+
* runtime engine in `@pie-players/pie-assessment-toolkit` is the only emitter;
|
|
7
|
+
* it dispatches the event on the layout element. `pie-loading-complete` is
|
|
8
|
+
* not a stage; it fires once per cohort when the section controller is ready
|
|
9
|
+
* and the element pre-warm has resolved. `readiness.mode = "strict"` only
|
|
10
|
+
* delays `interactive`.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `runtime-bound`, and `ui-rendered` stages. The retro keeps the same
|
|
15
|
-
* shape for both the toolkit CE and the layout CEs — every CE iterates
|
|
16
|
-
* through the same list so subscribers don't need to special-case shape.
|
|
17
|
-
*
|
|
18
|
-
* Locked decisions (see `m6_ready_vocab_canonical_followup.plan.md`):
|
|
19
|
-
* - **A1** Stage machine model with one canonical event family.
|
|
20
|
-
* - **B1** "All items loaded" is reclassified as `pie-loading-complete`,
|
|
21
|
-
* not a stage. (The previous `ready` DOM event alias was removed in
|
|
22
|
-
* the broad architecture review compat sweep.)
|
|
23
|
-
* - **C1** `readiness.mode = "strict"` only delays the `interactive`
|
|
24
|
-
* transition; every earlier stage fires identically across modes.
|
|
25
|
-
* - **D1** One DOM event family `pie-stage-change` with the stage in the
|
|
26
|
-
* payload, mirroring the M3 `framework-error` idiom.
|
|
27
|
-
*
|
|
28
|
-
* Failure semantics deliberately stay in the M3 framework-error contract —
|
|
29
|
-
* `status: "failed"` records the position; the framework error records the
|
|
30
|
-
* cause.
|
|
12
|
+
* `status` is `entered`, or `failed` for the stage a non-recoverable
|
|
13
|
+
* framework error stopped the chain at, with `skipped` for each later stage
|
|
14
|
+
* up to `interactive`. The `framework-error` event carries the cause.
|
|
31
15
|
*/
|
|
32
16
|
export const STAGES = Object.freeze([
|
|
33
17
|
"composed",
|
|
@@ -35,23 +19,3 @@ export const STAGES = Object.freeze([
|
|
|
35
19
|
"interactive",
|
|
36
20
|
"disposed",
|
|
37
21
|
]);
|
|
38
|
-
/**
|
|
39
|
-
* Stages that apply to a given CE shape. Post-retro the toolkit and the
|
|
40
|
-
* layout CEs share the same canonical list; `applicableStages` is kept as
|
|
41
|
-
* an indirection so the tracker, telemetry, and tests don't have to be
|
|
42
|
-
* rewritten when a future stage diverges between shapes.
|
|
43
|
-
*/
|
|
44
|
-
export function applicableStages(_sourceCe) {
|
|
45
|
-
return STAGES;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Strict ordering index used by the tracker to detect monotonic
|
|
49
|
-
* violations. `disposed` is last; `composed` is first.
|
|
50
|
-
*/
|
|
51
|
-
export function stageOrdinal(stage) {
|
|
52
|
-
const idx = STAGES.indexOf(stage);
|
|
53
|
-
if (idx === -1) {
|
|
54
|
-
throw new Error(`Unknown stage \`${stage}\``);
|
|
55
|
-
}
|
|
56
|
-
return idx;
|
|
57
|
-
}
|
package/dist/pie/updates.js
CHANGED
|
@@ -8,7 +8,6 @@ import { createPieLogger, isGlobalDebugEnabled } from "./logger.js";
|
|
|
8
8
|
import { findPieController } from "./scoring.js";
|
|
9
9
|
import { defaultPieElementOptions } from "./types.js";
|
|
10
10
|
import { findOrAddSession } from "./utils.js";
|
|
11
|
-
// Create module-level logger (respects global debug flag - pass function for dynamic checking)
|
|
12
11
|
const logger = createPieLogger("pie-updates", () => isGlobalDebugEnabled());
|
|
13
12
|
const describeControllerShape = (controller) => {
|
|
14
13
|
if (!controller)
|
|
@@ -67,8 +66,10 @@ const trackRemoval = (element) => {
|
|
|
67
66
|
return () => wasConnected && !element.isConnected;
|
|
68
67
|
};
|
|
69
68
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
69
|
+
* Runs the controller's `model()` and assigns the result to the element, with
|
|
70
|
+
* the authored model's `id` and `element` as defaults, together with the
|
|
71
|
+
* session the controller may have written to. Assigns nothing once the element
|
|
72
|
+
* was removed.
|
|
72
73
|
*/
|
|
73
74
|
const applyControllerToElement = async (element, model, elementSession, controller, env, logPrefix, wasRemoved, onElementSessionUpdate) => {
|
|
74
75
|
logger.debug(`${logPrefix} Using controller, env:`, env);
|
|
@@ -118,7 +119,7 @@ const applyControllerToElement = async (element, model, elementSession, controll
|
|
|
118
119
|
}
|
|
119
120
|
catch (err) {
|
|
120
121
|
logger.error(`${logPrefix} ❌ Controller error:`, err);
|
|
121
|
-
throw err; //
|
|
122
|
+
throw err; // The caller reports it and falls back to the authored model.
|
|
122
123
|
}
|
|
123
124
|
};
|
|
124
125
|
const resolveAndValidateUpdateOptions = (opts) => {
|
package/dist/pie/utils.d.ts
CHANGED
|
@@ -60,8 +60,6 @@ export declare const getPackageWithoutVersion: (packages: string) => string;
|
|
|
60
60
|
/**
|
|
61
61
|
* Find or add a session entry for a given element
|
|
62
62
|
*
|
|
63
|
-
* TODO: kinda gnarly, copied from player project
|
|
64
|
-
*
|
|
65
63
|
* @param data - Session data array
|
|
66
64
|
* @param id - Model/element ID
|
|
67
65
|
* @param element - Element tag name (optional)
|
package/dist/pie/utils.js
CHANGED
|
@@ -103,8 +103,6 @@ export const getPackageWithoutVersion = (packages) => packages
|
|
|
103
103
|
/**
|
|
104
104
|
* Find or add a session entry for a given element
|
|
105
105
|
*
|
|
106
|
-
* TODO: kinda gnarly, copied from player project
|
|
107
|
-
*
|
|
108
106
|
* @param data - Session data array
|
|
109
107
|
* @param id - Model/element ID
|
|
110
108
|
* @param element - Element tag name (optional)
|
|
@@ -31,10 +31,14 @@ export interface SanitizeItemMarkupOptions {
|
|
|
31
31
|
* Sanitize raw item/passage markup before it is injected into the DOM.
|
|
32
32
|
*
|
|
33
33
|
* - Strips `<script>`, event-handler attributes, unknown protocols and
|
|
34
|
-
*
|
|
35
|
-
* `form`, `meta`, `link`).
|
|
34
|
+
* the other tags in `SANITIZER_FORBIDDEN_TAGS` (`iframe`, `object`, `embed`,
|
|
35
|
+
* `base`, `form`, `meta`, `link`, `style`, `foreignObject`).
|
|
36
36
|
* - Preserves PIE custom elements (`pie-*`) and any extra tags listed in
|
|
37
37
|
* `allowedCustomElements`.
|
|
38
|
+
* - Marks elements that carry an authored color, as `markAuthoredColors`
|
|
39
|
+
* in `@pie-element/shared-utils` describes, so a color scheme can override
|
|
40
|
+
* it. A host that opts out with `trust-markup` or supplies its own
|
|
41
|
+
* sanitizer gets no markers.
|
|
38
42
|
* - During SSR (no `window`) returns an empty string so untrusted markup
|
|
39
43
|
* never reaches the prerender output; the live renderer will re-run the
|
|
40
44
|
* sanitizer on hydrate.
|
|
@@ -42,8 +46,8 @@ export interface SanitizeItemMarkupOptions {
|
|
|
42
46
|
export declare function sanitizeItemMarkup(markup: string, options?: SanitizeItemMarkupOptions): string;
|
|
43
47
|
/**
|
|
44
48
|
* Build the default `ItemMarkupSanitizer` used by the players. The returned
|
|
45
|
-
* function
|
|
46
|
-
*
|
|
49
|
+
* function captures the options once; each call returns a new function, so a
|
|
50
|
+
* caller that compares sanitizers by identity keeps the one it built.
|
|
47
51
|
*/
|
|
48
52
|
export declare function createDefaultItemMarkupSanitizer(options?: SanitizeItemMarkupOptions): ItemMarkupSanitizer;
|
|
49
53
|
/**
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* or supply their own sanitizer function if they need a stricter / looser
|
|
8
8
|
* allow-list.
|
|
9
9
|
*/
|
|
10
|
+
import { markAuthoredColors } from "@pie-element/shared-utils";
|
|
10
11
|
import DOMPurify from "dompurify";
|
|
11
12
|
import { SANITIZER_FORBIDDEN_ATTRS, SANITIZER_FORBIDDEN_TAGS, } from "./sanitize-forbidden-lists.js";
|
|
12
13
|
import { installStyleAttributeHook } from "./sanitize-style-attribute.js";
|
|
@@ -29,6 +30,40 @@ const BASE_ALLOWED_ATTRS = [
|
|
|
29
30
|
"lang",
|
|
30
31
|
"dir",
|
|
31
32
|
];
|
|
33
|
+
// MathML authored content uses that DOMPurify drops: elementary math, which
|
|
34
|
+
// `@pie-element/shared-math-rendering-mathjax` rewrites as a table before
|
|
35
|
+
// MathJax reads it, and `semantics`, `annotation`, `mprescripts` and `none`.
|
|
36
|
+
// `annotation-xml` stays out because it is an HTML integration point.
|
|
37
|
+
const MATHML_TAGS = new Set([
|
|
38
|
+
"mstack",
|
|
39
|
+
"mlongdiv",
|
|
40
|
+
"msgroup",
|
|
41
|
+
"msrow",
|
|
42
|
+
"msline",
|
|
43
|
+
"mscarries",
|
|
44
|
+
"mscarry",
|
|
45
|
+
"semantics",
|
|
46
|
+
"annotation",
|
|
47
|
+
"mprescripts",
|
|
48
|
+
"none",
|
|
49
|
+
]);
|
|
50
|
+
const MATHML_ATTRS = [
|
|
51
|
+
"stackalign",
|
|
52
|
+
"charalign",
|
|
53
|
+
"charspacing",
|
|
54
|
+
"longdivstyle",
|
|
55
|
+
"position",
|
|
56
|
+
"shift",
|
|
57
|
+
"location",
|
|
58
|
+
"crossout",
|
|
59
|
+
"leftoverhang",
|
|
60
|
+
"rightoverhang",
|
|
61
|
+
"mslinethickness",
|
|
62
|
+
"linebreak",
|
|
63
|
+
];
|
|
64
|
+
// Prefixed MathML such as `<m:math>`, which the HTML parser reads as unknown
|
|
65
|
+
// HTML elements and the math adapter re-creates as MathML.
|
|
66
|
+
const PREFIXED_TAG = /^[a-z_][\w.-]*:[a-z][\w.-]*$/;
|
|
32
67
|
const BASE_URI_SAFE_ATTRS = ["pie-id"];
|
|
33
68
|
const FORBIDDEN_TAGS = SANITIZER_FORBIDDEN_TAGS;
|
|
34
69
|
const FORBIDDEN_ATTRS = SANITIZER_FORBIDDEN_ATTRS;
|
|
@@ -57,16 +92,24 @@ function resolvePurifier() {
|
|
|
57
92
|
// `style` is URI-safe to DOMPurify, so nothing inside it is inspected
|
|
58
93
|
// without this. See sanitize-style-attribute.ts.
|
|
59
94
|
installStyleAttributeHook(purifierInstance);
|
|
95
|
+
// After the style filter, so only declarations that survive it are marked.
|
|
96
|
+
// The markers are the contract a color scheme's CSS overrides authored
|
|
97
|
+
// colors through, shared with element model HTML. (PIE-1119)
|
|
98
|
+
purifierInstance.addHook?.("afterSanitizeAttributes", (node) => markAuthoredColors(node));
|
|
60
99
|
return purifierInstance;
|
|
61
100
|
}
|
|
62
101
|
/**
|
|
63
102
|
* Sanitize raw item/passage markup before it is injected into the DOM.
|
|
64
103
|
*
|
|
65
104
|
* - Strips `<script>`, event-handler attributes, unknown protocols and
|
|
66
|
-
*
|
|
67
|
-
* `form`, `meta`, `link`).
|
|
105
|
+
* the other tags in `SANITIZER_FORBIDDEN_TAGS` (`iframe`, `object`, `embed`,
|
|
106
|
+
* `base`, `form`, `meta`, `link`, `style`, `foreignObject`).
|
|
68
107
|
* - Preserves PIE custom elements (`pie-*`) and any extra tags listed in
|
|
69
108
|
* `allowedCustomElements`.
|
|
109
|
+
* - Marks elements that carry an authored color, as `markAuthoredColors`
|
|
110
|
+
* in `@pie-element/shared-utils` describes, so a color scheme can override
|
|
111
|
+
* it. A host that opts out with `trust-markup` or supplies its own
|
|
112
|
+
* sanitizer gets no markers.
|
|
70
113
|
* - During SSR (no `window`) returns an empty string so untrusted markup
|
|
71
114
|
* never reaches the prerender output; the live renderer will re-run the
|
|
72
115
|
* sanitizer on hydrate.
|
|
@@ -80,8 +123,11 @@ export function sanitizeItemMarkup(markup, options = {}) {
|
|
|
80
123
|
const allowedCustomElements = (options.allowedCustomElements ?? []).map((name) => name.toLowerCase());
|
|
81
124
|
const explicitCustomElementSet = new Set(allowedCustomElements);
|
|
82
125
|
const result = purifier.sanitize(markup, {
|
|
83
|
-
|
|
84
|
-
|
|
126
|
+
// DOMPurify still removes a tag in FORBID_TAGS that this accepts.
|
|
127
|
+
ADD_TAGS: (tagName) => explicitCustomElementSet.has(tagName) ||
|
|
128
|
+
MATHML_TAGS.has(tagName) ||
|
|
129
|
+
PREFIXED_TAG.test(tagName),
|
|
130
|
+
ADD_ATTR: [...BASE_ALLOWED_ATTRS, ...MATHML_ATTRS],
|
|
85
131
|
ADD_URI_SAFE_ATTR: BASE_URI_SAFE_ATTRS,
|
|
86
132
|
FORBID_TAGS: FORBIDDEN_TAGS,
|
|
87
133
|
FORBID_ATTR: FORBIDDEN_ATTRS,
|
|
@@ -121,8 +167,8 @@ export function sanitizeItemMarkup(markup, options = {}) {
|
|
|
121
167
|
}
|
|
122
168
|
/**
|
|
123
169
|
* Build the default `ItemMarkupSanitizer` used by the players. The returned
|
|
124
|
-
* function
|
|
125
|
-
*
|
|
170
|
+
* function captures the options once; each call returns a new function, so a
|
|
171
|
+
* caller that compares sanitizers by identity keeps the one it built.
|
|
126
172
|
*/
|
|
127
173
|
export function createDefaultItemMarkupSanitizer(options = {}) {
|
|
128
174
|
const { allowedCustomElements, wrapOverwideContent } = options;
|
|
@@ -51,8 +51,8 @@ export function sanitizeSvgIcon(icon) {
|
|
|
51
51
|
});
|
|
52
52
|
const str = typeof result === "string" ? result : String(result ?? "");
|
|
53
53
|
if (stringCache.size >= STRING_CACHE_MAX) {
|
|
54
|
-
//
|
|
55
|
-
// a small fixed set per assessment so this
|
|
54
|
+
// Evict the oldest-inserted half when full; a hit does not refresh an entry.
|
|
55
|
+
// Tool icons are a small fixed set per assessment, so this rarely runs.
|
|
56
56
|
const keys = [...stringCache.keys()];
|
|
57
57
|
for (let i = 0; i < keys.length / 2; i += 1) {
|
|
58
58
|
stringCache.delete(keys[i]);
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* they never appeared in the authored markup string.
|
|
25
25
|
*
|
|
26
26
|
* The wrapping itself lives in `./wrap-overwide.js`, shared with the table
|
|
27
|
-
* wrapper: only the
|
|
27
|
+
* wrapper: only the spec below differs.
|
|
28
28
|
*/
|
|
29
29
|
import { type WrapOverwideOptions } from "./wrap-overwide.js";
|
|
30
30
|
export type WrapOverwideImagesInElementOptions = WrapOverwideOptions;
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* they never appeared in the authored markup string.
|
|
25
25
|
*
|
|
26
26
|
* The wrapping itself lives in `./wrap-overwide.js`, shared with the table
|
|
27
|
-
* wrapper: only the
|
|
27
|
+
* wrapper: only the spec below differs.
|
|
28
28
|
*/
|
|
29
29
|
import { isOverwideWrapMutation, wrapOverwideInElement, wrapOverwideMarkup, } from "./wrap-overwide.js";
|
|
30
30
|
function buildAriaLabel(image) {
|
|
@@ -32,12 +32,31 @@ function buildAriaLabel(image) {
|
|
|
32
32
|
const trimmed = alt ? alt.trim() : "";
|
|
33
33
|
return trimmed ? `Scrollable image: ${trimmed}` : "Scrollable image";
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* A block-level image — an element's own `display: block`, a flex item — gets a
|
|
37
|
+
* block wrapper, so wrapping it adds no line box below it — a host reset of
|
|
38
|
+
* `img { display: block }` included. The live pass reads the computed display;
|
|
39
|
+
* markup parsed off-document has only the inline style, so the live pass reads
|
|
40
|
+
* a wrapper built there again.
|
|
41
|
+
*/
|
|
42
|
+
function imageLayoutClass(image) {
|
|
43
|
+
const view = image.ownerDocument?.defaultView;
|
|
44
|
+
const display = view
|
|
45
|
+
? view.getComputedStyle(image).display
|
|
46
|
+
: image.style?.display;
|
|
47
|
+
const blockLevel = !!display &&
|
|
48
|
+
!display.startsWith("inline") &&
|
|
49
|
+
display !== "none" &&
|
|
50
|
+
display !== "contents";
|
|
51
|
+
return blockLevel ? "pie-image-scroll-block" : null;
|
|
52
|
+
}
|
|
35
53
|
const IMAGE_SPEC = {
|
|
36
54
|
selector: "img",
|
|
37
55
|
wrapperTag: "span",
|
|
38
56
|
wrapperClass: "pie-image-scroll",
|
|
39
57
|
markupProbe: /<img\b/i,
|
|
40
58
|
buildAriaLabel,
|
|
59
|
+
layoutClass: imageLayoutClass,
|
|
41
60
|
};
|
|
42
61
|
/**
|
|
43
62
|
* Wrap every unwrapped `<img>` descendant of `root` with the shared
|
|
@@ -32,6 +32,11 @@ export interface OverwideWrapSpec {
|
|
|
32
32
|
markupProbe: RegExp;
|
|
33
33
|
/** The wrapper's accessible name for one node. */
|
|
34
34
|
buildAriaLabel: (node: Element) => string;
|
|
35
|
+
/**
|
|
36
|
+
* An extra wrapper class carrying the node's own layout, or `null` for none.
|
|
37
|
+
* Absent when every wrapper lays out alike.
|
|
38
|
+
*/
|
|
39
|
+
layoutClass?: (node: Element) => string | null;
|
|
35
40
|
}
|
|
36
41
|
export interface WrapOverwideOptions {
|
|
37
42
|
/**
|
|
@@ -45,6 +50,9 @@ export interface WrapOverwideOptions {
|
|
|
45
50
|
/**
|
|
46
51
|
* Wrap every unwrapped match under `root`. Returns the number newly wrapped so
|
|
47
52
|
* callers can short-circuit when nothing changed. Idempotent.
|
|
53
|
+
*
|
|
54
|
+
* Under a connected `root`, a wrapper whose layout was never read live gains
|
|
55
|
+
* the layout class this pass would have given it.
|
|
48
56
|
*/
|
|
49
57
|
export declare function wrapOverwideInElement(root: Element, spec: OverwideWrapSpec, options?: WrapOverwideOptions): number;
|
|
50
58
|
/**
|
|
@@ -40,9 +40,18 @@ function isInsidePieCustomElement(node, root) {
|
|
|
40
40
|
}
|
|
41
41
|
return false;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Wrappers whose layout class was read from the live cascade. A wrapper built
|
|
45
|
+
* off-document saw only its node's inline style, so the first live pass to meet
|
|
46
|
+
* it reads the layout again; this set keeps that to once per wrapper.
|
|
47
|
+
*/
|
|
48
|
+
const liveLayoutRead = new WeakSet();
|
|
43
49
|
/**
|
|
44
50
|
* Wrap every unwrapped match under `root`. Returns the number newly wrapped so
|
|
45
51
|
* callers can short-circuit when nothing changed. Idempotent.
|
|
52
|
+
*
|
|
53
|
+
* Under a connected `root`, a wrapper whose layout was never read live gains
|
|
54
|
+
* the layout class this pass would have given it.
|
|
46
55
|
*/
|
|
47
56
|
export function wrapOverwideInElement(root, spec, options = {}) {
|
|
48
57
|
const { skipPieDescendants = false } = options;
|
|
@@ -52,24 +61,59 @@ export function wrapOverwideInElement(root, spec, options = {}) {
|
|
|
52
61
|
const ownerDocument = root.ownerDocument;
|
|
53
62
|
if (!ownerDocument)
|
|
54
63
|
return 0;
|
|
55
|
-
|
|
64
|
+
const readsLiveLayout = !!spec.layoutClass && !!ownerDocument.defaultView && root.isConnected;
|
|
65
|
+
const targets = [];
|
|
66
|
+
const unread = [];
|
|
56
67
|
for (const node of nodes) {
|
|
57
68
|
const parent = node.parentElement;
|
|
58
69
|
if (!parent)
|
|
59
70
|
continue;
|
|
60
71
|
// Idempotency — already wrapped.
|
|
61
|
-
|
|
72
|
+
const wrapped = parent.classList?.contains(spec.wrapperClass);
|
|
73
|
+
if (wrapped && (!readsLiveLayout || liveLayoutRead.has(parent)))
|
|
62
74
|
continue;
|
|
63
75
|
// Authored-markup pass: leave PIE custom-element internals alone.
|
|
64
76
|
if (skipPieDescendants && isInsidePieCustomElement(node, root))
|
|
65
77
|
continue;
|
|
66
78
|
if (node.closest(EDITING_HOST_SELECTOR))
|
|
67
79
|
continue;
|
|
80
|
+
if (!wrapped)
|
|
81
|
+
targets.push(node);
|
|
82
|
+
else if (!unread.some((other) => other.parentElement === parent))
|
|
83
|
+
unread.push(node);
|
|
84
|
+
}
|
|
85
|
+
// Layout is read for every node before the first insertion: a read after a
|
|
86
|
+
// wrap forces a fresh style resolution, one per node. A wrapper's own styles
|
|
87
|
+
// override its node's layout — the theme makes every wrapped image a block —
|
|
88
|
+
// so an existing wrapper is read with its classes lifted.
|
|
89
|
+
const unreadWrappers = unread.map((node) => node.parentElement);
|
|
90
|
+
const liftedClasses = unreadWrappers.map((wrapper) => wrapper.className);
|
|
91
|
+
for (const wrapper of unreadWrappers)
|
|
92
|
+
wrapper.className = "";
|
|
93
|
+
const layoutClasses = targets.map((node) => spec.layoutClass?.(node) ?? null);
|
|
94
|
+
const unreadLayoutClasses = unread.map((node) => spec.layoutClass?.(node) ?? null);
|
|
95
|
+
for (const [index, wrapper] of unreadWrappers.entries()) {
|
|
96
|
+
wrapper.className = liftedClasses[index];
|
|
97
|
+
const layoutClass = unreadLayoutClasses[index];
|
|
98
|
+
if (layoutClass)
|
|
99
|
+
wrapper.classList.add(layoutClass);
|
|
100
|
+
liveLayoutRead.add(wrapper);
|
|
101
|
+
}
|
|
102
|
+
let wrapped = 0;
|
|
103
|
+
for (const [index, node] of targets.entries()) {
|
|
104
|
+
const parent = node.parentElement;
|
|
105
|
+
if (!parent)
|
|
106
|
+
continue;
|
|
68
107
|
const wrapper = ownerDocument.createElement(spec.wrapperTag);
|
|
69
|
-
|
|
108
|
+
const layoutClass = layoutClasses[index];
|
|
109
|
+
wrapper.className = layoutClass
|
|
110
|
+
? `${spec.wrapperClass} ${layoutClass}`
|
|
111
|
+
: spec.wrapperClass;
|
|
70
112
|
wrapper.setAttribute("tabindex", "0");
|
|
71
113
|
wrapper.setAttribute("role", "region");
|
|
72
114
|
wrapper.setAttribute("aria-label", spec.buildAriaLabel(node));
|
|
115
|
+
if (readsLiveLayout)
|
|
116
|
+
liveLayoutRead.add(wrapper);
|
|
73
117
|
parent.insertBefore(wrapper, node);
|
|
74
118
|
wrapper.appendChild(node);
|
|
75
119
|
wrapped += 1;
|
|
@@ -116,6 +116,5 @@ export function createMediaElementTimeSource(element, options = {}) {
|
|
|
116
116
|
export function findMediaElement(root) {
|
|
117
117
|
if (!root)
|
|
118
118
|
return null;
|
|
119
|
-
|
|
120
|
-
return found ?? null;
|
|
119
|
+
return root.querySelector("video, audio");
|
|
121
120
|
}
|