@superbuilders/incept-renderer 0.1.11 → 0.1.13
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 +200 -1
- package/dist/actions/index.d.ts +4 -1
- package/dist/actions/index.js +65 -6
- package/dist/actions/index.js.map +1 -1
- package/dist/components/index.d.ts +2 -3
- package/dist/components/index.js +14 -1
- package/dist/components/index.js.map +1 -1
- package/dist/index.d.ts +6 -11
- package/dist/index.js +65 -1
- package/dist/index.js.map +1 -1
- package/dist/parser-B8n3iHSM.d.ts +29 -0
- package/dist/qti-stimulus-renderer-CSuLfoff.d.ts +178 -0
- package/dist/{schema-DZoGAQdF.d.ts → schema-DKduufCs.d.ts} +106 -128
- package/dist/styles/duolingo.css +20 -0
- package/dist/styles/duolingo.css.map +1 -1
- package/dist/styles/themes.css +237 -0
- package/dist/styles/themes.css.map +1 -1
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
- package/dist/types-B7YRTQKt.d.ts +0 -102
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ A secure, server-driven QTI 3.0 assessment renderer for React/Next.js applicatio
|
|
|
12
12
|
- [Server Functions](#server-functions)
|
|
13
13
|
- [Client Components](#client-components)
|
|
14
14
|
- [Types](#types)
|
|
15
|
+
- [Rendering Stimuli](#rendering-stimuli)
|
|
15
16
|
- [Theming](#theming)
|
|
16
17
|
- [Complete Examples](#complete-examples)
|
|
17
18
|
- [Supported Interactions](#supported-interactions)
|
|
@@ -317,6 +318,29 @@ const result = await validateResponsesSecure(qtiXmlString, {
|
|
|
317
318
|
}
|
|
318
319
|
```
|
|
319
320
|
|
|
321
|
+
#### `parseAssessmentStimulusXml(xml: string)`
|
|
322
|
+
|
|
323
|
+
Parses QTI Assessment Stimulus XML (reading materials, passages).
|
|
324
|
+
|
|
325
|
+
```tsx
|
|
326
|
+
import { parseAssessmentStimulusXml } from "@superbuilders/incept-renderer"
|
|
327
|
+
|
|
328
|
+
const stimulus = parseAssessmentStimulusXml(stimulusXmlString)
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**Parameters:**
|
|
332
|
+
- `xml` (string) - Raw QTI 3.0 Assessment Stimulus XML string
|
|
333
|
+
|
|
334
|
+
**Returns:**
|
|
335
|
+
```tsx
|
|
336
|
+
{
|
|
337
|
+
identifier: string // Unique identifier from the XML
|
|
338
|
+
title: string // Human-readable title
|
|
339
|
+
xmlLang: string // Language code (e.g., "en")
|
|
340
|
+
bodyHtml: string // Sanitized HTML content ready for rendering
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
320
344
|
---
|
|
321
345
|
|
|
322
346
|
### Client Components
|
|
@@ -357,6 +381,26 @@ import { QTIRenderer } from "@superbuilders/incept-renderer"
|
|
|
357
381
|
| `responseFeedback` | `Record<string, { isCorrect: boolean; messageHtml?: string }>` | ❌ | Per-response feedback messages |
|
|
358
382
|
| `theme` | `"duolingo" \| "neobrutalist" \| string` | ❌ | Visual theme (default: `"duolingo"`) |
|
|
359
383
|
|
|
384
|
+
#### `<QTIStimulusRenderer />`
|
|
385
|
+
|
|
386
|
+
Renders QTI Assessment Stimuli (reading materials, passages).
|
|
387
|
+
|
|
388
|
+
```tsx
|
|
389
|
+
import { QTIStimulusRenderer } from "@superbuilders/incept-renderer"
|
|
390
|
+
|
|
391
|
+
<QTIStimulusRenderer
|
|
392
|
+
stimulus={parsedStimulus}
|
|
393
|
+
className="my-8"
|
|
394
|
+
/>
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**Props:**
|
|
398
|
+
|
|
399
|
+
| Prop | Type | Required | Description |
|
|
400
|
+
|------|------|----------|-------------|
|
|
401
|
+
| `stimulus` | `AssessmentStimulus` | ✅ | Parsed stimulus from `parseAssessmentStimulusXml` |
|
|
402
|
+
| `className` | `string` | ❌ | Additional CSS classes for the container |
|
|
403
|
+
|
|
360
404
|
---
|
|
361
405
|
|
|
362
406
|
### Types
|
|
@@ -365,12 +409,15 @@ Import from `@superbuilders/incept-renderer`:
|
|
|
365
409
|
|
|
366
410
|
```tsx
|
|
367
411
|
import type {
|
|
412
|
+
// Assessment Items (questions)
|
|
368
413
|
DisplayItem,
|
|
369
414
|
DisplayBlock,
|
|
370
415
|
DisplayChoice,
|
|
371
416
|
DisplayChoiceInteraction,
|
|
372
417
|
FormShape,
|
|
373
|
-
ValidateResult
|
|
418
|
+
ValidateResult,
|
|
419
|
+
// Assessment Stimuli (reading materials)
|
|
420
|
+
AssessmentStimulus
|
|
374
421
|
} from "@superbuilders/incept-renderer"
|
|
375
422
|
```
|
|
376
423
|
|
|
@@ -435,6 +482,158 @@ interface ValidateResult {
|
|
|
435
482
|
}
|
|
436
483
|
```
|
|
437
484
|
|
|
485
|
+
#### `AssessmentStimulus`
|
|
486
|
+
|
|
487
|
+
```tsx
|
|
488
|
+
interface AssessmentStimulus {
|
|
489
|
+
identifier: string // Unique identifier from the XML
|
|
490
|
+
title: string // Human-readable title
|
|
491
|
+
xmlLang: string // Language code (e.g., "en", "es")
|
|
492
|
+
bodyHtml: string // Sanitized HTML content
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## Rendering Stimuli
|
|
499
|
+
|
|
500
|
+
In addition to assessment items (questions), the package supports rendering **Assessment Stimuli** — reading materials, passages, or articles that provide context for questions.
|
|
501
|
+
|
|
502
|
+
### What are Stimuli?
|
|
503
|
+
|
|
504
|
+
QTI Assessment Stimuli (`qti-assessment-stimulus`) are standalone content blocks that contain:
|
|
505
|
+
- Reading passages
|
|
506
|
+
- Articles with images
|
|
507
|
+
- Reference materials
|
|
508
|
+
- Any HTML content that accompanies questions
|
|
509
|
+
|
|
510
|
+
Unlike assessment items, stimuli have no interactions or correct answers — they're purely display content.
|
|
511
|
+
|
|
512
|
+
### Parsing Stimulus XML
|
|
513
|
+
|
|
514
|
+
Use `parseAssessmentStimulusXml` to parse stimulus XML:
|
|
515
|
+
|
|
516
|
+
```tsx
|
|
517
|
+
import { parseAssessmentStimulusXml } from "@superbuilders/incept-renderer"
|
|
518
|
+
|
|
519
|
+
const stimulusXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
520
|
+
<qti-assessment-stimulus
|
|
521
|
+
xmlns="http://www.imsglobal.org/xsd/imsqtiasi_v3p0"
|
|
522
|
+
identifier="stimulus-1"
|
|
523
|
+
xml:lang="en"
|
|
524
|
+
title="Biodiversity and Ecosystem Health">
|
|
525
|
+
<qti-stimulus-body>
|
|
526
|
+
<h2>Biodiversity and ecosystem health</h2>
|
|
527
|
+
<p><strong>Biodiversity</strong> is the variety of species in an ecosystem.</p>
|
|
528
|
+
<figure>
|
|
529
|
+
<img src="coral-reef.jpg" alt="A coral reef" />
|
|
530
|
+
<figcaption>Coral reef ecosystems have high biodiversity.</figcaption>
|
|
531
|
+
</figure>
|
|
532
|
+
</qti-stimulus-body>
|
|
533
|
+
</qti-assessment-stimulus>`
|
|
534
|
+
|
|
535
|
+
const stimulus = parseAssessmentStimulusXml(stimulusXml)
|
|
536
|
+
// Returns: { identifier, title, xmlLang, bodyHtml }
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### Rendering Stimuli
|
|
540
|
+
|
|
541
|
+
Use the `QTIStimulusRenderer` component to render parsed stimuli:
|
|
542
|
+
|
|
543
|
+
```tsx
|
|
544
|
+
import { QTIStimulusRenderer, parseAssessmentStimulusXml } from "@superbuilders/incept-renderer"
|
|
545
|
+
|
|
546
|
+
function ReadingPassage({ xml }: { xml: string }) {
|
|
547
|
+
const stimulus = parseAssessmentStimulusXml(xml)
|
|
548
|
+
|
|
549
|
+
return (
|
|
550
|
+
<QTIStimulusRenderer
|
|
551
|
+
stimulus={stimulus}
|
|
552
|
+
className="my-8"
|
|
553
|
+
/>
|
|
554
|
+
)
|
|
555
|
+
}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### Server Action Pattern
|
|
559
|
+
|
|
560
|
+
For Next.js apps, create a server action to parse stimuli:
|
|
561
|
+
|
|
562
|
+
```tsx
|
|
563
|
+
// lib/qti-actions.ts
|
|
564
|
+
"use server"
|
|
565
|
+
|
|
566
|
+
import { parseAssessmentStimulusXml } from "@superbuilders/incept-renderer"
|
|
567
|
+
|
|
568
|
+
export async function parseStimulus(xml: string) {
|
|
569
|
+
return parseAssessmentStimulusXml(xml)
|
|
570
|
+
}
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
```tsx
|
|
574
|
+
// app/reading/[id]/page.tsx
|
|
575
|
+
import { parseStimulus } from "@/lib/qti-actions"
|
|
576
|
+
import { QTIStimulusRenderer } from "@superbuilders/incept-renderer"
|
|
577
|
+
|
|
578
|
+
export default async function ReadingPage({ params }: { params: Promise<{ id: string }> }) {
|
|
579
|
+
const { id } = await params
|
|
580
|
+
const xml = await fetchStimulusXml(id) // Your data fetching
|
|
581
|
+
const stimulus = await parseStimulus(xml)
|
|
582
|
+
|
|
583
|
+
return <QTIStimulusRenderer stimulus={stimulus} />
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### Stimulus with Questions
|
|
588
|
+
|
|
589
|
+
A common pattern is showing a stimulus alongside related questions:
|
|
590
|
+
|
|
591
|
+
```tsx
|
|
592
|
+
import { QTIStimulusRenderer, QTIRenderer } from "@superbuilders/incept-renderer"
|
|
593
|
+
|
|
594
|
+
function QuestionWithPassage({ stimulus, item, ...props }) {
|
|
595
|
+
return (
|
|
596
|
+
<div className="grid grid-cols-2 gap-8">
|
|
597
|
+
{/* Reading passage on the left */}
|
|
598
|
+
<div className="overflow-y-auto max-h-[80vh]">
|
|
599
|
+
<QTIStimulusRenderer stimulus={stimulus} />
|
|
600
|
+
</div>
|
|
601
|
+
|
|
602
|
+
{/* Question on the right */}
|
|
603
|
+
<div>
|
|
604
|
+
<QTIRenderer item={item} {...props} />
|
|
605
|
+
</div>
|
|
606
|
+
</div>
|
|
607
|
+
)
|
|
608
|
+
}
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### AssessmentStimulus Type
|
|
612
|
+
|
|
613
|
+
```tsx
|
|
614
|
+
interface AssessmentStimulus {
|
|
615
|
+
identifier: string // Unique ID from the XML
|
|
616
|
+
title: string // Human-readable title
|
|
617
|
+
xmlLang: string // Language code (e.g., "en")
|
|
618
|
+
bodyHtml: string // Sanitized HTML content
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
### Supported Content
|
|
623
|
+
|
|
624
|
+
The stimulus renderer supports:
|
|
625
|
+
- **Headings** (`h1`–`h6`)
|
|
626
|
+
- **Text formatting** (`p`, `strong`, `em`, `b`, `i`, `u`, `sub`, `sup`)
|
|
627
|
+
- **Lists** (`ul`, `ol`, `li`)
|
|
628
|
+
- **Images** (`img`, `figure`, `figcaption`)
|
|
629
|
+
- **Tables** (`table`, `thead`, `tbody`, `tr`, `th`, `td`)
|
|
630
|
+
- **Links** (`a`)
|
|
631
|
+
- **MathML** (mathematical expressions)
|
|
632
|
+
- **Collapsible sections** (`details`, `summary`)
|
|
633
|
+
- **Semantic elements** (`article`, `section`, `blockquote`, `cite`)
|
|
634
|
+
|
|
635
|
+
All content is sanitized to prevent XSS attacks while preserving safe HTML structure.
|
|
636
|
+
|
|
438
637
|
---
|
|
439
638
|
|
|
440
639
|
## Theming
|
package/dist/actions/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
export { a as parseAssessmentStimulusXml } from '../parser-B8n3iHSM.js';
|
|
2
|
+
import { k as DisplayItem, m as FormShape, V as ValidateResult } from '../schema-DKduufCs.js';
|
|
3
|
+
export { a as AssessmentStimulus } from '../schema-DKduufCs.js';
|
|
4
|
+
import 'zod';
|
|
2
5
|
|
|
3
6
|
declare function buildDisplayModelFromXml(qtiXml: string): {
|
|
4
7
|
itemKey: string;
|
package/dist/actions/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { createHash } from 'crypto';
|
|
2
1
|
import * as errors from '@superbuilders/errors';
|
|
3
|
-
import * as logger2 from '@superbuilders/slog';
|
|
4
2
|
import { XMLParser } from 'fast-xml-parser';
|
|
5
3
|
import { z } from 'zod';
|
|
4
|
+
import { createHash } from 'crypto';
|
|
5
|
+
import * as logger2 from '@superbuilders/slog';
|
|
6
6
|
|
|
7
|
-
// src/
|
|
7
|
+
// src/parser.ts
|
|
8
8
|
|
|
9
9
|
// src/html/sanitize.ts
|
|
10
10
|
var DEFAULT_CONFIG = {
|
|
@@ -16,6 +16,13 @@ var DEFAULT_CONFIG = {
|
|
|
16
16
|
"div",
|
|
17
17
|
"br",
|
|
18
18
|
"hr",
|
|
19
|
+
// Headings
|
|
20
|
+
"h1",
|
|
21
|
+
"h2",
|
|
22
|
+
"h3",
|
|
23
|
+
"h4",
|
|
24
|
+
"h5",
|
|
25
|
+
"h6",
|
|
19
26
|
// Formatting
|
|
20
27
|
"b",
|
|
21
28
|
"i",
|
|
@@ -65,6 +72,9 @@ var DEFAULT_CONFIG = {
|
|
|
65
72
|
"figcaption",
|
|
66
73
|
"blockquote",
|
|
67
74
|
"cite",
|
|
75
|
+
// Interactive
|
|
76
|
+
"details",
|
|
77
|
+
"summary",
|
|
68
78
|
// Links
|
|
69
79
|
"a",
|
|
70
80
|
// Forms (for future interactive elements)
|
|
@@ -575,6 +585,16 @@ var AssessmentItemSchema = z.object({
|
|
|
575
585
|
itemBody: ItemBodySchema,
|
|
576
586
|
responseProcessing: ResponseProcessingSchema
|
|
577
587
|
});
|
|
588
|
+
var AssessmentStimulusSchema = z.object({
|
|
589
|
+
/** Unique identifier for the stimulus */
|
|
590
|
+
identifier: z.string().min(1),
|
|
591
|
+
/** Human-readable title */
|
|
592
|
+
title: z.string().default(""),
|
|
593
|
+
/** Language code (e.g., "en", "es") */
|
|
594
|
+
xmlLang: z.string().default("en"),
|
|
595
|
+
/** The HTML content of the stimulus body */
|
|
596
|
+
bodyHtml: z.string()
|
|
597
|
+
});
|
|
578
598
|
|
|
579
599
|
// src/parser.ts
|
|
580
600
|
function createXmlParser() {
|
|
@@ -1200,8 +1220,47 @@ function parseAssessmentItemXml(xml) {
|
|
|
1200
1220
|
}
|
|
1201
1221
|
return validation.data;
|
|
1202
1222
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1223
|
+
function parseAssessmentStimulusXml(xml) {
|
|
1224
|
+
if (!xml || typeof xml !== "string") {
|
|
1225
|
+
throw errors.new("xml input must be a non-empty string");
|
|
1226
|
+
}
|
|
1227
|
+
const parser = createXmlParser();
|
|
1228
|
+
const parseResult = errors.trySync(() => {
|
|
1229
|
+
return parser.parse(xml, true);
|
|
1230
|
+
});
|
|
1231
|
+
if (parseResult.error) {
|
|
1232
|
+
throw errors.wrap(parseResult.error, "xml parse");
|
|
1233
|
+
}
|
|
1234
|
+
const raw = parseResult.data;
|
|
1235
|
+
if (!Array.isArray(raw)) {
|
|
1236
|
+
throw errors.new("expected xml parser to output an array for preserveOrder");
|
|
1237
|
+
}
|
|
1238
|
+
const normalizedTree = raw.map(normalizeNode).filter((n) => typeof n !== "string");
|
|
1239
|
+
const rootNode = normalizedTree.find(
|
|
1240
|
+
(n) => n.tagName === "qti-assessment-stimulus" || n.tagName.endsWith("assessment-stimulus")
|
|
1241
|
+
);
|
|
1242
|
+
if (!rootNode) {
|
|
1243
|
+
throw errors.new("qti assessment stimulus not found in xml document");
|
|
1244
|
+
}
|
|
1245
|
+
const rootChildren = rootNode.children.filter((c) => typeof c !== "string");
|
|
1246
|
+
const stimulusBodyNode = rootChildren.find((n) => n.tagName === "qti-stimulus-body");
|
|
1247
|
+
if (!stimulusBodyNode) {
|
|
1248
|
+
throw errors.new("qti-stimulus-body not found in stimulus document");
|
|
1249
|
+
}
|
|
1250
|
+
const bodyHtml = getInnerHtml(stimulusBodyNode);
|
|
1251
|
+
const normalizedStimulus = {
|
|
1252
|
+
identifier: coerceString(rootNode.attrs.identifier),
|
|
1253
|
+
title: coerceString(rootNode.attrs.title),
|
|
1254
|
+
xmlLang: coerceString(rootNode.attrs["xml:lang"]) || coerceString(rootNode.attrs["xml-lang"]) || "en",
|
|
1255
|
+
bodyHtml
|
|
1256
|
+
};
|
|
1257
|
+
const validation = AssessmentStimulusSchema.safeParse(normalizedStimulus);
|
|
1258
|
+
if (!validation.success) {
|
|
1259
|
+
const errorDetails = validation.error.issues.map((err) => `${err.path.join(".")}: ${err.message}`).join("; ");
|
|
1260
|
+
throw errors.new(`qti stimulus validation: ${errorDetails}`);
|
|
1261
|
+
}
|
|
1262
|
+
return validation.data;
|
|
1263
|
+
}
|
|
1205
1264
|
function shuffleArray(items) {
|
|
1206
1265
|
const arr = items.slice();
|
|
1207
1266
|
for (let i = arr.length - 1; i > 0; i--) {
|
|
@@ -1688,6 +1747,6 @@ async function validateResponsesSecure(qtiXml, responses) {
|
|
|
1688
1747
|
return validateResponsesFromXml(qtiXml, responses);
|
|
1689
1748
|
}
|
|
1690
1749
|
|
|
1691
|
-
export { buildDisplayModel, buildDisplayModelFromXml, validateResponsesFromXml, validateResponsesSecure };
|
|
1750
|
+
export { buildDisplayModel, buildDisplayModelFromXml, parseAssessmentStimulusXml, validateResponsesFromXml, validateResponsesSecure };
|
|
1692
1751
|
//# sourceMappingURL=index.js.map
|
|
1693
1752
|
//# sourceMappingURL=index.js.map
|