@qti-components/base 2.2.0 → 2.4.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.
@@ -3,7 +3,9 @@ export interface SessionContext {
3
3
  navPartId?: string | null;
4
4
  navSectionId?: string | null;
5
5
  navItemRefId?: string | null;
6
+ /** @deprecated Never set by the library. Removed in the next major. */
6
7
  navItemLoading?: boolean;
8
+ /** @deprecated Never set by the library. Removed in the next major. */
7
9
  navTestLoading?: boolean;
8
10
  view?: View;
9
11
  }
@@ -1,4 +1,4 @@
1
1
  import { createContext } from '@lit/context';
2
2
  export const INITIAL_SESSION_CONTEXT = { view: 'candidate' };
3
- export const sessionContext = createContext(Symbol('testContext'));
3
+ export const sessionContext = createContext(Symbol('sessionContext'));
4
4
  //# sourceMappingURL=session.context.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"session.context.js","sourceRoot":"","sources":["../../src/context/session.context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAa7C,MAAM,CAAC,MAAM,uBAAuB,GAA6B,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;AAEvF,MAAM,CAAC,MAAM,cAAc,GAAG,aAAa,CAA2B,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC","sourcesContent":["import { createContext } from '@lit/context';\n\nexport type View = 'author' | 'candidate' | 'proctor' | 'scorer' | 'testConstructor' | 'tutor' | '';\n\nexport interface SessionContext {\n navPartId?: string | null;\n navSectionId?: string | null;\n navItemRefId?: string | null;\n navItemLoading?: boolean;\n navTestLoading?: boolean;\n view?: View;\n}\n\nexport const INITIAL_SESSION_CONTEXT: Readonly<SessionContext> = { view: 'candidate' };\n\nexport const sessionContext = createContext<Readonly<SessionContext>>(Symbol('testContext'));\n"]}
1
+ {"version":3,"file":"session.context.js","sourceRoot":"","sources":["../../src/context/session.context.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAe7C,MAAM,CAAC,MAAM,uBAAuB,GAA6B,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;AAEvF,MAAM,CAAC,MAAM,cAAc,GAAG,aAAa,CAA2B,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC","sourcesContent":["import { createContext } from '@lit/context';\n\nexport type View = 'author' | 'candidate' | 'proctor' | 'scorer' | 'testConstructor' | 'tutor' | '';\n\nexport interface SessionContext {\n navPartId?: string | null;\n navSectionId?: string | null;\n navItemRefId?: string | null;\n /** @deprecated Never set by the library. Removed in the next major. */\n navItemLoading?: boolean;\n /** @deprecated Never set by the library. Removed in the next major. */\n navTestLoading?: boolean;\n view?: View;\n}\n\nexport const INITIAL_SESSION_CONTEXT: Readonly<SessionContext> = { view: 'candidate' };\n\nexport const sessionContext = createContext<Readonly<SessionContext>>(Symbol('sessionContext'));\n"]}
@@ -8,5 +8,15 @@ export interface ItemContext {
8
8
  * Used for interactions that support state save/restore (e.g. PCI).
9
9
  */
10
10
  state?: Record<string, string | null>;
11
+ /** The item is shown for reading only. Interactions follow it; see `qti-assessment-item`'s `readonly`. */
12
+ readonly?: boolean;
13
+ /** The item is locked, for example after it was submitted. Interactions follow it; see `qti-assessment-item`'s `disabled`. */
14
+ disabled?: boolean;
15
+ /**
16
+ * Whether the interactions in the item are valid right now (a required answer is given, a limit is
17
+ * respected). Published by the item whenever it checks, so a reader never has to ask the item.
18
+ * Unset until the item has checked once.
19
+ */
20
+ valid?: boolean;
11
21
  }
12
22
  export declare const itemContextVariables: VariableDeclaration<string | string[]>[];
@@ -1 +1 @@
1
- {"version":3,"file":"item.types.js","sourceRoot":"","sources":["../../../src/context/types/item.types.ts"],"names":[],"mappings":"AAaA,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC;QACE,UAAU,EAAE,kBAAkB;QAC9B,WAAW,EAAE,QAAQ;QACrB,QAAQ,EAAE,QAAQ;QAClB,KAAK,EAAE,SAAS;QAChB,IAAI,EAAE,SAAS;KAChB;IACD;QACE,UAAU,EAAE,aAAa;QACzB,WAAW,EAAE,QAAQ;QACrB,QAAQ,EAAE,SAAS;QACnB,KAAK,EAAE,GAAG;QACV,IAAI,EAAE,UAAU;KACjB;CAC0C,CAAC","sourcesContent":["import type { VariableDeclaration } from '../../lib/variables';\n\nexport interface ItemContext {\n identifier?: string;\n href?: string;\n variables?: ReadonlyArray<VariableDeclaration<string | string[] | null>>;\n /**\n * Optional per-interaction opaque state, keyed by responseIdentifier.\n * Used for interactions that support state save/restore (e.g. PCI).\n */\n state?: Record<string, string | null>;\n}\n\nexport const itemContextVariables = [\n {\n identifier: 'completionStatus',\n cardinality: 'single',\n baseType: 'string',\n value: 'unknown',\n type: 'outcome'\n },\n {\n identifier: 'numAttempts',\n cardinality: 'single',\n baseType: 'integer',\n value: '0',\n type: 'response'\n }\n] as VariableDeclaration<string | string[]>[];\n"]}
1
+ {"version":3,"file":"item.types.js","sourceRoot":"","sources":["../../../src/context/types/item.types.ts"],"names":[],"mappings":"AAuBA,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC;QACE,UAAU,EAAE,kBAAkB;QAC9B,WAAW,EAAE,QAAQ;QACrB,QAAQ,EAAE,QAAQ;QAClB,KAAK,EAAE,SAAS;QAChB,IAAI,EAAE,SAAS;KAChB;IACD;QACE,UAAU,EAAE,aAAa;QACzB,WAAW,EAAE,QAAQ;QACrB,QAAQ,EAAE,SAAS;QACnB,KAAK,EAAE,GAAG;QACV,IAAI,EAAE,UAAU;KACjB;CAC0C,CAAC","sourcesContent":["import type { VariableDeclaration } from '../../lib/variables';\n\nexport interface ItemContext {\n identifier?: string;\n href?: string;\n variables?: ReadonlyArray<VariableDeclaration<string | string[] | null>>;\n /**\n * Optional per-interaction opaque state, keyed by responseIdentifier.\n * Used for interactions that support state save/restore (e.g. PCI).\n */\n state?: Record<string, string | null>;\n /** The item is shown for reading only. Interactions follow it; see `qti-assessment-item`'s `readonly`. */\n readonly?: boolean;\n /** The item is locked, for example after it was submitted. Interactions follow it; see `qti-assessment-item`'s `disabled`. */\n disabled?: boolean;\n /**\n * Whether the interactions in the item are valid right now (a required answer is given, a limit is\n * respected). Published by the item whenever it checks, so a reader never has to ask the item.\n * Unset until the item has checked once.\n */\n valid?: boolean;\n}\n\nexport const itemContextVariables = [\n {\n identifier: 'completionStatus',\n cardinality: 'single',\n baseType: 'string',\n value: 'unknown',\n type: 'outcome'\n },\n {\n identifier: 'numAttempts',\n cardinality: 'single',\n baseType: 'integer',\n value: '0',\n type: 'response'\n }\n] as VariableDeclaration<string | string[]>[];\n"]}
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import './context/context-root';
1
2
  import './register';
2
3
  export * from './context/computed-item.context';
3
4
  export * from './context/types/computed-item.types';
@@ -23,6 +24,7 @@ export * from './lib/expression-result';
23
24
  export * from './lib/interaction.interface';
24
25
  export * from './lib/variables';
25
26
  export * from './lib/qti-rule-base';
27
+ export * from './lib/custom-operators';
26
28
  export * from './utils/custom-state-set';
27
29
  export * from './utils/scoring-helper';
28
30
  export * from './utils/utils';
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import './context/context-root';
1
2
  import './register';
2
3
  export * from './context/computed-item.context';
3
4
  export * from './context/types/computed-item.types';
@@ -23,6 +24,7 @@ export * from './lib/expression-result';
23
24
  export * from './lib/interaction.interface';
24
25
  export * from './lib/variables';
25
26
  export * from './lib/qti-rule-base';
27
+ export * from './lib/custom-operators';
26
28
  export * from './utils/custom-state-set';
27
29
  export * from './utils/scoring-helper';
28
30
  export * from './utils/utils';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,CAAC;AAEpB,cAAc,iCAAiC,CAAC;AAChD,cAAc,qCAAqC,CAAC;AACpD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,0BAA0B,CAAC;AACzC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AAEvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,oCAAoC,CAAC;AAEnD,cAAc,wBAAwB,CAAC;AACvC,cAAc,qCAAqC,CAAC;AACpD,cAAc,qCAAqC,CAAC;AACpD,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAE1C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,yBAAyB,CAAC;AACxC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AAEpC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC","sourcesContent":["import './register';\n\nexport * from './context/computed-item.context';\nexport * from './context/types/computed-item.types';\nexport * from './context/computed.context';\nexport * from './context/types/computed.types';\nexport * from './context/config.context';\nexport * from './context/interaction.context';\nexport * from './context/item.context';\nexport * from './context/types/item.types';\nexport * from './context/qti.context';\nexport * from './context/session.context';\nexport * from './context/test.context';\n\nexport * from './styles/box-sizing.styles';\nexport * from './styles/drop-region.styles';\nexport * from './styles/validation-message.styles';\n\nexport * from './abstract/interaction';\nexport * from './abstract/qti-variable-declaration';\nexport * from './abstract/qti-condition-expression';\nexport * from './abstract/qti-feedback';\nexport * from './abstract/qti-expression';\n\nexport * from './lib/response';\nexport * from './lib/expression-result';\nexport * from './lib/interaction.interface';\nexport * from './lib/variables';\nexport * from './lib/qti-rule-base';\n\nexport * from './utils/custom-state-set';\nexport * from './utils/scoring-helper';\nexport * from './utils/utils';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,wBAAwB,CAAC;AAChC,OAAO,YAAY,CAAC;AAEpB,cAAc,iCAAiC,CAAC;AAChD,cAAc,qCAAqC,CAAC;AACpD,cAAc,4BAA4B,CAAC;AAC3C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,0BAA0B,CAAC;AACzC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,wBAAwB,CAAC;AACvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,wBAAwB,CAAC;AAEvC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,oCAAoC,CAAC;AAEnD,cAAc,wBAAwB,CAAC;AACvC,cAAc,qCAAqC,CAAC;AACpD,cAAc,qCAAqC,CAAC;AACpD,cAAc,yBAAyB,CAAC;AACxC,cAAc,2BAA2B,CAAC;AAE1C,cAAc,gBAAgB,CAAC;AAC/B,cAAc,yBAAyB,CAAC;AACxC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AAEvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,eAAe,CAAC","sourcesContent":["import './context/context-root';\nimport './register';\n\nexport * from './context/computed-item.context';\nexport * from './context/types/computed-item.types';\nexport * from './context/computed.context';\nexport * from './context/types/computed.types';\nexport * from './context/config.context';\nexport * from './context/interaction.context';\nexport * from './context/item.context';\nexport * from './context/types/item.types';\nexport * from './context/qti.context';\nexport * from './context/session.context';\nexport * from './context/test.context';\n\nexport * from './styles/box-sizing.styles';\nexport * from './styles/drop-region.styles';\nexport * from './styles/validation-message.styles';\n\nexport * from './abstract/interaction';\nexport * from './abstract/qti-variable-declaration';\nexport * from './abstract/qti-condition-expression';\nexport * from './abstract/qti-feedback';\nexport * from './abstract/qti-expression';\n\nexport * from './lib/response';\nexport * from './lib/expression-result';\nexport * from './lib/interaction.interface';\nexport * from './lib/variables';\nexport * from './lib/qti-rule-base';\nexport * from './lib/custom-operators';\n\nexport * from './utils/custom-state-set';\nexport * from './utils/scoring-helper';\nexport * from './utils/utils';\n"]}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The registry behind `<qti-custom-operator definition="…">`.
3
+ *
4
+ * A custom operator is by definition outside the QTI vocabulary, so which ones
5
+ * exist is a property of the delivery engine rather than of an item. An item
6
+ * names one in its `definition` attribute and the engine supplies it — here,
7
+ * through this registry.
8
+ *
9
+ * This is the second of two mechanisms on `qti-custom-operator`. The other runs
10
+ * inline JavaScript out of a CDATA section and is selected with
11
+ * `class="js.org"`; it stays as it is. `definition` is what items authored
12
+ * against the Citolab scoring engine use, and the two do not interact.
13
+ */
14
+ export type CustomOperatorValue = string | string[] | number | boolean | null;
15
+ /**
16
+ * Handed the calculated values of the element's child expressions, in document
17
+ * order, and returns the value of the expression.
18
+ */
19
+ export type CustomOperator = (values: CustomOperatorValue[]) => CustomOperatorValue;
20
+ /**
21
+ * Make an operator available to every `qti-custom-operator` that names it.
22
+ *
23
+ * The key is the `definition` attribute in full, prefix included. Registering a
24
+ * key a built-in already uses overrides that built-in, which is how a delivery
25
+ * engine replaces one it disagrees with.
26
+ */
27
+ export declare const registerCustomOperator: (definition: string, operator: CustomOperator) => void;
28
+ /** Undo a {@link registerCustomOperator}, restoring any built-in of that name. */
29
+ export declare const unregisterCustomOperator: (definition: string) => void;
30
+ /** The operator for a `definition`, or null when nothing provides it. */
31
+ export declare const getCustomOperator: (definition: string) => CustomOperator | null;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The registry behind `<qti-custom-operator definition="…">`.
3
+ *
4
+ * A custom operator is by definition outside the QTI vocabulary, so which ones
5
+ * exist is a property of the delivery engine rather than of an item. An item
6
+ * names one in its `definition` attribute and the engine supplies it — here,
7
+ * through this registry.
8
+ *
9
+ * This is the second of two mechanisms on `qti-custom-operator`. The other runs
10
+ * inline JavaScript out of a CDATA section and is selected with
11
+ * `class="js.org"`; it stays as it is. `definition` is what items authored
12
+ * against the Citolab scoring engine use, and the two do not interact.
13
+ */
14
+ /** The value an operator most often works on: the first child, as text. */
15
+ const firstString = (values) => {
16
+ const value = values[0];
17
+ if (value === null || value === undefined)
18
+ return null;
19
+ return Array.isArray(value) ? (value[0] ?? null) : String(value);
20
+ };
21
+ /** A text operator that leaves a NULL or absent first value alone. */
22
+ const textOperator = (transform) => values => {
23
+ const value = firstString(values);
24
+ return value === null ? null : transform(value);
25
+ };
26
+ /**
27
+ * The three operators the scoring engine ships, under each of the prefixes it
28
+ * registers them with. The prefix is part of the key because `definition` is
29
+ * matched whole, exactly as the item writes it.
30
+ */
31
+ const BUILT_IN_PREFIXES = ['depcp', 'questify', 'qade'];
32
+ const BUILT_IN_OPERATORS = {
33
+ Trim: textOperator(value => value.trim()),
34
+ // Decompose, then drop the combining marks that decomposition split off, so
35
+ // "café" compares as "cafe".
36
+ ToAscii: textOperator(value => value.normalize('NFD').replace(/[̀-ͯ]/g, '')),
37
+ // A decimal comma written where the response processing expects a dot. Only
38
+ // the first comma, which is the decimal separator in the locales that use it.
39
+ ParseCommaDecimal: textOperator(value => value.replace(',', '.'))
40
+ };
41
+ const builtIns = new Map(BUILT_IN_PREFIXES.flatMap(prefix => Object.entries(BUILT_IN_OPERATORS).map(([name, operator]) => [
42
+ `${prefix}:${name}`,
43
+ operator
44
+ ])));
45
+ /** Registered by the host application; consulted before the built-ins. */
46
+ const registered = new Map();
47
+ /**
48
+ * Make an operator available to every `qti-custom-operator` that names it.
49
+ *
50
+ * The key is the `definition` attribute in full, prefix included. Registering a
51
+ * key a built-in already uses overrides that built-in, which is how a delivery
52
+ * engine replaces one it disagrees with.
53
+ */
54
+ export const registerCustomOperator = (definition, operator) => {
55
+ registered.set(definition, operator);
56
+ };
57
+ /** Undo a {@link registerCustomOperator}, restoring any built-in of that name. */
58
+ export const unregisterCustomOperator = (definition) => {
59
+ registered.delete(definition);
60
+ };
61
+ /** The operator for a `definition`, or null when nothing provides it. */
62
+ export const getCustomOperator = (definition) => registered.get(definition) ?? builtIns.get(definition) ?? null;
63
+ //# sourceMappingURL=custom-operators.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"custom-operators.js","sourceRoot":"","sources":["../../src/lib/custom-operators.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAUH,2EAA2E;AAC3E,MAAM,WAAW,GAAG,CAAC,MAA6B,EAAiB,EAAE;IACnE,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACxB,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IACvD,OAAO,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACnE,CAAC,CAAC;AAEF,sEAAsE;AACtE,MAAM,YAAY,GAChB,CAAC,SAAoC,EAAkB,EAAE,CACzD,MAAM,CAAC,EAAE;IACP,MAAM,KAAK,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;AAClD,CAAC,CAAC;AAEJ;;;;GAIG;AACH,MAAM,iBAAiB,GAAG,CAAC,OAAO,EAAE,UAAU,EAAE,MAAM,CAAU,CAAC;AAEjE,MAAM,kBAAkB,GAAmC;IACzD,IAAI,EAAE,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IACzC,4EAA4E;IAC5E,6BAA6B;IAC7B,OAAO,EAAE,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;IAC5E,4EAA4E;IAC5E,8EAA8E;IAC9E,iBAAiB,EAAE,YAAY,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;CAClE,CAAC;AAEF,MAAM,QAAQ,GAAG,IAAI,GAAG,CACtB,iBAAiB,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CACjC,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,QAAQ,CAAC,EAA4B,EAAE,CAAC;IACrF,GAAG,MAAM,IAAI,IAAI,EAAE;IACnB,QAAQ;CACT,CAAC,CACH,CACF,CAAC;AAEF,0EAA0E;AAC1E,MAAM,UAAU,GAAG,IAAI,GAAG,EAA0B,CAAC;AAErD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,UAAkB,EAAE,QAAwB,EAAQ,EAAE;IAC3F,UAAU,CAAC,GAAG,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;AACvC,CAAC,CAAC;AAEF,kFAAkF;AAClF,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,UAAkB,EAAQ,EAAE;IACnE,UAAU,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;AAChC,CAAC,CAAC;AAEF,yEAAyE;AACzE,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,UAAkB,EAAyB,EAAE,CAC7E,UAAU,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC","sourcesContent":["/**\n * The registry behind `<qti-custom-operator definition=\"…\">`.\n *\n * A custom operator is by definition outside the QTI vocabulary, so which ones\n * exist is a property of the delivery engine rather than of an item. An item\n * names one in its `definition` attribute and the engine supplies it — here,\n * through this registry.\n *\n * This is the second of two mechanisms on `qti-custom-operator`. The other runs\n * inline JavaScript out of a CDATA section and is selected with\n * `class=\"js.org\"`; it stays as it is. `definition` is what items authored\n * against the Citolab scoring engine use, and the two do not interact.\n */\n\nexport type CustomOperatorValue = string | string[] | number | boolean | null;\n\n/**\n * Handed the calculated values of the element's child expressions, in document\n * order, and returns the value of the expression.\n */\nexport type CustomOperator = (values: CustomOperatorValue[]) => CustomOperatorValue;\n\n/** The value an operator most often works on: the first child, as text. */\nconst firstString = (values: CustomOperatorValue[]): string | null => {\n const value = values[0];\n if (value === null || value === undefined) return null;\n return Array.isArray(value) ? (value[0] ?? null) : String(value);\n};\n\n/** A text operator that leaves a NULL or absent first value alone. */\nconst textOperator =\n (transform: (value: string) => string): CustomOperator =>\n values => {\n const value = firstString(values);\n return value === null ? null : transform(value);\n };\n\n/**\n * The three operators the scoring engine ships, under each of the prefixes it\n * registers them with. The prefix is part of the key because `definition` is\n * matched whole, exactly as the item writes it.\n */\nconst BUILT_IN_PREFIXES = ['depcp', 'questify', 'qade'] as const;\n\nconst BUILT_IN_OPERATORS: Record<string, CustomOperator> = {\n Trim: textOperator(value => value.trim()),\n // Decompose, then drop the combining marks that decomposition split off, so\n // \"café\" compares as \"cafe\".\n ToAscii: textOperator(value => value.normalize('NFD').replace(/[̀-ͯ]/g, '')),\n // A decimal comma written where the response processing expects a dot. Only\n // the first comma, which is the decimal separator in the locales that use it.\n ParseCommaDecimal: textOperator(value => value.replace(',', '.'))\n};\n\nconst builtIns = new Map<string, CustomOperator>(\n BUILT_IN_PREFIXES.flatMap(prefix =>\n Object.entries(BUILT_IN_OPERATORS).map(([name, operator]): [string, CustomOperator] => [\n `${prefix}:${name}`,\n operator\n ])\n )\n);\n\n/** Registered by the host application; consulted before the built-ins. */\nconst registered = new Map<string, CustomOperator>();\n\n/**\n * Make an operator available to every `qti-custom-operator` that names it.\n *\n * The key is the `definition` attribute in full, prefix included. Registering a\n * key a built-in already uses overrides that built-in, which is how a delivery\n * engine replaces one it disagrees with.\n */\nexport const registerCustomOperator = (definition: string, operator: CustomOperator): void => {\n registered.set(definition, operator);\n};\n\n/** Undo a {@link registerCustomOperator}, restoring any built-in of that name. */\nexport const unregisterCustomOperator = (definition: string): void => {\n registered.delete(definition);\n};\n\n/** The operator for a `definition`, or null when nothing provides it. */\nexport const getCustomOperator = (definition: string): CustomOperator | null =>\n registered.get(definition) ?? builtIns.get(definition) ?? null;\n"]}
@@ -11,3 +11,28 @@ export interface QtiRuleBase {
11
11
  export declare abstract class QtiRuleElement extends LitElement implements QtiRuleBase {
12
12
  abstract process(): any;
13
13
  }
14
+ /**
15
+ * Thrown by `qti-exit-response` to end the response processing of an attempt.
16
+ *
17
+ * An exit can sit at any depth — inside a `qti-response-condition` branch, or
18
+ * inside a `qti-response-processing-fragment` — and has to stop every rule
19
+ * after it, not just its own siblings. Unwinding the stack is what reaches
20
+ * every one of those loops without each of them having to report upwards.
21
+ *
22
+ * `qti-response-processing` catches it at the top of the run. The outcomes set
23
+ * before the exit are kept, because each `qti-set-outcome-value` has already
24
+ * dispatched by the time this is thrown.
25
+ */
26
+ export declare class QtiExitResponseSignal extends Error {
27
+ constructor();
28
+ }
29
+ /**
30
+ * Thrown by `qti-exit-test` to end the outcome processing of a test.
31
+ *
32
+ * The test-level twin of {@link QtiExitResponseSignal}, and unwinds the stack
33
+ * for the same reason: an exit inside a `qti-outcome-condition` branch has to
34
+ * stop the rules after the condition, not just its own siblings.
35
+ */
36
+ export declare class QtiExitTestSignal extends Error {
37
+ constructor();
38
+ }
@@ -4,4 +4,35 @@ import { LitElement } from 'lit';
4
4
  */
5
5
  export class QtiRuleElement extends LitElement {
6
6
  }
7
+ /**
8
+ * Thrown by `qti-exit-response` to end the response processing of an attempt.
9
+ *
10
+ * An exit can sit at any depth — inside a `qti-response-condition` branch, or
11
+ * inside a `qti-response-processing-fragment` — and has to stop every rule
12
+ * after it, not just its own siblings. Unwinding the stack is what reaches
13
+ * every one of those loops without each of them having to report upwards.
14
+ *
15
+ * `qti-response-processing` catches it at the top of the run. The outcomes set
16
+ * before the exit are kept, because each `qti-set-outcome-value` has already
17
+ * dispatched by the time this is thrown.
18
+ */
19
+ export class QtiExitResponseSignal extends Error {
20
+ constructor() {
21
+ super('qti-exit-response');
22
+ this.name = 'QtiExitResponseSignal';
23
+ }
24
+ }
25
+ /**
26
+ * Thrown by `qti-exit-test` to end the outcome processing of a test.
27
+ *
28
+ * The test-level twin of {@link QtiExitResponseSignal}, and unwinds the stack
29
+ * for the same reason: an exit inside a `qti-outcome-condition` branch has to
30
+ * stop the rules after the condition, not just its own siblings.
31
+ */
32
+ export class QtiExitTestSignal extends Error {
33
+ constructor() {
34
+ super('qti-exit-test');
35
+ this.name = 'QtiExitTestSignal';
36
+ }
37
+ }
7
38
  //# sourceMappingURL=qti-rule-base.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"qti-rule-base.js","sourceRoot":"","sources":["../../src/lib/qti-rule-base.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AASjC;;GAEG;AACH,MAAM,OAAgB,cAAe,SAAQ,UAAU;CAEtD","sourcesContent":["import { LitElement } from 'lit';\n\n/**\n * Interface for any object that can process QTI rules\n */\nexport interface QtiRuleBase {\n process(): any;\n}\n\n/**\n * Abstract base class for QTI rule DOM elements\n */\nexport abstract class QtiRuleElement extends LitElement implements QtiRuleBase {\n abstract process(): any;\n}\n"]}
1
+ {"version":3,"file":"qti-rule-base.js","sourceRoot":"","sources":["../../src/lib/qti-rule-base.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,KAAK,CAAC;AASjC;;GAEG;AACH,MAAM,OAAgB,cAAe,SAAQ,UAAU;CAEtD;AAED;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAC9C;QACE,KAAK,CAAC,mBAAmB,CAAC,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAC1C;QACE,KAAK,CAAC,eAAe,CAAC,CAAC;QACvB,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF","sourcesContent":["import { LitElement } from 'lit';\n\n/**\n * Interface for any object that can process QTI rules\n */\nexport interface QtiRuleBase {\n process(): any;\n}\n\n/**\n * Abstract base class for QTI rule DOM elements\n */\nexport abstract class QtiRuleElement extends LitElement implements QtiRuleBase {\n abstract process(): any;\n}\n\n/**\n * Thrown by `qti-exit-response` to end the response processing of an attempt.\n *\n * An exit can sit at any depth — inside a `qti-response-condition` branch, or\n * inside a `qti-response-processing-fragment` — and has to stop every rule\n * after it, not just its own siblings. Unwinding the stack is what reaches\n * every one of those loops without each of them having to report upwards.\n *\n * `qti-response-processing` catches it at the top of the run. The outcomes set\n * before the exit are kept, because each `qti-set-outcome-value` has already\n * dispatched by the time this is thrown.\n */\nexport class QtiExitResponseSignal extends Error {\n constructor() {\n super('qti-exit-response');\n this.name = 'QtiExitResponseSignal';\n }\n}\n\n/**\n * Thrown by `qti-exit-test` to end the outcome processing of a test.\n *\n * The test-level twin of {@link QtiExitResponseSignal}, and unwinds the stack\n * for the same reason: an exit inside a `qti-outcome-condition` branch has to\n * stop the rules after the condition, not just its own siblings.\n */\nexport class QtiExitTestSignal extends Error {\n constructor() {\n super('qti-exit-test');\n this.name = 'QtiExitTestSignal';\n }\n}\n"]}
@@ -33,8 +33,24 @@ export interface VariableDeclaration<T> extends VariableValue<T> {
33
33
  cardinality?: Cardinality;
34
34
  baseType?: BaseType;
35
35
  }
36
+ /**
37
+ * One `qti-interpolation-table-entry`. `sourceValue` is the *lower bound* of a
38
+ * range rather than a value to match exactly, so the entries have to keep their
39
+ * document order: the first whose bound the source value clears wins.
40
+ */
41
+ export interface InterpolationTableEntry {
42
+ sourceValue: number;
43
+ targetValue: number;
44
+ /** Whether `sourceValue` itself is in range. Defaults to true, per the spec. */
45
+ includeBoundary: boolean;
46
+ }
36
47
  export interface OutcomeVariable extends VariableDeclaration<string | string[] | null> {
37
- interpolationTable?: Map<number, number>;
48
+ interpolationTable?: InterpolationTableEntry[];
49
+ /**
50
+ * A qti-match-table's entries. Its targets are not necessarily numeric — a
51
+ * match table may map onto identifiers or strings — so they stay as written.
52
+ */
53
+ matchTable?: Map<number, string>;
38
54
  externalScored?: 'human' | 'externalMachine' | null;
39
55
  }
40
56
  export interface ResponseVariable extends VariableDeclaration<string | string[] | null> {
@@ -1 +1 @@
1
- {"version":3,"file":"variables.js","sourceRoot":"","sources":["../../src/lib/variables.ts"],"names":[],"mappings":"","sourcesContent":["import type { BaseType, Cardinality } from './expression-result';\n\nexport type QtiAreaMapping = {\n defaultValue: number;\n lowerBound?: number;\n upperBound?: number;\n areaMapEntries: QtiAreaMapEntry[];\n};\n\nexport type QtiAreaMapEntry = {\n shape: areaShape;\n coords: string;\n mappedValue: number;\n defaultValue: number;\n};\n\nexport type QtiMapping = {\n defaultValue: number;\n lowerBound?: number;\n upperBound?: number;\n mapEntries: QtiMapEntry[];\n};\n\nexport type QtiMapEntry = { mapKey: string; mappedValue: number; caseSensitive: boolean };\n\nexport type areaShape = 'default' | 'circle' | 'rect' | 'ellipse' | 'poly';\nexport interface VariableValue<T> {\n identifier: string;\n value: Readonly<T>;\n defaultValue?: T | null;\n type: 'outcome' | 'response' | 'context' | 'template';\n}\n\nexport interface VariableDeclaration<T> extends VariableValue<T> {\n cardinality?: Cardinality;\n baseType?: BaseType;\n}\n\nexport interface OutcomeVariable extends VariableDeclaration<string | string[] | null> {\n // specific to outcome variables\n interpolationTable?: Map<number, number>;\n externalScored?: 'human' | 'externalMachine' | null;\n}\n\nexport interface ResponseVariable extends VariableDeclaration<string | string[] | null> {\n // specific to response variables\n candidateResponse?: string | string[] | null;\n mapping?: QtiMapping;\n areaMapping?: QtiAreaMapping; // Optional property for area mappings\n correctResponse?: string | string[] | null;\n}\n\nexport interface TemplateVariable extends VariableDeclaration<string | string[] | number | boolean | null> {\n type: 'template';\n mathVariable?: boolean;\n paramVariable?: boolean;\n}\n"]}
1
+ {"version":3,"file":"variables.js","sourceRoot":"","sources":["../../src/lib/variables.ts"],"names":[],"mappings":"","sourcesContent":["import type { BaseType, Cardinality } from './expression-result';\n\nexport type QtiAreaMapping = {\n defaultValue: number;\n lowerBound?: number;\n upperBound?: number;\n areaMapEntries: QtiAreaMapEntry[];\n};\n\nexport type QtiAreaMapEntry = {\n shape: areaShape;\n coords: string;\n mappedValue: number;\n defaultValue: number;\n};\n\nexport type QtiMapping = {\n defaultValue: number;\n lowerBound?: number;\n upperBound?: number;\n mapEntries: QtiMapEntry[];\n};\n\nexport type QtiMapEntry = { mapKey: string; mappedValue: number; caseSensitive: boolean };\n\nexport type areaShape = 'default' | 'circle' | 'rect' | 'ellipse' | 'poly';\nexport interface VariableValue<T> {\n identifier: string;\n value: Readonly<T>;\n defaultValue?: T | null;\n type: 'outcome' | 'response' | 'context' | 'template';\n}\n\nexport interface VariableDeclaration<T> extends VariableValue<T> {\n cardinality?: Cardinality;\n baseType?: BaseType;\n}\n\n/**\n * One `qti-interpolation-table-entry`. `sourceValue` is the *lower bound* of a\n * range rather than a value to match exactly, so the entries have to keep their\n * document order: the first whose bound the source value clears wins.\n */\nexport interface InterpolationTableEntry {\n sourceValue: number;\n targetValue: number;\n /** Whether `sourceValue` itself is in range. Defaults to true, per the spec. */\n includeBoundary: boolean;\n}\n\nexport interface OutcomeVariable extends VariableDeclaration<string | string[] | null> {\n // specific to outcome variables\n interpolationTable?: InterpolationTableEntry[];\n /**\n * A qti-match-table's entries. Its targets are not necessarily numeric — a\n * match table may map onto identifiers or strings — so they stay as written.\n */\n matchTable?: Map<number, string>;\n externalScored?: 'human' | 'externalMachine' | null;\n}\n\nexport interface ResponseVariable extends VariableDeclaration<string | string[] | null> {\n // specific to response variables\n candidateResponse?: string | string[] | null;\n mapping?: QtiMapping;\n areaMapping?: QtiAreaMapping; // Optional property for area mappings\n correctResponse?: string | string[] | null;\n}\n\nexport interface TemplateVariable extends VariableDeclaration<string | string[] | number | boolean | null> {\n type: 'template';\n mathVariable?: boolean;\n paramVariable?: boolean;\n}\n"]}
@@ -17,8 +17,15 @@ export class ScoringHelper {
17
17
  const [shape, ...coords] = areaKey.split(',');
18
18
  const coordinates = coords.map(Number);
19
19
  switch (shape.toLowerCase()) {
20
- case 'circle':
21
- case 'default': {
20
+ // "The default shape refers to the entire area of the associated image" (QTI 3.0 §8.29), so
21
+ // every point is inside it and the coords are not read — §7.9 says none should be given, and
22
+ // the XSD still requires the attribute, so whatever is there is filler.
23
+ //
24
+ // This used to share the `circle` branch below, which demands exactly three coords and so
25
+ // rejected any real default entry as an "Invalid circle definition" (Citolab/qti-components#83).
26
+ case 'default':
27
+ return true;
28
+ case 'circle': {
22
29
  const [cx, cy, radius] = coordinates;
23
30
  if (coordinates.length !== 3) {
24
31
  console.warn(`Invalid circle definition: ${areaKey}`);
@@ -88,6 +95,23 @@ export class ScoringHelper {
88
95
  const int1 = parseInt(value1, 10);
89
96
  const int2 = parseInt(value2, 10);
90
97
  if (!isNaN(int1) && !isNaN(int2)) {
98
+ /*
99
+ * `parseInt` truncates, so on its own it reads "2" and "2.5" as the
100
+ * same number. That matters because the base type is taken from one
101
+ * operand: an expression whose result happens to be whole is an
102
+ * integer, and it was then compared against a fractional value with
103
+ * the fraction thrown away — `equal(divide(4, 2), 2.5)` was true.
104
+ *
105
+ * When either side has a fractional part the two are, by definition,
106
+ * not the same number. Comparing them in full says so, while the
107
+ * integer path keeps `parseInt`'s tolerance of a value like "12 euro"
108
+ * for everything that really is whole.
109
+ */
110
+ const float1 = parseFloat(value1);
111
+ const float2 = parseFloat(value2);
112
+ if (!Number.isInteger(float1) || !Number.isInteger(float2)) {
113
+ return float1 === float2;
114
+ }
91
115
  return int1 === int2;
92
116
  }
93
117
  else {
@@ -108,8 +132,17 @@ export class ScoringHelper {
108
132
  }
109
133
  case 'pair':
110
134
  case 'directedPair': {
111
- const pair1 = value1.split(' ').sort();
112
- const pair2 = value2.split(' ').sort();
135
+ /*
136
+ * Only a `pair` is unordered. A `directedPair` is directed — "A B" and
137
+ * "B A" are different values — so it must be compared as written.
138
+ *
139
+ * Both operands used to be sorted the moment they were split, which
140
+ * made the `baseType === 'pair'` sort below it dead code and every
141
+ * directedPair comparison order-insensitive: a candidate who matched
142
+ * the right two identifiers the wrong way round scored as correct.
143
+ */
144
+ const pair1 = value1.split(' ');
145
+ const pair2 = value2.split(' ');
113
146
  if (pair1.length === 2 && pair2.length === 2) {
114
147
  if (baseType === 'pair') {
115
148
  pair1.sort();
@@ -1 +1 @@
1
- {"version":3,"file":"scoring-helper.js","sourceRoot":"","sources":["../../src/utils/scoring-helper.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,aAAa;IACxB;;;;;;OAMG;IACI,MAAM,CAAC,aAAa,CAAC,KAAa,EAAE,OAAe,EAAE,QAAgB;QAC1E,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;YACzB,OAAO,CAAC,IAAI,CAAC,aAAa,QAAQ,2CAA2C,CAAC,CAAC;YAC/E,OAAO,KAAK,CAAC;QACf,CAAC;QAED,yCAAyC;QACzC,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAE9C,4BAA4B;QAC5B,MAAM,CAAC,KAAK,EAAE,GAAG,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC9C,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAEvC,QAAQ,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC;YAC5B,KAAK,QAAQ,CAAC;YACd,KAAK,SAAS,CAAC,CAAC,CAAC;gBACf,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,WAAW,CAAC;gBACrC,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBAC7B,OAAO,CAAC,IAAI,CAAC,8BAA8B,OAAO,EAAE,CAAC,CAAC;oBACtD,OAAO,KAAK,CAAC;gBACf,CAAC;gBACD,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;gBAC5D,OAAO,QAAQ,IAAI,MAAM,CAAC;YAC5B,CAAC;YAED,KAAK,MAAM,CAAC,CAAC,CAAC;gBACZ,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,GAAG,WAAW,CAAC;gBACrC,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBAC7B,OAAO,CAAC,IAAI,CAAC,iCAAiC,OAAO,EAAE,CAAC,CAAC;oBACzD,OAAO,KAAK,CAAC;gBACf,CAAC;gBACD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;YACtD,CAAC;YAED,KAAK,SAAS,CAAC,CAAC,CAAC;gBACf,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,GAAG,WAAW,CAAC;gBACrC,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBAC7B,OAAO,CAAC,IAAI,CAAC,+BAA+B,OAAO,EAAE,CAAC,CAAC;oBACvD,OAAO,KAAK,CAAC;gBACf,CAAC;gBACD,iEAAiE;gBACjE,MAAM,WAAW,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;gBAC7C,MAAM,WAAW,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;gBAC7C,OAAO,WAAW,GAAG,WAAW,IAAI,CAAC,CAAC;YACxC,CAAC;YAED,KAAK,MAAM,CAAC,CAAC,CAAC;gBACZ,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;oBAC3D,OAAO,CAAC,IAAI,CAAC,+BAA+B,OAAO,EAAE,CAAC,CAAC;oBACvD,OAAO,KAAK,CAAC;gBACf,CAAC;gBACD,MAAM,QAAQ,GAAG,EAAE,CAAC;gBACpB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,WAAW,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;oBAC/C,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;gBAC9D,CAAC;gBACD,OAAO,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,CAAC;YAC3D,CAAC;YAED;gBACE,OAAO,CAAC,IAAI,CAAC,2BAA2B,KAAK,EAAE,CAAC,CAAC;gBACjD,OAAO,KAAK,CAAC;QACjB,CAAC;IACH,CAAC;IAED;;;;;OAKG;IACH,MAAM,CAAC,gBAAgB,CAAC,KAA+B,EAAE,QAAoC;QAC3F,IAAI,MAAM,GAAG,KAAK,CAAC;QACnB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACtE,MAAM,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,EACtB,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACrB,MAAM,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,EACtB,EAAE,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAErB,MAAM,SAAS,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC,KAAK,EAAE,GAAG,KAAK,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC;YAE3G,IAAI,SAAS;gBAAE,MAAM,GAAG,CAAC,MAAM,CAAC;QAClC,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAEM,MAAM,CAAC,mBAAmB,CAAC,MAAwB,EAAE,MAAwB,EAAE,QAAkB;QACtG,QAAQ,QAAQ,EAAE,CAAC;YACjB,KAAK,YAAY,CAAC;YAClB,KAAK,QAAQ;gBACX,OAAO,MAAM,KAAK,MAAM,CAAC;YAC3B,KAAK,SAAS,CAAC,CAAC,CAAC;gBACf,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;gBAClC,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;gBAClC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;oBACjC,OAAO,IAAI,KAAK,IAAI,CAAC;gBACvB,CAAC;qBAAM,CAAC;oBACN,OAAO,CAAC,KAAK,CAAC,kBAAkB,MAAM,WAAW,MAAM,UAAU,CAAC,CAAC;gBACrE,CAAC;gBACD,MAAM;YACR,CAAC;YACD,KAAK,OAAO,CAAC,CAAC,CAAC;gBACb,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;gBAClC,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;gBAClC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;oBACrC,OAAO,MAAM,KAAK,MAAM,CAAC;gBAC3B,CAAC;qBAAM,CAAC;oBACN,OAAO,CAAC,KAAK,CAAC,oBAAoB,MAAM,WAAW,MAAM,YAAY,CAAC,CAAC;gBACzE,CAAC;gBACD,MAAM;YACR,CAAC;YACD,KAAK,MAAM,CAAC;YACZ,KAAK,cAAc,CAAC,CAAC,CAAC;gBACpB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;gBACvC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;gBACvC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBAC7C,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;wBACxB,KAAK,CAAC,IAAI,EAAE,CAAC;wBACb,KAAK,CAAC,IAAI,EAAE,CAAC;oBACf,CAAC;oBACD,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBAC7C,CAAC;qBAAM,CAAC;oBACN,OAAO,CAAC,KAAK,CAAC,sEAAsE,MAAM,OAAO,MAAM,EAAE,CAAC,CAAC;gBAC7G,CAAC;gBACD,MAAM;YACR,CAAC;QACH,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC;CACF","sourcesContent":["import type { BaseType } from '../lib/expression-result';\n\nexport class ScoringHelper {\n /**\n * Checks if a given point is within a specified area.\n * @param point The point to test, represented as a string \"x y\" (e.g., \"102 113\").\n * @param areaKey The area definition, including shape and coordinates (e.g., \"circle,102,113,16\").\n * @param baseType The base type of the response, must be \"point\" for this method to proceed.\n * @returns True if the point is within the area; false otherwise.\n */\n public static isPointInArea(point: string, areaKey: string, baseType: string): boolean {\n if (baseType !== 'point') {\n console.warn(`Base type ${baseType} is not supported for point area mapping.`);\n return false;\n }\n\n // Parse the point as x and y coordinates\n const [px, py] = point.split(' ').map(Number);\n\n // Parse the area definition\n const [shape, ...coords] = areaKey.split(',');\n const coordinates = coords.map(Number);\n\n switch (shape.toLowerCase()) {\n case 'circle':\n case 'default': {\n const [cx, cy, radius] = coordinates;\n if (coordinates.length !== 3) {\n console.warn(`Invalid circle definition: ${areaKey}`);\n return false;\n }\n const distance = Math.sqrt((px - cx) ** 2 + (py - cy) ** 2);\n return distance <= radius;\n }\n\n case 'rect': {\n const [x1, y1, x2, y2] = coordinates;\n if (coordinates.length !== 4) {\n console.warn(`Invalid rectangle definition: ${areaKey}`);\n return false;\n }\n return px >= x1 && px <= x2 && py >= y1 && py <= y2;\n }\n\n case 'ellipse': {\n const [cx, cy, rx, ry] = coordinates;\n if (coordinates.length !== 4) {\n console.warn(`Invalid ellipse definition: ${areaKey}`);\n return false;\n }\n // Ellipse equation: ((px - cx)² / rx²) + ((py - cy)² / ry²) <= 1\n const normalizedX = (px - cx) ** 2 / rx ** 2;\n const normalizedY = (py - cy) ** 2 / ry ** 2;\n return normalizedX + normalizedY <= 1;\n }\n\n case 'poly': {\n if (coordinates.length < 6 || coordinates.length % 2 !== 0) {\n console.warn(`Invalid polygon definition: ${areaKey}`);\n return false;\n }\n const vertices = [];\n for (let i = 0; i < coordinates.length; i += 2) {\n vertices.push({ x: coordinates[i], y: coordinates[i + 1] });\n }\n return this.isPointInPolygon({ x: px, y: py }, vertices);\n }\n\n default:\n console.warn(`Unsupported shape type: ${shape}`);\n return false;\n }\n }\n\n /**\n * Checks if a point is inside a polygon using the ray-casting algorithm.\n * @param point The point to test.\n * @param vertices The vertices of the polygon in order (array of {x, y} objects).\n * @returns True if the point is inside the polygon; false otherwise.\n */\n static isPointInPolygon(point: { x: number; y: number }, vertices: { x: number; y: number }[]): boolean {\n let inside = false;\n for (let i = 0, j = vertices.length - 1; i < vertices.length; j = i++) {\n const xi = vertices[i].x,\n yi = vertices[i].y;\n const xj = vertices[j].x,\n yj = vertices[j].y;\n\n const intersect = yi > point.y !== yj > point.y && point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi;\n\n if (intersect) inside = !inside;\n }\n return inside;\n }\n\n public static compareSingleValues(value1: Readonly<string>, value2: Readonly<string>, baseType: BaseType): boolean {\n switch (baseType) {\n case 'identifier':\n case 'string':\n return value1 === value2;\n case 'integer': {\n const int1 = parseInt(value1, 10);\n const int2 = parseInt(value2, 10);\n if (!isNaN(int1) && !isNaN(int2)) {\n return int1 === int2;\n } else {\n console.error(`Cannot convert ${value1} and/or ${value2} to int.`);\n }\n break;\n }\n case 'float': {\n const float1 = parseFloat(value1);\n const float2 = parseFloat(value2);\n if (!isNaN(float1) && !isNaN(float2)) {\n return float1 === float2;\n } else {\n console.error(`couldn't convert ${value1} and/or ${value2} to float.`);\n }\n break;\n }\n case 'pair':\n case 'directedPair': {\n const pair1 = value1.split(' ').sort();\n const pair2 = value2.split(' ').sort();\n if (pair1.length === 2 && pair2.length === 2) {\n if (baseType === 'pair') {\n pair1.sort();\n pair2.sort();\n }\n return pair1.join(' ') === pair2.join(' ');\n } else {\n console.error(`compared two pair but one of the values does not have 2 values: 1: ${value1} 2: ${value2}`);\n }\n break;\n }\n }\n\n return false;\n }\n}\n"]}
1
+ {"version":3,"file":"scoring-helper.js","sourceRoot":"","sources":["../../src/utils/scoring-helper.ts"],"names":[],"mappings":"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","sourcesContent":["import type { BaseType } from '../lib/expression-result';\n\nexport class ScoringHelper {\n /**\n * Checks if a given point is within a specified area.\n * @param point The point to test, represented as a string \"x y\" (e.g., \"102 113\").\n * @param areaKey The area definition, including shape and coordinates (e.g., \"circle,102,113,16\").\n * @param baseType The base type of the response, must be \"point\" for this method to proceed.\n * @returns True if the point is within the area; false otherwise.\n */\n public static isPointInArea(point: string, areaKey: string, baseType: string): boolean {\n if (baseType !== 'point') {\n console.warn(`Base type ${baseType} is not supported for point area mapping.`);\n return false;\n }\n\n // Parse the point as x and y coordinates\n const [px, py] = point.split(' ').map(Number);\n\n // Parse the area definition\n const [shape, ...coords] = areaKey.split(',');\n const coordinates = coords.map(Number);\n\n switch (shape.toLowerCase()) {\n // \"The default shape refers to the entire area of the associated image\" (QTI 3.0 §8.29), so\n // every point is inside it and the coords are not read — §7.9 says none should be given, and\n // the XSD still requires the attribute, so whatever is there is filler.\n //\n // This used to share the `circle` branch below, which demands exactly three coords and so\n // rejected any real default entry as an \"Invalid circle definition\" (Citolab/qti-components#83).\n case 'default':\n return true;\n\n case 'circle': {\n const [cx, cy, radius] = coordinates;\n if (coordinates.length !== 3) {\n console.warn(`Invalid circle definition: ${areaKey}`);\n return false;\n }\n const distance = Math.sqrt((px - cx) ** 2 + (py - cy) ** 2);\n return distance <= radius;\n }\n\n case 'rect': {\n const [x1, y1, x2, y2] = coordinates;\n if (coordinates.length !== 4) {\n console.warn(`Invalid rectangle definition: ${areaKey}`);\n return false;\n }\n return px >= x1 && px <= x2 && py >= y1 && py <= y2;\n }\n\n case 'ellipse': {\n const [cx, cy, rx, ry] = coordinates;\n if (coordinates.length !== 4) {\n console.warn(`Invalid ellipse definition: ${areaKey}`);\n return false;\n }\n // Ellipse equation: ((px - cx)² / rx²) + ((py - cy)² / ry²) <= 1\n const normalizedX = (px - cx) ** 2 / rx ** 2;\n const normalizedY = (py - cy) ** 2 / ry ** 2;\n return normalizedX + normalizedY <= 1;\n }\n\n case 'poly': {\n if (coordinates.length < 6 || coordinates.length % 2 !== 0) {\n console.warn(`Invalid polygon definition: ${areaKey}`);\n return false;\n }\n const vertices = [];\n for (let i = 0; i < coordinates.length; i += 2) {\n vertices.push({ x: coordinates[i], y: coordinates[i + 1] });\n }\n return this.isPointInPolygon({ x: px, y: py }, vertices);\n }\n\n default:\n console.warn(`Unsupported shape type: ${shape}`);\n return false;\n }\n }\n\n /**\n * Checks if a point is inside a polygon using the ray-casting algorithm.\n * @param point The point to test.\n * @param vertices The vertices of the polygon in order (array of {x, y} objects).\n * @returns True if the point is inside the polygon; false otherwise.\n */\n static isPointInPolygon(point: { x: number; y: number }, vertices: { x: number; y: number }[]): boolean {\n let inside = false;\n for (let i = 0, j = vertices.length - 1; i < vertices.length; j = i++) {\n const xi = vertices[i].x,\n yi = vertices[i].y;\n const xj = vertices[j].x,\n yj = vertices[j].y;\n\n const intersect = yi > point.y !== yj > point.y && point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi;\n\n if (intersect) inside = !inside;\n }\n return inside;\n }\n\n public static compareSingleValues(value1: Readonly<string>, value2: Readonly<string>, baseType: BaseType): boolean {\n switch (baseType) {\n case 'identifier':\n case 'string':\n return value1 === value2;\n case 'integer': {\n const int1 = parseInt(value1, 10);\n const int2 = parseInt(value2, 10);\n if (!isNaN(int1) && !isNaN(int2)) {\n /*\n * `parseInt` truncates, so on its own it reads \"2\" and \"2.5\" as the\n * same number. That matters because the base type is taken from one\n * operand: an expression whose result happens to be whole is an\n * integer, and it was then compared against a fractional value with\n * the fraction thrown away — `equal(divide(4, 2), 2.5)` was true.\n *\n * When either side has a fractional part the two are, by definition,\n * not the same number. Comparing them in full says so, while the\n * integer path keeps `parseInt`'s tolerance of a value like \"12 euro\"\n * for everything that really is whole.\n */\n const float1 = parseFloat(value1);\n const float2 = parseFloat(value2);\n if (!Number.isInteger(float1) || !Number.isInteger(float2)) {\n return float1 === float2;\n }\n return int1 === int2;\n } else {\n console.error(`Cannot convert ${value1} and/or ${value2} to int.`);\n }\n break;\n }\n case 'float': {\n const float1 = parseFloat(value1);\n const float2 = parseFloat(value2);\n if (!isNaN(float1) && !isNaN(float2)) {\n return float1 === float2;\n } else {\n console.error(`couldn't convert ${value1} and/or ${value2} to float.`);\n }\n break;\n }\n case 'pair':\n case 'directedPair': {\n /*\n * Only a `pair` is unordered. A `directedPair` is directed — \"A B\" and\n * \"B A\" are different values — so it must be compared as written.\n *\n * Both operands used to be sorted the moment they were split, which\n * made the `baseType === 'pair'` sort below it dead code and every\n * directedPair comparison order-insensitive: a candidate who matched\n * the right two identifiers the wrong way round scored as correct.\n */\n const pair1 = value1.split(' ');\n const pair2 = value2.split(' ');\n if (pair1.length === 2 && pair2.length === 2) {\n if (baseType === 'pair') {\n pair1.sort();\n pair2.sort();\n }\n return pair1.join(' ') === pair2.join(' ');\n } else {\n console.error(`compared two pair but one of the values does not have 2 values: 1: ${value1} 2: ${value2}`);\n }\n break;\n }\n }\n\n return false;\n }\n}\n"]}
@@ -1,4 +1,14 @@
1
1
  export declare const decimalSeparator: () => string;
2
+ /**
3
+ * Renders a number the way QTI stores one: a plain decimal string with `.` as the separator and no
4
+ * grouping, whatever the browser's locale is. Strings are already in that form and pass through.
5
+ *
6
+ * `Number.prototype.toString()` is exactly that format. The locale-aware versions this used to call
7
+ * are not: under a comma-decimal locale (nl, de, fr, ...) it ran `.replace('.', '')` over a string
8
+ * that always uses `.`, so a score of 0.5 was stored as "05" and 1.5 as "15" — an item's SCORE came
9
+ * out ten times too big for every value with a fraction. Under a dot-decimal locale
10
+ * `toLocaleString()` grouped thousands instead, turning 1234.5 into "1,234.5".
11
+ */
2
12
  export declare const convertNumberToUniversalFormat: (number: number | string) => string;
3
13
  export declare function IsNullOrUndefined(value: unknown): boolean;
4
14
  export declare function removeDoubleSlashes(str: string): string;
@@ -1,18 +1,18 @@
1
1
  export const decimalSeparator = () => {
2
2
  return new Intl.NumberFormat().format(0.1).replace(/\d/g, '');
3
3
  };
4
+ /**
5
+ * Renders a number the way QTI stores one: a plain decimal string with `.` as the separator and no
6
+ * grouping, whatever the browser's locale is. Strings are already in that form and pass through.
7
+ *
8
+ * `Number.prototype.toString()` is exactly that format. The locale-aware versions this used to call
9
+ * are not: under a comma-decimal locale (nl, de, fr, ...) it ran `.replace('.', '')` over a string
10
+ * that always uses `.`, so a score of 0.5 was stored as "05" and 1.5 as "15" — an item's SCORE came
11
+ * out ten times too big for every value with a fraction. Under a dot-decimal locale
12
+ * `toLocaleString()` grouped thousands instead, turning 1234.5 into "1,234.5".
13
+ */
4
14
  export const convertNumberToUniversalFormat = (number) => {
5
- // check if type is string
6
- if (typeof number === 'string') {
7
- return number;
8
- }
9
- const dSep = decimalSeparator();
10
- if (dSep === '.') {
11
- return number.toLocaleString();
12
- }
13
- else {
14
- return number.toString().replace('.', '').replace(dSep, '.');
15
- }
15
+ return typeof number === 'string' ? number : number.toString();
16
16
  };
17
17
  export function IsNullOrUndefined(value) {
18
18
  return value === null || value === undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/utils/utils.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,EAAE;IACnC,OAAO,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAChE,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAAC,MAAuB,EAAE,EAAE;IACxE,0BAA0B;IAC1B,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC/B,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,MAAM,IAAI,GAAG,gBAAgB,EAAE,CAAC;IAChC,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;QACjB,OAAO,MAAM,CAAC,cAAc,EAAE,CAAC;IACjC,CAAC;SAAM,CAAC;QACN,OAAO,MAAM,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC/D,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,UAAU,iBAAiB,CAAC,KAAc;IAC9C,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC7C,MAAM,oBAAoB,GAAG,GAAG;SAC7B,OAAO,CAAC,cAAc,EAAE,IAAI,CAAC;SAC7B,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC;SACrB,OAAO,CAAC,QAAQ,EAAE,SAAS,CAAC;SAC5B,OAAO,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAClC,OAAO,oBAAoB,CAAC;AAC9B,CAAC","sourcesContent":["export const decimalSeparator = () => {\n return new Intl.NumberFormat().format(0.1).replace(/\\d/g, '');\n};\n\nexport const convertNumberToUniversalFormat = (number: number | string) => {\n // check if type is string\n if (typeof number === 'string') {\n return number;\n }\n const dSep = decimalSeparator();\n if (dSep === '.') {\n return number.toLocaleString();\n } else {\n return number.toString().replace('.', '').replace(dSep, '.');\n }\n};\n\nexport function IsNullOrUndefined(value: unknown) {\n return value === null || value === undefined;\n}\n\nexport function removeDoubleSlashes(str: string) {\n const singleForwardSlashes = str\n .replace(/([^:]\\/)\\/+/g, '$1')\n .replace(/\\/\\//g, '/')\n .replace('http:/', 'http://')\n .replace('https:/', 'https://');\n return singleForwardSlashes;\n}\n"]}
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/utils/utils.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAG,EAAE;IACnC,OAAO,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;AAChE,CAAC,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAAC,MAAuB,EAAE,EAAE;IACxE,OAAO,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;AACjE,CAAC,CAAC;AAEF,MAAM,UAAU,iBAAiB,CAAC,KAAc;IAC9C,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,CAAC;AAC/C,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC7C,MAAM,oBAAoB,GAAG,GAAG;SAC7B,OAAO,CAAC,cAAc,EAAE,IAAI,CAAC;SAC7B,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC;SACrB,OAAO,CAAC,QAAQ,EAAE,SAAS,CAAC;SAC5B,OAAO,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IAClC,OAAO,oBAAoB,CAAC;AAC9B,CAAC","sourcesContent":["export const decimalSeparator = () => {\n return new Intl.NumberFormat().format(0.1).replace(/\\d/g, '');\n};\n\n/**\n * Renders a number the way QTI stores one: a plain decimal string with `.` as the separator and no\n * grouping, whatever the browser's locale is. Strings are already in that form and pass through.\n *\n * `Number.prototype.toString()` is exactly that format. The locale-aware versions this used to call\n * are not: under a comma-decimal locale (nl, de, fr, ...) it ran `.replace('.', '')` over a string\n * that always uses `.`, so a score of 0.5 was stored as \"05\" and 1.5 as \"15\" — an item's SCORE came\n * out ten times too big for every value with a fraction. Under a dot-decimal locale\n * `toLocaleString()` grouped thousands instead, turning 1234.5 into \"1,234.5\".\n */\nexport const convertNumberToUniversalFormat = (number: number | string) => {\n return typeof number === 'string' ? number : number.toString();\n};\n\nexport function IsNullOrUndefined(value: unknown) {\n return value === null || value === undefined;\n}\n\nexport function removeDoubleSlashes(str: string) {\n const singleForwardSlashes = str\n .replace(/([^:]\\/)\\/+/g, '$1')\n .replace(/\\/\\//g, '/')\n .replace('http:/', 'http://')\n .replace('https:/', 'https://');\n return singleForwardSlashes;\n}\n"]}
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@qti-components/base",
3
3
  "description": "QTI shared utilities",
4
- "version": "2.2.0",
4
+ "version": "2.4.0",
5
5
  "author": "",
6
6
  "dependencies": {
7
7
  "@qti-components/utilities": "^1.3.3"
8
8
  },
9
9
  "devDependencies": {
10
10
  "@lit/context": "^1.1.6",
11
- "lit": "^3.3.3"
11
+ "lit": "^3.3.1"
12
12
  },
13
13
  "keywords": [
14
14
  "qti",
@@ -50,7 +50,7 @@
50
50
  },
51
51
  "scripts": {
52
52
  "attw": "attw --profile esm-only --pack",
53
- "build": "tsc --paths null",
53
+ "build": "pnpm run clean && tsc --paths null",
54
54
  "clean": "rm -rf dist",
55
55
  "yalc": "yalc publish",
56
56
  "yalc:push": "yalc push"