@pie-players/pie-players-shared 0.3.74 → 0.3.75
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 +3 -1
- package/dist/i18n/messages/en-US.d.ts +1 -24
- package/dist/i18n/messages/en-US.js +1 -24
- package/dist/i18n/messages/nl-NL.d.ts +1 -24
- package/dist/i18n/messages/nl-NL.js +1 -24
- package/dist/i18n/types.d.ts +4 -5
- package/dist/index.d.ts +1 -1
- package/dist/loaders/esm-adapter.d.ts +11 -0
- package/dist/loaders/esm-adapter.js +26 -8
- 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/pie/elements-rendered.d.ts +25 -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/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/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/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 +10 -47
- package/dist/pie/stages.js +10 -47
- package/dist/security/sanitize-item-markup.d.ts +4 -0
- package/dist/security/sanitize-item-markup.js +48 -2
- package/dist/security/wrap-overwide-images.js +19 -0
- package/dist/security/wrap-overwide.d.ts +8 -0
- package/dist/security/wrap-overwide.js +47 -3
- package/dist/types/index.d.ts +37 -112
- package/package.json +4 -3
- 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/index.js
CHANGED
|
@@ -23,20 +23,17 @@ export { createAuthoringAssetEventManager, initializeAuthoringConfigures, resolv
|
|
|
23
23
|
export { initializeMathRendering, setMathRenderer, } from "./math-rendering.js";
|
|
24
24
|
export { attachInstrumentationEventBridge } from "./instrumentation-event-bridge.js";
|
|
25
25
|
export { resolveInstrumentationProvider } from "./instrumentation-provider-resolution.js";
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
// `pie-stage-change` event family stays coherent across CE shapes.
|
|
29
|
-
export { STAGES, applicableStages, stageOrdinal, } from "./stages.js";
|
|
30
|
-
export { createStageTracker } from "./stage-tracker.js";
|
|
26
|
+
// Stage vocabulary of the section player's `pie-stage-change` event.
|
|
27
|
+
export { STAGES } from "./stages.js";
|
|
31
28
|
export { ASSESSMENT_INSTRUMENTATION_EVENT_MAP, ITEM_INSTRUMENTATION_EVENT_MAP, SECTION_INSTRUMENTATION_EVENT_MAP, TOOLKIT_INSTRUMENTATION_EVENT_MAP, } from "./instrumentation-event-map.js";
|
|
32
29
|
export { ItemController, normalizeItemSessionContainer, } from "./item-controller.js";
|
|
33
|
-
export { ensureHostSessionEntries, hasLearnerResponse, hasResponseValue, normalizeItemSessionChange, projectSessionIntoHostContainer, } from "./item-session-contract.js";
|
|
30
|
+
export { ensureHostSessionEntries, hasLearnerResponse, hasResponseField, hasResponseValue, normalizeItemSessionChange, projectSessionIntoHostContainer, } from "./item-session-contract.js";
|
|
34
31
|
export { MemoryItemSessionStorage, SessionStorageItemSessionStorage, } from "./item-controller-storage.js";
|
|
35
32
|
export { SESSION_COMMIT_METHOD, bindPageLifecycleCommit, commitPendingSessions, flushPendingSessionNotifications, noteSessionBaseline, noteSessionObserved, } from "./session-commit.js";
|
|
36
33
|
// Opt-in device-local snapshot of a committed session, offered back to the host
|
|
37
34
|
// after a crash rather than applied.
|
|
38
35
|
export { createSessionSnapshot, isSessionSnapshotEnabled, isSnapshotIdentitySpecific, sessionSnapshotKey, sessionStorageSnapshotStore, } from "./session-snapshot.js";
|
|
39
|
-
export { createPieLogger, isGlobalDebugEnabled } from "./logger.js";
|
|
36
|
+
export { createPieLogger, isGlobalDebugEnabled, isTtsDebugEnabled, } from "./logger.js";
|
|
40
37
|
// Registry
|
|
41
38
|
export { pieRegistry } from "./registry.js";
|
|
42
39
|
// Scoring
|
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
import { isInstrumentationProvider } from "../instrumentation/provider-guards.js";
|
|
2
2
|
import { createPieLogger, isGlobalDebugEnabled } from "./logger.js";
|
|
3
3
|
const logger = createPieLogger("instrumentation-event-bridge", () => isGlobalDebugEnabled());
|
|
4
|
+
/**
|
|
5
|
+
* Telemetry attributes are data. A detail's live objects (a coordinator on
|
|
6
|
+
* `toolkit-ready`, an `Error` cause, a function) stay on the event for its
|
|
7
|
+
* listeners and are left out of the attributes; primitives, arrays and plain
|
|
8
|
+
* objects pass.
|
|
9
|
+
*/
|
|
10
|
+
function isAttributeValue(value) {
|
|
11
|
+
if (value === null)
|
|
12
|
+
return true;
|
|
13
|
+
const type = typeof value;
|
|
14
|
+
if (type === "function" || type === "symbol")
|
|
15
|
+
return false;
|
|
16
|
+
if (type !== "object" || Array.isArray(value))
|
|
17
|
+
return true;
|
|
18
|
+
const prototype = Object.getPrototypeOf(value);
|
|
19
|
+
return prototype === Object.prototype || prototype === null;
|
|
20
|
+
}
|
|
4
21
|
function normalizeEventDetail(detail) {
|
|
5
22
|
if (detail && typeof detail === "object") {
|
|
6
|
-
|
|
23
|
+
const attributes = {};
|
|
24
|
+
for (const [key, value] of Object.entries(detail)) {
|
|
25
|
+
if (isAttributeValue(value))
|
|
26
|
+
attributes[key] = value;
|
|
27
|
+
}
|
|
28
|
+
return attributes;
|
|
7
29
|
}
|
|
8
30
|
if (typeof detail === "undefined") {
|
|
9
31
|
return {};
|
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
export const TOOLKIT_INSTRUMENTATION_EVENT_MAP = [
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// `attached`, `runtime-bound`, and `ui-rendered` stages because they
|
|
6
|
-
// had zero internal or external consumers. Hosts listen for the
|
|
7
|
-
// DOM-prefixed name directly (`pie-stage-change`); the instrumentation
|
|
8
|
-
// bridge forwards the same name to telemetry.
|
|
9
|
-
{
|
|
10
|
-
sourceEventName: "pie-stage-change",
|
|
11
|
-
instrumentationEventName: "pie-toolkit-stage-change",
|
|
12
|
-
},
|
|
2
|
+
// The toolkit emits no stage events: the section player's runtime
|
|
3
|
+
// engine dispatches `pie-stage-change` on the layout element, above the
|
|
4
|
+
// toolkit, and the section map tracks it there.
|
|
13
5
|
{
|
|
14
6
|
sourceEventName: "runtime-owned",
|
|
15
7
|
instrumentationEventName: "pie-toolkit-runtime-owned",
|
|
@@ -18,6 +10,10 @@ export const TOOLKIT_INSTRUMENTATION_EVENT_MAP = [
|
|
|
18
10
|
sourceEventName: "runtime-inherited",
|
|
19
11
|
instrumentationEventName: "pie-toolkit-runtime-inherited",
|
|
20
12
|
},
|
|
13
|
+
{
|
|
14
|
+
sourceEventName: "runtime-ready",
|
|
15
|
+
instrumentationEventName: "pie-toolkit-runtime-ready",
|
|
16
|
+
},
|
|
21
17
|
{
|
|
22
18
|
sourceEventName: "toolkit-ready",
|
|
23
19
|
instrumentationEventName: "pie-toolkit-ready",
|
|
@@ -32,13 +28,10 @@ export const TOOLKIT_INSTRUMENTATION_EVENT_MAP = [
|
|
|
32
28
|
},
|
|
33
29
|
];
|
|
34
30
|
export const SECTION_INSTRUMENTATION_EVENT_MAP = [
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
// `ready`) and the `section-controller-ready` mapping were removed
|
|
40
|
-
// alongside their DOM-event surfaces. `session-changed` stays off the
|
|
41
|
-
// bridge for the reason the item map gives below.
|
|
31
|
+
// The runtime engine dispatches `pie-stage-change` and
|
|
32
|
+
// `pie-loading-complete` on the layout element; `framework-error`
|
|
33
|
+
// bubbles to it from the player's toolkit. `session-changed` stays off
|
|
34
|
+
// the bridge for the reason the item map gives below.
|
|
42
35
|
{
|
|
43
36
|
sourceEventName: "pie-stage-change",
|
|
44
37
|
instrumentationEventName: "pie-section-stage-change",
|
|
@@ -45,7 +45,11 @@ export class ItemController {
|
|
|
45
45
|
setSession(input, options = {}) {
|
|
46
46
|
const next = normalizeSessionContainer(input, this.sessionId);
|
|
47
47
|
const allowMetadataOverwrite = options.allowMetadataOverwrite ?? false;
|
|
48
|
+
// The guard keeps a response from metadata churn on the same session. A
|
|
49
|
+
// session with another id is a new attempt or item, and replaces it.
|
|
50
|
+
const sameSession = next.id === "" || this.session.id === "" || next.id === this.session.id;
|
|
48
51
|
if (!allowMetadataOverwrite &&
|
|
52
|
+
sameSession &&
|
|
49
53
|
hasResponseValue(this.session) &&
|
|
50
54
|
!hasResponseValue(next) &&
|
|
51
55
|
!hasResponseField(next)) {
|
package/dist/pie/logger.d.ts
CHANGED
|
@@ -61,3 +61,9 @@ declare global {
|
|
|
61
61
|
* Check if debug mode is enabled globally
|
|
62
62
|
*/
|
|
63
63
|
export declare function isGlobalDebugEnabled(): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Read-aloud's debug flag: `PIE_TTS_DEBUG=1` in the environment, or
|
|
66
|
+
* `globalThis.__PIE_TTS_DEBUG__ = true` in a page. Every read-aloud logger is
|
|
67
|
+
* created with it, and reads it on each debug line.
|
|
68
|
+
*/
|
|
69
|
+
export declare function isTtsDebugEnabled(): boolean;
|
package/dist/pie/logger.js
CHANGED
|
@@ -42,3 +42,13 @@ export function createPieLogger(namespace, debugEnabledFn) {
|
|
|
42
42
|
export function isGlobalDebugEnabled() {
|
|
43
43
|
return typeof window !== "undefined" && window.PIE_DEBUG === true;
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Read-aloud's debug flag: `PIE_TTS_DEBUG=1` in the environment, or
|
|
47
|
+
* `globalThis.__PIE_TTS_DEBUG__ = true` in a page. Every read-aloud logger is
|
|
48
|
+
* created with it, and reads it on each debug line.
|
|
49
|
+
*/
|
|
50
|
+
export function isTtsDebugEnabled() {
|
|
51
|
+
const scope = globalThis;
|
|
52
|
+
return (scope.process?.env?.PIE_TTS_DEBUG === "1" ||
|
|
53
|
+
scope.__PIE_TTS_DEBUG__ === true);
|
|
54
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the MathJax adapter's copies load MathJax's fonts, speech worker and,
|
|
3
|
+
* on its npm build, MathJax itself from, and whether math is in the tab order.
|
|
4
|
+
* Every copy of `@pie-element/shared-math-rendering-mathjax` reads these from
|
|
5
|
+
* the legacy renderer's page options as it starts MathJax, on its first render,
|
|
6
|
+
* so they are set before any element renders. See
|
|
7
|
+
* https://github.com/pie-framework/pie-elements-ng/blob/develop/docs/MATH-RENDERING.md#assets
|
|
8
|
+
*/
|
|
9
|
+
export interface MathAssetOptions {
|
|
10
|
+
/**
|
|
11
|
+
* An npm root: a URL under which `<package>@<version>/<path>` serves that
|
|
12
|
+
* file of the package, such as `https://cdn.jsdelivr.net/npm`. A relative URL
|
|
13
|
+
* resolves against the page.
|
|
14
|
+
*/
|
|
15
|
+
assetRoot?: string | URL;
|
|
16
|
+
/**
|
|
17
|
+
* The directory of `speech-worker.js` and its `mathmaps/`, by default
|
|
18
|
+
* `mathjax@<version>/sre` under the asset root.
|
|
19
|
+
*/
|
|
20
|
+
speechPath?: string | URL;
|
|
21
|
+
/**
|
|
22
|
+
* The URL of individual files by npm path, `<package>@<version>/<path>`: a
|
|
23
|
+
* listed file loads from its URL, the rest from the asset root. A bundle lists
|
|
24
|
+
* each file it ships as `new URL("./…", import.meta.url)`, which its host's
|
|
25
|
+
* bundler emits, and needs no root. Read by the adapter's browser build, for
|
|
26
|
+
* its fonts, speech worker and speech rules.
|
|
27
|
+
*/
|
|
28
|
+
assetUrls?: Readonly<Record<string, string | URL>>;
|
|
29
|
+
/**
|
|
30
|
+
* The speech locales the MathJax menu lists, by id (`["en", "es"]`), or by id
|
|
31
|
+
* with the label it shows. Unset, it lists every locale SRE ships.
|
|
32
|
+
*/
|
|
33
|
+
speechLocales?: readonly string[] | Readonly<Record<string, string>>;
|
|
34
|
+
/**
|
|
35
|
+
* Puts typeset math in the keyboard tab order, for the MathJax menu and its
|
|
36
|
+
* explorer. Off by default.
|
|
37
|
+
*/
|
|
38
|
+
inTabOrder?: boolean;
|
|
39
|
+
}
|
|
40
|
+
/** Why `options` is not a `MathAssetOptions`, or `undefined` when it is. */
|
|
41
|
+
export declare function mathAssetOptionsError(options: unknown): string | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Writes `options` to the page options, keeping the ones it leaves unset and
|
|
44
|
+
* the files of `assetUrls` it does not list.
|
|
45
|
+
*/
|
|
46
|
+
export declare function setMathAssetOptions(options: MathAssetOptions): void;
|
|
47
|
+
/** The asset root the page options set, if any. */
|
|
48
|
+
export declare function pageMathAssetRoot(): string | undefined;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the MathJax adapter's copies load MathJax's fonts, speech worker and,
|
|
3
|
+
* on its npm build, MathJax itself from, and whether math is in the tab order.
|
|
4
|
+
* Every copy of `@pie-element/shared-math-rendering-mathjax` reads these from
|
|
5
|
+
* the legacy renderer's page options as it starts MathJax, on its first render,
|
|
6
|
+
* so they are set before any element renders. See
|
|
7
|
+
* https://github.com/pie-framework/pie-elements-ng/blob/develop/docs/MATH-RENDERING.md#assets
|
|
8
|
+
*/
|
|
9
|
+
/** The legacy renderer's page options, which the adapter reads as well. */
|
|
10
|
+
const PAGE_OPTIONS_KEY = "@pie-lib/math-rendering@2";
|
|
11
|
+
const isUrl = (value) => (typeof value === "string" && value !== "") || value instanceof URL;
|
|
12
|
+
/** Why `options` is not a `MathAssetOptions`, or `undefined` when it is. */
|
|
13
|
+
export function mathAssetOptionsError(options) {
|
|
14
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
15
|
+
return "must be an object";
|
|
16
|
+
}
|
|
17
|
+
const { assetRoot, speechPath, assetUrls, speechLocales, inTabOrder } = options;
|
|
18
|
+
if (assetRoot !== undefined && !isUrl(assetRoot)) {
|
|
19
|
+
return `assetRoot must be a URL, got ${JSON.stringify(assetRoot)}`;
|
|
20
|
+
}
|
|
21
|
+
if (speechPath !== undefined && !isUrl(speechPath)) {
|
|
22
|
+
return `speechPath must be a URL, got ${JSON.stringify(speechPath)}`;
|
|
23
|
+
}
|
|
24
|
+
if (assetUrls !== undefined &&
|
|
25
|
+
(!assetUrls ||
|
|
26
|
+
typeof assetUrls !== "object" ||
|
|
27
|
+
Array.isArray(assetUrls) ||
|
|
28
|
+
!Object.values(assetUrls).every(isUrl))) {
|
|
29
|
+
return "assetUrls must map npm paths to URLs";
|
|
30
|
+
}
|
|
31
|
+
if (speechLocales !== undefined) {
|
|
32
|
+
const listed = Array.isArray(speechLocales)
|
|
33
|
+
? speechLocales
|
|
34
|
+
: speechLocales && typeof speechLocales === "object"
|
|
35
|
+
? Object.values(speechLocales)
|
|
36
|
+
: null;
|
|
37
|
+
if (!listed || listed.some((entry) => typeof entry !== "string" || !entry)) {
|
|
38
|
+
return "speechLocales must be locale ids, or locale ids mapped to their labels";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (inTabOrder !== undefined && typeof inTabOrder !== "boolean") {
|
|
42
|
+
return `inTabOrder must be a boolean, got ${JSON.stringify(inTabOrder)}`;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Writes `options` to the page options, keeping the ones it leaves unset and
|
|
48
|
+
* the files of `assetUrls` it does not list.
|
|
49
|
+
*/
|
|
50
|
+
export function setMathAssetOptions(options) {
|
|
51
|
+
if (typeof window === "undefined")
|
|
52
|
+
return;
|
|
53
|
+
const host = window;
|
|
54
|
+
const page = (host[PAGE_OPTIONS_KEY] ??= {});
|
|
55
|
+
const opts = (page.opts ??= {});
|
|
56
|
+
if (options.assetRoot !== undefined)
|
|
57
|
+
opts.assetRoot = String(options.assetRoot);
|
|
58
|
+
if (options.speechPath !== undefined)
|
|
59
|
+
opts.speechPath = String(options.speechPath);
|
|
60
|
+
if (options.assetUrls !== undefined) {
|
|
61
|
+
const listed = opts.assetUrls && typeof opts.assetUrls === "object" ? opts.assetUrls : {};
|
|
62
|
+
opts.assetUrls = {
|
|
63
|
+
...listed,
|
|
64
|
+
...Object.fromEntries(Object.entries(options.assetUrls).map(([path, url]) => [path, String(url)])),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (options.speechLocales !== undefined) {
|
|
68
|
+
opts.speechLocales = Array.isArray(options.speechLocales)
|
|
69
|
+
? [...options.speechLocales]
|
|
70
|
+
: { ...options.speechLocales };
|
|
71
|
+
}
|
|
72
|
+
if (options.inTabOrder !== undefined)
|
|
73
|
+
opts.inTabOrder = options.inTabOrder;
|
|
74
|
+
}
|
|
75
|
+
/** The asset root the page options set, if any. */
|
|
76
|
+
export function pageMathAssetRoot() {
|
|
77
|
+
if (typeof window === "undefined")
|
|
78
|
+
return undefined;
|
|
79
|
+
const root = window[PAGE_OPTIONS_KEY]?.opts?.assetRoot;
|
|
80
|
+
return typeof root === "string" && root ? root : undefined;
|
|
81
|
+
}
|
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,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Stage vocabulary of the section player's `pie-stage-change` event.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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`
|
|
8
|
+
* reports that every item loaded and is not a stage. `readiness.mode =
|
|
9
|
+
* "strict"` only delays `interactive`.
|
|
10
10
|
*
|
|
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.
|
|
11
|
+
* `status` is `entered`, or `failed` for the stage a non-recoverable
|
|
12
|
+
* framework error stopped the chain at, with `skipped` for each later stage
|
|
13
|
+
* up to `interactive`. The `framework-error` event carries the cause.
|
|
31
14
|
*/
|
|
32
15
|
export declare const STAGES: readonly ["composed", "engine-ready", "interactive", "disposed"];
|
|
33
16
|
export type Stage = (typeof STAGES)[number];
|
|
34
17
|
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
18
|
export type StageChangeDetail = {
|
|
55
19
|
stage: Stage;
|
|
56
20
|
status: StageStatus;
|
|
@@ -68,7 +32,6 @@ export type LoadingCompleteDetail = {
|
|
|
68
32
|
sectionId: string;
|
|
69
33
|
attemptId?: string;
|
|
70
34
|
itemCount: number;
|
|
71
|
-
loadedCount: number;
|
|
72
35
|
timestamp: string;
|
|
73
36
|
sourceCe: string;
|
|
74
37
|
};
|
package/dist/pie/stages.js
CHANGED
|
@@ -1,33 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Stage vocabulary of the section player's `pie-stage-change` event.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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`
|
|
8
|
+
* reports that every item loaded and is not a stage. `readiness.mode =
|
|
9
|
+
* "strict"` only delays `interactive`.
|
|
10
10
|
*
|
|
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.
|
|
11
|
+
* `status` is `entered`, or `failed` for the stage a non-recoverable
|
|
12
|
+
* framework error stopped the chain at, with `skipped` for each later stage
|
|
13
|
+
* up to `interactive`. The `framework-error` event carries the cause.
|
|
31
14
|
*/
|
|
32
15
|
export const STAGES = Object.freeze([
|
|
33
16
|
"composed",
|
|
@@ -35,23 +18,3 @@ export const STAGES = Object.freeze([
|
|
|
35
18
|
"interactive",
|
|
36
19
|
"disposed",
|
|
37
20
|
]);
|
|
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
|
-
}
|
|
@@ -35,6 +35,10 @@ export interface SanitizeItemMarkupOptions {
|
|
|
35
35
|
* `form`, `meta`, `link`).
|
|
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.
|
|
@@ -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,6 +92,10 @@ 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
|
/**
|
|
@@ -67,6 +106,10 @@ function resolvePurifier() {
|
|
|
67
106
|
* `form`, `meta`, `link`).
|
|
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,
|
|
@@ -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
|
/**
|