@longsightgroup/qti3-core 0.7.1 → 0.7.3
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/dist/content-text.d.ts +8 -0
- package/dist/content-text.d.ts.map +1 -0
- package/dist/content-text.js +44 -0
- package/dist/content-text.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +18 -4
- package/dist/parser.js.map +1 -1
- package/dist/pattern-mask.d.ts +4 -0
- package/dist/pattern-mask.d.ts.map +1 -0
- package/dist/pattern-mask.js +22 -0
- package/dist/pattern-mask.js.map +1 -0
- package/dist/shared-vocabulary-generated-families.js +1 -1
- package/dist/shared-vocabulary-generated-families.js.map +1 -1
- package/dist/shared-vocabulary-support.d.ts.map +1 -1
- package/dist/shared-vocabulary-support.js +5 -2
- package/dist/shared-vocabulary-support.js.map +1 -1
- package/dist/shared-vocabulary-validation.d.ts +1 -0
- package/dist/shared-vocabulary-validation.d.ts.map +1 -1
- package/dist/shared-vocabulary-validation.js +10 -1
- package/dist/shared-vocabulary-validation.js.map +1 -1
- package/dist/shared-vocabulary.d.ts +12 -0
- package/dist/shared-vocabulary.d.ts.map +1 -1
- package/dist/shared-vocabulary.js +26 -0
- package/dist/shared-vocabulary.js.map +1 -1
- package/dist/support.js +40 -2
- package/dist/support.js.map +1 -1
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +17 -0
- package/dist/validation.js.map +1 -1
- package/dist/xml.d.ts.map +1 -1
- package/dist/xml.js +24 -1
- package/dist/xml.js.map +1 -1
- package/package.json +1 -1
- package/src/content-text.ts +61 -0
- package/src/index.ts +6 -0
- package/src/parser.ts +24 -4
- package/src/pattern-mask.ts +20 -0
- package/src/shared-vocabulary-generated-families.ts +1 -1
- package/src/shared-vocabulary-support.ts +5 -2
- package/src/shared-vocabulary-validation.ts +12 -1
- package/src/shared-vocabulary.ts +41 -0
- package/src/support.ts +42 -2
- package/src/types.ts +6 -0
- package/src/validation.ts +19 -0
- package/src/xml.ts +24 -1
package/src/parser.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { flatTextFromContent } from "./content-text.js";
|
|
1
2
|
import { getInteractionSupport, interactionNameToType, processingSupport } from "./support.js";
|
|
2
3
|
import type {
|
|
3
4
|
QtiAssessmentItem,
|
|
@@ -480,13 +481,22 @@ function parseInteraction(
|
|
|
480
481
|
(child) => child.localName === "object" || child.localName === "img",
|
|
481
482
|
)[0];
|
|
482
483
|
|
|
484
|
+
const promptContent = prompt
|
|
485
|
+
? parseContentChildren(prompt, diagnostics, responseDeclarationMap, [])
|
|
486
|
+
: undefined;
|
|
487
|
+
|
|
483
488
|
return {
|
|
484
489
|
type: interactionType ?? "custom",
|
|
485
490
|
qtiName: node.localName,
|
|
486
491
|
responseIdentifier,
|
|
487
492
|
responseCardinality: responseDeclaration?.cardinality,
|
|
488
493
|
responseBaseType: responseDeclaration?.baseType,
|
|
489
|
-
prompt:
|
|
494
|
+
prompt: promptContent
|
|
495
|
+
? flatTextFromContent(promptContent, { excludeAnnotations: true }) || undefined
|
|
496
|
+
: prompt
|
|
497
|
+
? textContent(prompt)
|
|
498
|
+
: undefined,
|
|
499
|
+
promptContent: promptContent && promptContent.length > 0 ? promptContent : undefined,
|
|
490
500
|
promptAttributes: prompt?.attributes,
|
|
491
501
|
promptSource: prompt?.source,
|
|
492
502
|
contextText: inlineInteractionContext(node, interactionType),
|
|
@@ -499,7 +509,7 @@ function parseInteraction(
|
|
|
499
509
|
interactionType === "portableCustom"
|
|
500
510
|
? parsePortableCustomDefinition(node, diagnostics)
|
|
501
511
|
: undefined,
|
|
502
|
-
choices: parseChoices(node),
|
|
512
|
+
choices: parseChoices(node, diagnostics, responseDeclarationMap),
|
|
503
513
|
hottextSegments: interactionType === "hottext" ? parseHottextSegments(node) : undefined,
|
|
504
514
|
gapMatchSegments:
|
|
505
515
|
interactionType === "gapMatch" || interactionType === "graphicGapMatch"
|
|
@@ -965,7 +975,11 @@ function assetTypeFromData(data: string | undefined): string | undefined {
|
|
|
965
975
|
return undefined;
|
|
966
976
|
}
|
|
967
977
|
|
|
968
|
-
function parseChoices(
|
|
978
|
+
function parseChoices(
|
|
979
|
+
node: XmlNode,
|
|
980
|
+
diagnostics: QtiDiagnostic[],
|
|
981
|
+
responseDeclarationMap: Map<string, QtiResponseDeclaration>,
|
|
982
|
+
): QtiChoice[] {
|
|
969
983
|
const choiceNames = new Set([
|
|
970
984
|
"qti-simple-choice",
|
|
971
985
|
"qti-simple-associable-choice",
|
|
@@ -981,14 +995,20 @@ function parseChoices(node: XmlNode): QtiChoice[] {
|
|
|
981
995
|
return descendants(node, (child) => choiceNames.has(child.localName)).map((choice, index) => {
|
|
982
996
|
const identifier = choice.attributes.identifier ?? "";
|
|
983
997
|
const asset = parseChoiceAsset(choice);
|
|
998
|
+
const content = parseContentChildren(choice, diagnostics, responseDeclarationMap, []);
|
|
999
|
+
const flatChoiceText =
|
|
1000
|
+
content.length > 0
|
|
1001
|
+
? flatTextFromContent(content, { excludeAnnotations: true })
|
|
1002
|
+
: textContent(choice);
|
|
984
1003
|
return {
|
|
985
1004
|
identifier,
|
|
986
1005
|
text:
|
|
987
|
-
|
|
1006
|
+
flatChoiceText ||
|
|
988
1007
|
choice.attributes["object-label"] ||
|
|
989
1008
|
asset?.text ||
|
|
990
1009
|
identifier ||
|
|
991
1010
|
`Choice ${index + 1}`,
|
|
1011
|
+
content: content.length > 0 ? content : undefined,
|
|
992
1012
|
asset,
|
|
993
1013
|
role: choiceRole(choice),
|
|
994
1014
|
qtiName: choice.localName,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Compiles a QTI pattern-mask attribute value for full-string matching. */
|
|
2
|
+
export function compileQtiPatternMask(patternMask: string | undefined): RegExp | undefined {
|
|
3
|
+
if (patternMask === undefined) return undefined;
|
|
4
|
+
let mask = patternMask.trim();
|
|
5
|
+
if (mask.length === 0) return undefined;
|
|
6
|
+
|
|
7
|
+
mask = mask.startsWith("^") ? mask.slice(1) : mask;
|
|
8
|
+
mask = mask.endsWith("$") ? mask.slice(0, -1) : mask;
|
|
9
|
+
if (mask.length === 0) return undefined;
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
return new RegExp(`^(?:${mask.replaceAll("/", "\\/")})$`);
|
|
13
|
+
} catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isValidQtiPatternMask(patternMask: string): boolean {
|
|
19
|
+
return compileQtiPatternMask(patternMask) !== undefined;
|
|
20
|
+
}
|
|
@@ -237,7 +237,7 @@ export const sharedVocabularyMatrixCoverageFamilies: SharedVocabularyMatrixCover
|
|
|
237
237
|
levels: ["stylesheet"],
|
|
238
238
|
coveredBy: ["qti-writing-orientation-vertical-rl"],
|
|
239
239
|
rationale:
|
|
240
|
-
"Choice writing-orientation classes share
|
|
240
|
+
"Choice and inline-choice writing-orientation classes share vertical rendering paths; vertical-rl covers the representative orientation rule.",
|
|
241
241
|
matches: (className) => choiceWritingOrientationPattern.test(className),
|
|
242
242
|
},
|
|
243
243
|
];
|
|
@@ -39,7 +39,10 @@ const browserBehaviorTests = [
|
|
|
39
39
|
"tests/browser/player-dom-behavior.spec.ts",
|
|
40
40
|
...sharedVocabularyMatrixTests,
|
|
41
41
|
];
|
|
42
|
-
const graphicBrowserTests = [
|
|
42
|
+
const graphicBrowserTests = [
|
|
43
|
+
"tests/browser/player-graphic.spec.ts",
|
|
44
|
+
"tests/browser/player-graphic-gap-match.spec.ts",
|
|
45
|
+
];
|
|
43
46
|
const mediaBrowserTests = ["tests/browser/player.spec.ts", ...sharedVocabularyMatrixTests];
|
|
44
47
|
const mediaPlayerFixture =
|
|
45
48
|
"packages/fixtures/packages/sv-matrix/items/media-controls-and-pause.xml";
|
|
@@ -226,7 +229,7 @@ export const sharedVocabularyClassSupport: SharedVocabularyClassSupport[] = [
|
|
|
226
229
|
...SHARED_VOCABULARY_CHOICE_WRITING_ORIENTATIONS.map((orientation) =>
|
|
227
230
|
interactionStylesheetEntry(
|
|
228
231
|
`qti-writing-orientation-${orientation}`,
|
|
229
|
-
["choice"],
|
|
232
|
+
["choice", "inlineChoice"],
|
|
230
233
|
[...browserBehaviorTests],
|
|
231
234
|
),
|
|
232
235
|
),
|
|
@@ -97,6 +97,7 @@ export function validateSharedVocabularyInputWidth(
|
|
|
97
97
|
export interface SharedVocabularyExtendedTextValidationOptions {
|
|
98
98
|
classNames: string[];
|
|
99
99
|
subjectQtiName: string;
|
|
100
|
+
expectedLength?: string | undefined;
|
|
100
101
|
path?: string | undefined;
|
|
101
102
|
source?: QtiSourceLocation | undefined;
|
|
102
103
|
}
|
|
@@ -122,7 +123,7 @@ export function validateSharedVocabularyExtendedTextHeightLines(
|
|
|
122
123
|
export function validateSharedVocabularyExtendedTextCounter(
|
|
123
124
|
options: SharedVocabularyExtendedTextValidationOptions,
|
|
124
125
|
): QtiDiagnostic[] {
|
|
125
|
-
const { classNames, subjectQtiName, path, source } = options;
|
|
126
|
+
const { classNames, subjectQtiName, expectedLength, path, source } = options;
|
|
126
127
|
const diagnostics: QtiDiagnostic[] = [];
|
|
127
128
|
const counterClasses = supportedExtendedTextCounterClassNames(classNames);
|
|
128
129
|
if (new Set(counterClasses).size > 1) {
|
|
@@ -135,6 +136,16 @@ export function validateSharedVocabularyExtendedTextCounter(
|
|
|
135
136
|
});
|
|
136
137
|
}
|
|
137
138
|
|
|
139
|
+
if (counterClasses.length > 0 && !expectedLength) {
|
|
140
|
+
diagnostics.push({
|
|
141
|
+
code: "interaction.sharedVocabulary.extendedTextCounterExpectedLength",
|
|
142
|
+
severity: "warning",
|
|
143
|
+
message: `${subjectQtiName} uses ${[...new Set(counterClasses)].join(", ")} but does not define expected-length; qti-counter-* shared vocabulary depends on expected-length.`,
|
|
144
|
+
path,
|
|
145
|
+
source,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
138
149
|
for (const className of classNames) {
|
|
139
150
|
if (!className.startsWith("qti-counter-")) continue;
|
|
140
151
|
if (isSupportedExtendedTextCounterClassName(className)) continue;
|
package/src/shared-vocabulary.ts
CHANGED
|
@@ -106,6 +106,47 @@ export function extendedTextCounterPosition(
|
|
|
106
106
|
return extendedTextCounterPositionFromAttributes(interaction.attributes);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
export function extendedTextExpectedLengthFromAttributes(
|
|
110
|
+
attributes: Record<string, string>,
|
|
111
|
+
): number | undefined {
|
|
112
|
+
const raw = attributes["expected-length"];
|
|
113
|
+
if (raw === undefined) return undefined;
|
|
114
|
+
const length = Number(raw);
|
|
115
|
+
if (!Number.isFinite(length) || length < 0) return undefined;
|
|
116
|
+
return length;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface ExtendedTextCounterState {
|
|
120
|
+
position: SharedVocabularyExtendedTextCounterPosition;
|
|
121
|
+
expectedLength: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Counter chrome is authored only when both qti-counter-* and a valid expected-length are present. */
|
|
125
|
+
export function extendedTextCounterStateFromAttributes(
|
|
126
|
+
attributes: Record<string, string>,
|
|
127
|
+
): ExtendedTextCounterState | undefined {
|
|
128
|
+
const position = extendedTextCounterPositionFromAttributes(attributes);
|
|
129
|
+
if (position === undefined) return undefined;
|
|
130
|
+
const expectedLength = extendedTextExpectedLengthFromAttributes(attributes);
|
|
131
|
+
if (expectedLength === undefined) return undefined;
|
|
132
|
+
return { position, expectedLength };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function extendedTextCounterState(
|
|
136
|
+
interaction: QtiInteraction,
|
|
137
|
+
): ExtendedTextCounterState | undefined {
|
|
138
|
+
return extendedTextCounterStateFromAttributes(interaction.attributes);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function extendedTextCounterValues(
|
|
142
|
+
position: SharedVocabularyExtendedTextCounterPosition,
|
|
143
|
+
valueLength: number,
|
|
144
|
+
expectedLength: number,
|
|
145
|
+
): { count: number; expectedLength: number } {
|
|
146
|
+
const count = position === "down" ? Math.max(0, expectedLength - valueLength) : valueLength;
|
|
147
|
+
return { count, expectedLength };
|
|
148
|
+
}
|
|
149
|
+
|
|
109
150
|
export function supportedInputWidthClassNames(classNames: string[]): string[] {
|
|
110
151
|
return classNames.filter((className) => {
|
|
111
152
|
const value = inputWidthClassPattern.exec(className)?.[1];
|
package/src/support.ts
CHANGED
|
@@ -10,7 +10,7 @@ export const interactionSupport: QtiInteractionElementSupport[] = [
|
|
|
10
10
|
entry("qti-choice-interaction", "choice"),
|
|
11
11
|
entry("qti-drawing-interaction", "drawing"),
|
|
12
12
|
entry("qti-end-attempt-interaction", "endAttempt"),
|
|
13
|
-
|
|
13
|
+
extendedTextInteractionEntry(),
|
|
14
14
|
entry("qti-gap-match-interaction", "gapMatch"),
|
|
15
15
|
entry("qti-graphic-associate-interaction", "graphicAssociate"),
|
|
16
16
|
entry("qti-graphic-gap-match-interaction", "graphicGapMatch"),
|
|
@@ -25,7 +25,7 @@ export const interactionSupport: QtiInteractionElementSupport[] = [
|
|
|
25
25
|
pciEntry(),
|
|
26
26
|
entry("qti-select-point-interaction", "selectPoint"),
|
|
27
27
|
entry("qti-slider-interaction", "slider"),
|
|
28
|
-
|
|
28
|
+
textEntryInteractionEntry(),
|
|
29
29
|
entry("qti-upload-interaction", "upload"),
|
|
30
30
|
];
|
|
31
31
|
|
|
@@ -225,6 +225,46 @@ function entry(qtiName: string, interactionType: QtiInteractionType): QtiInterac
|
|
|
225
225
|
};
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
function extendedTextInteractionEntry(): QtiInteractionElementSupport {
|
|
229
|
+
return {
|
|
230
|
+
...entry("qti-extended-text-interaction", "extendedText"),
|
|
231
|
+
fixtures: [
|
|
232
|
+
"packages/fixtures/xml/extendedText-reference.xml",
|
|
233
|
+
"packages/fixtures/packages/sv-matrix/items/extended-text-pattern-mask.xml",
|
|
234
|
+
"packages/fixtures/packages/sv-matrix/items/extended-text-xhtml.xml",
|
|
235
|
+
],
|
|
236
|
+
tests: [
|
|
237
|
+
"packages/fixtures/src/fixtures.test.ts",
|
|
238
|
+
"packages/conformance/src/conformance.test.ts",
|
|
239
|
+
"packages/a11y/src/a11y.test.ts",
|
|
240
|
+
"packages/core/src/pattern-mask.test.ts",
|
|
241
|
+
"tests/browser/player.spec.ts",
|
|
242
|
+
"tests/browser/player-dom-behavior.spec.ts",
|
|
243
|
+
"tests/browser/player-extended-text-xhtml.spec.ts",
|
|
244
|
+
],
|
|
245
|
+
notes: "Supports plain and format=xhtml extended text.",
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function textEntryInteractionEntry(): QtiInteractionElementSupport {
|
|
250
|
+
return {
|
|
251
|
+
...entry("qti-text-entry-interaction", "textEntry"),
|
|
252
|
+
fixtures: [
|
|
253
|
+
"packages/fixtures/xml/textEntry-reference.xml",
|
|
254
|
+
"packages/fixtures/packages/sv-matrix/items/text-entry-pattern-mask-inline.xml",
|
|
255
|
+
],
|
|
256
|
+
tests: [
|
|
257
|
+
"packages/fixtures/src/fixtures.test.ts",
|
|
258
|
+
"packages/conformance/src/conformance.test.ts",
|
|
259
|
+
"packages/a11y/src/a11y.test.ts",
|
|
260
|
+
"packages/core/src/pattern-mask.test.ts",
|
|
261
|
+
"tests/browser/player.spec.ts",
|
|
262
|
+
"tests/browser/player-dom-behavior.spec.ts",
|
|
263
|
+
],
|
|
264
|
+
notes: "Supports placeholder-text and pattern-mask on text-entry controls.",
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
228
268
|
function pciEntry(): QtiInteractionElementSupport {
|
|
229
269
|
return {
|
|
230
270
|
...entry("qti-portable-custom-interaction", "portableCustom"),
|
package/src/types.ts
CHANGED
|
@@ -138,7 +138,10 @@ export interface QtiTemplateDeclaration extends QtiVariableDeclaration {
|
|
|
138
138
|
|
|
139
139
|
export interface QtiChoice {
|
|
140
140
|
identifier: string;
|
|
141
|
+
/** Plain-text fallback used for speech, TTS, diagnostics, and native controls. */
|
|
141
142
|
text: string;
|
|
143
|
+
/** Parsed visual content for renderers that can display rich choice markup such as MathML. */
|
|
144
|
+
content?: QtiContentNode[] | undefined;
|
|
142
145
|
asset?: QtiObjectAsset | undefined;
|
|
143
146
|
role: QtiChoiceRole;
|
|
144
147
|
qtiName: string;
|
|
@@ -192,7 +195,10 @@ export interface QtiInteraction {
|
|
|
192
195
|
responseIdentifier?: string | undefined;
|
|
193
196
|
responseCardinality?: QtiCardinality | undefined;
|
|
194
197
|
responseBaseType?: QtiBaseType | undefined;
|
|
198
|
+
/** Flat accessibility/fallback label derived from prompt content with annotations stripped. */
|
|
195
199
|
prompt?: string | undefined;
|
|
200
|
+
/** Structured prompt content for visual rendering. When present, block headings render this instead of prompt. */
|
|
201
|
+
promptContent?: QtiContentNode[] | undefined;
|
|
196
202
|
promptAttributes?: Record<string, string> | undefined;
|
|
197
203
|
promptSource?: QtiSourceLocation | undefined;
|
|
198
204
|
contextText?: string | undefined;
|
package/src/validation.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
validateSharedVocabularyInputWidth,
|
|
27
27
|
validateSharedVocabularyMediaPlayerControls,
|
|
28
28
|
} from "./shared-vocabulary-validation.js";
|
|
29
|
+
import { isValidQtiPatternMask } from "./pattern-mask.js";
|
|
29
30
|
import { validateQtiDataSsmlMetadata } from "./tts.js";
|
|
30
31
|
import { qtiValueToStringList } from "./value-format.js";
|
|
31
32
|
|
|
@@ -1672,6 +1673,7 @@ function validateInteractions(item: QtiAssessmentItem, diagnostics: QtiDiagnosti
|
|
|
1672
1673
|
validateInteractionRequiredAttributes(interaction, diagnostics);
|
|
1673
1674
|
validatePortableCustomInteraction(interaction, item, diagnostics);
|
|
1674
1675
|
validateInteractionLimitAttributes(interaction, diagnostics);
|
|
1676
|
+
validatePatternMaskAttribute(interaction, diagnostics);
|
|
1675
1677
|
validateGraphicHotspotObjectDimensions(interaction, diagnostics);
|
|
1676
1678
|
validateCorrectResponseReferences(
|
|
1677
1679
|
interaction,
|
|
@@ -1724,6 +1726,7 @@ function validateInteractionSharedVocabulary(
|
|
|
1724
1726
|
...validateSharedVocabularyExtendedText({
|
|
1725
1727
|
classNames,
|
|
1726
1728
|
subjectQtiName: interaction.qtiName,
|
|
1729
|
+
expectedLength: interaction.attributes["expected-length"],
|
|
1727
1730
|
path: interaction.source?.path,
|
|
1728
1731
|
source: interaction.source,
|
|
1729
1732
|
}),
|
|
@@ -2526,6 +2529,22 @@ function invalidNumber(
|
|
|
2526
2529
|
});
|
|
2527
2530
|
}
|
|
2528
2531
|
|
|
2532
|
+
function validatePatternMaskAttribute(
|
|
2533
|
+
interaction: QtiInteraction,
|
|
2534
|
+
diagnostics: QtiDiagnostic[],
|
|
2535
|
+
): void {
|
|
2536
|
+
if (interaction.type !== "textEntry" && interaction.type !== "extendedText") return;
|
|
2537
|
+
const patternMask = interaction.attributes["pattern-mask"];
|
|
2538
|
+
if (patternMask === undefined || isValidQtiPatternMask(patternMask)) return;
|
|
2539
|
+
diagnostics.push({
|
|
2540
|
+
code: "interaction.patternMask.invalid",
|
|
2541
|
+
severity: "error",
|
|
2542
|
+
message: `${interaction.qtiName} pattern-mask is not a valid regular expression.`,
|
|
2543
|
+
path: interaction.source?.path,
|
|
2544
|
+
source: interaction.source,
|
|
2545
|
+
});
|
|
2546
|
+
}
|
|
2547
|
+
|
|
2529
2548
|
function validateInteractionLimitAttributes(
|
|
2530
2549
|
interaction: QtiInteraction,
|
|
2531
2550
|
diagnostics: QtiDiagnostic[],
|
package/src/xml.ts
CHANGED
|
@@ -142,6 +142,12 @@ const inlineMixedContentChildNames = new Set([
|
|
|
142
142
|
"i",
|
|
143
143
|
"kbd",
|
|
144
144
|
"mark",
|
|
145
|
+
"math",
|
|
146
|
+
"mi",
|
|
147
|
+
"mn",
|
|
148
|
+
"mo",
|
|
149
|
+
"mrow",
|
|
150
|
+
"msup",
|
|
145
151
|
"q",
|
|
146
152
|
"rp",
|
|
147
153
|
"rt",
|
|
@@ -174,6 +180,8 @@ function restoreMixedContentFromSource(xml: string, node: XmlNode): void {
|
|
|
174
180
|
if (typeof entry !== "string") restoreMixedContentFromSource(xml, entry);
|
|
175
181
|
}
|
|
176
182
|
|
|
183
|
+
restoreLeafTextFromSource(xml, node);
|
|
184
|
+
|
|
177
185
|
if (!shouldRestoreMixedContentWhitespace(node)) return;
|
|
178
186
|
|
|
179
187
|
const contentEndOffset = node.endSource?.offset ?? node.sourceRange.endOffset;
|
|
@@ -203,11 +211,26 @@ function restoreMixedContentFromSource(xml: string, node: XmlNode): void {
|
|
|
203
211
|
node.text = restored.filter((entry): entry is string => typeof entry === "string").join("");
|
|
204
212
|
}
|
|
205
213
|
|
|
214
|
+
function restoreLeafTextFromSource(xml: string, node: XmlNode): void {
|
|
215
|
+
if (node.children.length > 0) return;
|
|
216
|
+
const contentEndOffset = node.endSource?.offset ?? node.sourceRange.endOffset;
|
|
217
|
+
if (node.sourceRange.startTagEndOffset < 0 || contentEndOffset === undefined) return;
|
|
218
|
+
const raw = xml.slice(node.sourceRange.startTagEndOffset + 1, contentEndOffset);
|
|
219
|
+
if (raw.includes("<![CDATA[")) return;
|
|
220
|
+
const decoded = decodeXmlCharacterData(stripNonCharacterMarkup(raw));
|
|
221
|
+
node.text = decoded;
|
|
222
|
+
node.content = decoded.length > 0 ? [decoded] : [];
|
|
223
|
+
}
|
|
224
|
+
|
|
206
225
|
function appendDecodedTextSegment(content: Array<string | XmlNode>, raw: string): void {
|
|
207
|
-
const decoded = decodeXmlCharacterData(raw);
|
|
226
|
+
const decoded = decodeXmlCharacterData(stripNonCharacterMarkup(raw));
|
|
208
227
|
if (decoded.length > 0) content.push(decoded);
|
|
209
228
|
}
|
|
210
229
|
|
|
230
|
+
function stripNonCharacterMarkup(value: string): string {
|
|
231
|
+
return value.replace(/<!--[\s\S]*?-->/g, "").replace(/<\?[\s\S]*?\?>/g, "");
|
|
232
|
+
}
|
|
233
|
+
|
|
211
234
|
const predefinedXmlEntities: Record<string, string> = {
|
|
212
235
|
amp: "&",
|
|
213
236
|
apos: "'",
|