@mathscan/math-exercise-engine 1.0.8 → 1.0.10
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/index.d.ts +1384 -397
- package/dist/index.js +623 -16
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +623 -16
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +238 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -36,6 +36,10 @@ const AST_TYPES = {
|
|
|
36
36
|
TABLE_ROW: "TableRow",
|
|
37
37
|
/** Cell within a table row */
|
|
38
38
|
TABLE_CELL: "TableCell",
|
|
39
|
+
/** Number line layout block */
|
|
40
|
+
NUMBER_LINE_BLOCK: "NumberLineBlock",
|
|
41
|
+
/** A position marker on the number line */
|
|
42
|
+
NUMBER_LINE_POSITION: "NumberLinePosition",
|
|
39
43
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
40
44
|
// Tools
|
|
41
45
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -88,6 +92,10 @@ const JSON_TYPES = {
|
|
|
88
92
|
TABLE_ROW: "table-row",
|
|
89
93
|
/** Cell within a table */
|
|
90
94
|
TABLE_CELL: "table-cell",
|
|
95
|
+
/** Number line layout block */
|
|
96
|
+
NUMBER_LINE: "number-line",
|
|
97
|
+
/** Position marker within a number line */
|
|
98
|
+
NUMBER_LINE_POSITION: "number-line-position",
|
|
91
99
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
92
100
|
// Widget Types
|
|
93
101
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -100,6 +108,7 @@ const JSON_TYPES = {
|
|
|
100
108
|
/** Numeric keypad widget */
|
|
101
109
|
NUMERIC_KEYPAD: "numeric-keypad"
|
|
102
110
|
};
|
|
111
|
+
const DRAG_OPTIONS_WIDGET_ID = "_drag_options";
|
|
103
112
|
const VALIDATION_MODES = {
|
|
104
113
|
/** Simple answer matching validation */
|
|
105
114
|
SIMPLE: "simple",
|
|
@@ -112,6 +121,9 @@ const INPUT_TYPES = {
|
|
|
112
121
|
/** Numeric input (digits only) */
|
|
113
122
|
NUMERIC: "numeric"
|
|
114
123
|
};
|
|
124
|
+
const NUMBER_LINE_STYLES = ["arrow-right", "arrow-left", "arrow-both", "line"];
|
|
125
|
+
const SIDE_PAD = 7;
|
|
126
|
+
const CSS_VAR_SIDE_PAD = "--nl-side-pad";
|
|
115
127
|
const Newline = chevrotain.createToken({
|
|
116
128
|
name: "Newline",
|
|
117
129
|
pattern: /\r?\n/,
|
|
@@ -185,6 +197,14 @@ const NUMERIC_INPUT = chevrotain.createToken({
|
|
|
185
197
|
name: "NUMERIC_INPUT",
|
|
186
198
|
pattern: /NUMERIC_INPUT/
|
|
187
199
|
});
|
|
200
|
+
const NUMBER_LINE = chevrotain.createToken({
|
|
201
|
+
name: "NUMBER_LINE",
|
|
202
|
+
pattern: /NUMBER_LINE/
|
|
203
|
+
});
|
|
204
|
+
const POSITION = chevrotain.createToken({
|
|
205
|
+
name: "POSITION",
|
|
206
|
+
pattern: /POSITION/
|
|
207
|
+
});
|
|
188
208
|
const TABLE = chevrotain.createToken({
|
|
189
209
|
name: "TABLE",
|
|
190
210
|
pattern: /TABLE/
|
|
@@ -299,6 +319,8 @@ const allTokens = [
|
|
|
299
319
|
WIDGET_ANSWER,
|
|
300
320
|
W_N_I,
|
|
301
321
|
NUMERIC_INPUT,
|
|
322
|
+
NUMBER_LINE,
|
|
323
|
+
POSITION,
|
|
302
324
|
TABLE,
|
|
303
325
|
ROW,
|
|
304
326
|
CELL,
|
|
@@ -388,14 +410,14 @@ const MathExerciseLexer = new chevrotain.Lexer(allTokens, {
|
|
|
388
410
|
positionTracking: "full"
|
|
389
411
|
});
|
|
390
412
|
function tokenize(input) {
|
|
391
|
-
const result = MathExerciseLexer.tokenize(input);
|
|
413
|
+
const result = MathExerciseLexer.tokenize(input.replace(/\u00A0/g, " "));
|
|
392
414
|
if (result.errors.length > 0) {
|
|
393
415
|
throw new LexerError(result.errors);
|
|
394
416
|
}
|
|
395
417
|
return result.tokens;
|
|
396
418
|
}
|
|
397
419
|
function tokenizeSafe(input) {
|
|
398
|
-
const result = MathExerciseLexer.tokenize(input);
|
|
420
|
+
const result = MathExerciseLexer.tokenize(input.replace(/\u00A0/g, " "));
|
|
399
421
|
return {
|
|
400
422
|
tokens: result.tokens,
|
|
401
423
|
errors: result.errors
|
|
@@ -423,7 +445,9 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
423
445
|
this.MANY3(() => {
|
|
424
446
|
this.SUBRULE(this.tool);
|
|
425
447
|
});
|
|
426
|
-
this.
|
|
448
|
+
this.OPTION2(() => {
|
|
449
|
+
this.SUBRULE(this.validation);
|
|
450
|
+
});
|
|
427
451
|
this.MANY4(() => {
|
|
428
452
|
this.OR2([
|
|
429
453
|
{ ALT: () => this.CONSUME2(Newline) },
|
|
@@ -453,6 +477,10 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
453
477
|
GATE: this.BACKTRACK(this.radioButtons),
|
|
454
478
|
ALT: () => this.SUBRULE(this.radioButtons)
|
|
455
479
|
},
|
|
480
|
+
{
|
|
481
|
+
GATE: this.BACKTRACK(this.numberLine),
|
|
482
|
+
ALT: () => this.SUBRULE(this.numberLine)
|
|
483
|
+
},
|
|
456
484
|
{ ALT: () => this.SUBRULE(this.line) }
|
|
457
485
|
]);
|
|
458
486
|
});
|
|
@@ -557,6 +585,79 @@ class MathExerciseParser extends chevrotain.CstParser {
|
|
|
557
585
|
]);
|
|
558
586
|
});
|
|
559
587
|
});
|
|
588
|
+
this.numberLine = this.RULE("numberLine", () => {
|
|
589
|
+
this.MANY(() => {
|
|
590
|
+
this.OR2([
|
|
591
|
+
{ ALT: () => this.CONSUME(Space) },
|
|
592
|
+
{ ALT: () => this.CONSUME(Newline) }
|
|
593
|
+
]);
|
|
594
|
+
});
|
|
595
|
+
this.CONSUME(NUMBER_LINE);
|
|
596
|
+
this.CONSUME(LBracket);
|
|
597
|
+
this.SUBRULE(this.parameterList);
|
|
598
|
+
this.CONSUME(RBracket);
|
|
599
|
+
this.MANY2(() => {
|
|
600
|
+
this.OR3([
|
|
601
|
+
{ ALT: () => this.CONSUME2(Space) },
|
|
602
|
+
{ ALT: () => this.CONSUME2(Newline) }
|
|
603
|
+
]);
|
|
604
|
+
});
|
|
605
|
+
this.CONSUME(LBrace);
|
|
606
|
+
this.MANY3(() => {
|
|
607
|
+
this.OR4([
|
|
608
|
+
{ ALT: () => this.CONSUME3(Space) },
|
|
609
|
+
{ ALT: () => this.CONSUME3(Newline) }
|
|
610
|
+
]);
|
|
611
|
+
});
|
|
612
|
+
this.MANY4(() => {
|
|
613
|
+
this.SUBRULE(this.position);
|
|
614
|
+
});
|
|
615
|
+
this.CONSUME(RBrace);
|
|
616
|
+
this.MANY5(() => {
|
|
617
|
+
this.OR5([
|
|
618
|
+
{ ALT: () => this.CONSUME4(Newline) },
|
|
619
|
+
{ ALT: () => this.CONSUME4(Space) }
|
|
620
|
+
]);
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
this.position = this.RULE("position", () => {
|
|
624
|
+
this.MANY(() => {
|
|
625
|
+
this.OR([
|
|
626
|
+
{ ALT: () => this.CONSUME(Space) },
|
|
627
|
+
{ ALT: () => this.CONSUME(Newline) }
|
|
628
|
+
]);
|
|
629
|
+
});
|
|
630
|
+
this.CONSUME(POSITION);
|
|
631
|
+
this.CONSUME(LBracket);
|
|
632
|
+
this.SUBRULE(this.parameterList);
|
|
633
|
+
this.CONSUME(RBracket);
|
|
634
|
+
this.MANY2(() => {
|
|
635
|
+
this.OR2([
|
|
636
|
+
{ ALT: () => this.CONSUME2(Space) },
|
|
637
|
+
{ ALT: () => this.CONSUME2(Newline) }
|
|
638
|
+
]);
|
|
639
|
+
});
|
|
640
|
+
this.CONSUME(LBrace);
|
|
641
|
+
this.MANY3(() => {
|
|
642
|
+
this.OR3([
|
|
643
|
+
{ ALT: () => this.CONSUME3(Space) },
|
|
644
|
+
{ ALT: () => this.CONSUME3(Newline) }
|
|
645
|
+
]);
|
|
646
|
+
});
|
|
647
|
+
this.MANY4(() => {
|
|
648
|
+
this.OR4([
|
|
649
|
+
{ ALT: () => this.SUBRULE(this.inlineElement) },
|
|
650
|
+
{ ALT: () => this.CONSUME4(Newline) }
|
|
651
|
+
]);
|
|
652
|
+
});
|
|
653
|
+
this.CONSUME(RBrace);
|
|
654
|
+
this.MANY5(() => {
|
|
655
|
+
this.OR5([
|
|
656
|
+
{ ALT: () => this.CONSUME5(Space) },
|
|
657
|
+
{ ALT: () => this.CONSUME5(Newline) }
|
|
658
|
+
]);
|
|
659
|
+
});
|
|
660
|
+
});
|
|
560
661
|
this.line = this.RULE("line", () => {
|
|
561
662
|
this.MANY(() => {
|
|
562
663
|
this.CONSUME(Space);
|
|
@@ -919,6 +1020,71 @@ function isTextBlock(value) {
|
|
|
919
1020
|
function isLineBlock(value) {
|
|
920
1021
|
return LineBlockSchema.safeParse(value).success;
|
|
921
1022
|
}
|
|
1023
|
+
const NumberLinePositionSchema = zod.z.object({
|
|
1024
|
+
type: zod.z.literal(AST_TYPES.NUMBER_LINE_POSITION),
|
|
1025
|
+
value: zod.z.number(),
|
|
1026
|
+
inlines: zod.z.array(InlineNodeSchema).min(1)
|
|
1027
|
+
});
|
|
1028
|
+
function createNumberLinePosition(value, inlines) {
|
|
1029
|
+
const hasRealContent = inlines.some((n) => n.type !== AST_TYPES.SPACE);
|
|
1030
|
+
if (!hasRealContent) {
|
|
1031
|
+
throw new Error(`POSITION[value=${value}]: content cannot be empty or whitespace only`);
|
|
1032
|
+
}
|
|
1033
|
+
const inputs = inlines.filter((i) => i.type === AST_TYPES.INLINE_INPUT);
|
|
1034
|
+
if (inputs.length > 1) {
|
|
1035
|
+
throw new Error(
|
|
1036
|
+
`POSITION[value=${value}]: only one input widget is allowed, got ${inputs.length}`
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
return NumberLinePositionSchema.parse({
|
|
1040
|
+
type: AST_TYPES.NUMBER_LINE_POSITION,
|
|
1041
|
+
value,
|
|
1042
|
+
inlines
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
const NumberLineBlockSchema = zod.z.object({
|
|
1046
|
+
type: zod.z.literal(AST_TYPES.NUMBER_LINE_BLOCK),
|
|
1047
|
+
min: zod.z.number(),
|
|
1048
|
+
max: zod.z.number(),
|
|
1049
|
+
step: zod.z.number().positive({ message: "NUMBER_LINE: step must be greater than 0" }).default(1),
|
|
1050
|
+
minorTicks: zod.z.number().int().nonnegative({ message: "NUMBER_LINE: minorTicks must be >= 0" }).optional(),
|
|
1051
|
+
style: zod.z.enum(NUMBER_LINE_STYLES, {
|
|
1052
|
+
errorMap: () => ({
|
|
1053
|
+
message: `NUMBER_LINE: style must be one of ${NUMBER_LINE_STYLES.join(", ")}`
|
|
1054
|
+
})
|
|
1055
|
+
}).optional(),
|
|
1056
|
+
positions: zod.z.array(NumberLinePositionSchema)
|
|
1057
|
+
}).refine((data) => data.min < data.max, {
|
|
1058
|
+
message: "NUMBER_LINE: min must be less than max"
|
|
1059
|
+
}).superRefine((data, ctx) => {
|
|
1060
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1061
|
+
for (const pos of data.positions) {
|
|
1062
|
+
if (pos.value < data.min || pos.value > data.max) {
|
|
1063
|
+
ctx.addIssue({
|
|
1064
|
+
code: zod.z.ZodIssueCode.custom,
|
|
1065
|
+
message: `NUMBER_LINE: POSITION value (${pos.value}) must be within [${data.min}, ${data.max}]`
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
if (seen.has(pos.value)) {
|
|
1069
|
+
ctx.addIssue({
|
|
1070
|
+
code: zod.z.ZodIssueCode.custom,
|
|
1071
|
+
message: `NUMBER_LINE: duplicate POSITION value (${pos.value})`
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
seen.add(pos.value);
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
function createNumberLineBlock(min, max, positions, step, minorTicks, style) {
|
|
1078
|
+
return NumberLineBlockSchema.parse({
|
|
1079
|
+
type: AST_TYPES.NUMBER_LINE_BLOCK,
|
|
1080
|
+
min,
|
|
1081
|
+
max,
|
|
1082
|
+
step,
|
|
1083
|
+
minorTicks,
|
|
1084
|
+
style,
|
|
1085
|
+
positions
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
922
1088
|
const TableCellSchema = zod.z.object({
|
|
923
1089
|
type: zod.z.literal(AST_TYPES.TABLE_CELL),
|
|
924
1090
|
colspan: zod.z.number().int().positive().optional(),
|
|
@@ -1033,13 +1199,13 @@ function isExpressionValidation(value) {
|
|
|
1033
1199
|
function isValidationNode(value) {
|
|
1034
1200
|
return ValidationNodeSchema.safeParse(value).success;
|
|
1035
1201
|
}
|
|
1036
|
-
const ContentBlockSchema = zod.z.union([LineBlockSchema, TableBlockSchema, RadioOptionsBlockSchema]);
|
|
1202
|
+
const ContentBlockSchema = zod.z.union([LineBlockSchema, TableBlockSchema, RadioOptionsBlockSchema, NumberLineBlockSchema]);
|
|
1037
1203
|
const ExerciseDocumentSchema = zod.z.object({
|
|
1038
1204
|
type: zod.z.literal(AST_TYPES.EXERCISE_DOCUMENT),
|
|
1039
1205
|
question: TextBlockSchema.optional(),
|
|
1040
1206
|
lines: zod.z.array(ContentBlockSchema),
|
|
1041
1207
|
tools: zod.z.array(ToolNodeSchema).optional(),
|
|
1042
|
-
validation: ValidationNodeSchema
|
|
1208
|
+
validation: ValidationNodeSchema.optional()
|
|
1043
1209
|
});
|
|
1044
1210
|
function createExerciseDocument(question, lines, validation, tools) {
|
|
1045
1211
|
return ExerciseDocumentSchema.parse({
|
|
@@ -1066,7 +1232,7 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
1066
1232
|
const question = ctx.question ? this.visit(ctx.question) : void 0;
|
|
1067
1233
|
const contentBlocks = ctx.contentBlock ? ctx.contentBlock.map((cb) => this.visit(cb)) : [];
|
|
1068
1234
|
const tools = ctx.tool ? ctx.tool.map((t) => this.visit(t)) : [];
|
|
1069
|
-
const validation = this.visit(ctx.validation);
|
|
1235
|
+
const validation = ctx.validation ? this.visit(ctx.validation) : void 0;
|
|
1070
1236
|
return createExerciseDocument(question, contentBlocks, validation, tools);
|
|
1071
1237
|
}
|
|
1072
1238
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -1083,6 +1249,7 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
1083
1249
|
contentBlock(ctx) {
|
|
1084
1250
|
if (ctx.table) return this.visit(ctx.table);
|
|
1085
1251
|
if (ctx.radioButtons) return this.visit(ctx.radioButtons);
|
|
1252
|
+
if (ctx.numberLine) return this.visit(ctx.numberLine);
|
|
1086
1253
|
if (ctx.line) return this.visit(ctx.line);
|
|
1087
1254
|
throw new Error("Unknown content block");
|
|
1088
1255
|
}
|
|
@@ -1113,6 +1280,31 @@ class NaturalDSLVisitor extends BaseCstVisitor {
|
|
|
1113
1280
|
return createTableCell(inlines, colspan);
|
|
1114
1281
|
}
|
|
1115
1282
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1283
|
+
// Number Line Block
|
|
1284
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1285
|
+
numberLine(ctx) {
|
|
1286
|
+
const params = this.visit(ctx.parameterList);
|
|
1287
|
+
if (params.min === void 0 || params.max === void 0) {
|
|
1288
|
+
throw new Error("NUMBER_LINE requires parameters: min and max");
|
|
1289
|
+
}
|
|
1290
|
+
const min = Number(params.min);
|
|
1291
|
+
const max = Number(params.max);
|
|
1292
|
+
const step = params.step !== void 0 ? Number(params.step) : void 0;
|
|
1293
|
+
const minorTicks = params.minorTicks !== void 0 ? Number(params.minorTicks) : void 0;
|
|
1294
|
+
const style = params.style;
|
|
1295
|
+
const positions = ctx.position ? ctx.position.map((p) => this.visit(p)) : [];
|
|
1296
|
+
return createNumberLineBlock(min, max, positions, step, minorTicks, style);
|
|
1297
|
+
}
|
|
1298
|
+
position(ctx) {
|
|
1299
|
+
const params = this.visit(ctx.parameterList);
|
|
1300
|
+
if (params.value === void 0) {
|
|
1301
|
+
throw new Error("POSITION requires parameter: value");
|
|
1302
|
+
}
|
|
1303
|
+
const value = Number(params.value);
|
|
1304
|
+
const inlines = ctx.inlineElement ? ctx.inlineElement.map((elem) => this.visit(elem)) : [];
|
|
1305
|
+
return createNumberLinePosition(value, inlines);
|
|
1306
|
+
}
|
|
1307
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1116
1308
|
// Line Block
|
|
1117
1309
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1118
1310
|
line(ctx) {
|
|
@@ -1413,7 +1605,8 @@ const LayoutItemSchema = zod.z.lazy(
|
|
|
1413
1605
|
LinebreakSchema,
|
|
1414
1606
|
GroupItemSchema,
|
|
1415
1607
|
OperatorItemSchema,
|
|
1416
|
-
TableItemSchema
|
|
1608
|
+
TableItemSchema,
|
|
1609
|
+
NumberLineItemSchema
|
|
1417
1610
|
])
|
|
1418
1611
|
);
|
|
1419
1612
|
const GroupItemSchema = zod.z.object({
|
|
@@ -1438,6 +1631,22 @@ const TableItemSchema = zod.z.object({
|
|
|
1438
1631
|
cols: zod.z.number().int().positive(),
|
|
1439
1632
|
rows: zod.z.array(TableRowItemSchema)
|
|
1440
1633
|
});
|
|
1634
|
+
const NumberLinePositionItemSchema = zod.z.object({
|
|
1635
|
+
id: zod.z.string(),
|
|
1636
|
+
type: zod.z.literal(JSON_TYPES.NUMBER_LINE_POSITION),
|
|
1637
|
+
value: zod.z.number(),
|
|
1638
|
+
content: zod.z.array(zod.z.lazy(() => LayoutItemSchema))
|
|
1639
|
+
});
|
|
1640
|
+
const NumberLineItemSchema = zod.z.object({
|
|
1641
|
+
id: zod.z.string(),
|
|
1642
|
+
type: zod.z.literal(JSON_TYPES.NUMBER_LINE),
|
|
1643
|
+
min: zod.z.number(),
|
|
1644
|
+
max: zod.z.number(),
|
|
1645
|
+
step: zod.z.number().positive(),
|
|
1646
|
+
minorTicks: zod.z.number().int().nonnegative().optional(),
|
|
1647
|
+
style: zod.z.enum(NUMBER_LINE_STYLES).optional(),
|
|
1648
|
+
positions: zod.z.array(NumberLinePositionItemSchema)
|
|
1649
|
+
});
|
|
1441
1650
|
const StringInputJSONSchema = zod.z.object({
|
|
1442
1651
|
type: zod.z.literal(JSON_TYPES.INLINE_INPUT),
|
|
1443
1652
|
inputType: zod.z.literal(INPUT_TYPES.STRING),
|
|
@@ -1488,7 +1697,7 @@ const ExerciseJSONSchema = zod.z.object({
|
|
|
1488
1697
|
id: zod.z.string().optional(),
|
|
1489
1698
|
layout: zod.z.array(LayoutItemSchema),
|
|
1490
1699
|
widgets: zod.z.record(WidgetJSONSchema),
|
|
1491
|
-
validation: ValidationJSONSchema,
|
|
1700
|
+
validation: ValidationJSONSchema.optional(),
|
|
1492
1701
|
meta: ExerciseMetaSchema.optional()
|
|
1493
1702
|
});
|
|
1494
1703
|
function validateExerciseJSON(data) {
|
|
@@ -1509,7 +1718,7 @@ function isQuestionText(item) {
|
|
|
1509
1718
|
function isTextItem$1(item) {
|
|
1510
1719
|
return item.type === JSON_TYPES.TEXT;
|
|
1511
1720
|
}
|
|
1512
|
-
function isWidgetRef(item) {
|
|
1721
|
+
function isWidgetRef$1(item) {
|
|
1513
1722
|
return item.type === JSON_TYPES.WIDGET_REF;
|
|
1514
1723
|
}
|
|
1515
1724
|
function isHighlightedText(item) {
|
|
@@ -1578,6 +1787,8 @@ function toJSON(ast) {
|
|
|
1578
1787
|
for (const block of ast.lines) {
|
|
1579
1788
|
if (block.type === AST_TYPES.TABLE_BLOCK) {
|
|
1580
1789
|
layout.push(transformTable(block, idGenerator));
|
|
1790
|
+
} else if (block.type === AST_TYPES.NUMBER_LINE_BLOCK) {
|
|
1791
|
+
layout.push(transformNumberLine(block, idGenerator));
|
|
1581
1792
|
} else if (block.type === AST_TYPES.RADIO_OPTIONS) {
|
|
1582
1793
|
const radioBlock = block;
|
|
1583
1794
|
layout.push({
|
|
@@ -1643,13 +1854,12 @@ function toJSON(ast) {
|
|
|
1643
1854
|
}
|
|
1644
1855
|
}
|
|
1645
1856
|
}
|
|
1646
|
-
const validation = transformValidation(ast.validation, answers, widgets);
|
|
1647
1857
|
return {
|
|
1648
1858
|
version: "2.0",
|
|
1649
1859
|
// Natural DSL version
|
|
1650
1860
|
layout,
|
|
1651
1861
|
widgets,
|
|
1652
|
-
validation
|
|
1862
|
+
...ast.validation ? { validation: transformValidation(ast.validation, answers, widgets) } : {}
|
|
1653
1863
|
};
|
|
1654
1864
|
}
|
|
1655
1865
|
function collectAllInlines(blocks) {
|
|
@@ -1663,10 +1873,36 @@ function collectAllInlines(blocks) {
|
|
|
1663
1873
|
result.push(...cell.inlines);
|
|
1664
1874
|
}
|
|
1665
1875
|
}
|
|
1876
|
+
} else if (block.type === AST_TYPES.NUMBER_LINE_BLOCK) {
|
|
1877
|
+
for (const position of block.positions) {
|
|
1878
|
+
result.push(...position.inlines);
|
|
1879
|
+
}
|
|
1666
1880
|
}
|
|
1667
1881
|
}
|
|
1668
1882
|
return result;
|
|
1669
1883
|
}
|
|
1884
|
+
function transformNumberLine(numberLine, idGenerator) {
|
|
1885
|
+
const positions = numberLine.positions.map((pos) => {
|
|
1886
|
+
const content = pos.inlines.map((inline) => transformInline(inline, idGenerator));
|
|
1887
|
+
return {
|
|
1888
|
+
id: idGenerator.generate(JSON_TYPES.NUMBER_LINE_POSITION),
|
|
1889
|
+
type: JSON_TYPES.NUMBER_LINE_POSITION,
|
|
1890
|
+
value: pos.value,
|
|
1891
|
+
content
|
|
1892
|
+
};
|
|
1893
|
+
});
|
|
1894
|
+
const item = {
|
|
1895
|
+
id: idGenerator.generate(JSON_TYPES.NUMBER_LINE),
|
|
1896
|
+
type: JSON_TYPES.NUMBER_LINE,
|
|
1897
|
+
min: numberLine.min,
|
|
1898
|
+
max: numberLine.max,
|
|
1899
|
+
step: numberLine.step,
|
|
1900
|
+
positions
|
|
1901
|
+
};
|
|
1902
|
+
if (numberLine.minorTicks !== void 0) item.minorTicks = numberLine.minorTicks;
|
|
1903
|
+
if (numberLine.style !== void 0) item.style = numberLine.style;
|
|
1904
|
+
return item;
|
|
1905
|
+
}
|
|
1670
1906
|
function transformTable(table, idGenerator) {
|
|
1671
1907
|
const rows = table.rows.map((row) => {
|
|
1672
1908
|
const cells = row.cells.map((cell) => {
|
|
@@ -2094,11 +2330,18 @@ function findValidCombination(expression, ranges) {
|
|
|
2094
2330
|
return null;
|
|
2095
2331
|
}
|
|
2096
2332
|
function validate(exercise, userInputs) {
|
|
2097
|
-
|
|
2333
|
+
if (!exercise.validation) {
|
|
2334
|
+
return {
|
|
2335
|
+
isCorrect: true,
|
|
2336
|
+
details: {}
|
|
2337
|
+
};
|
|
2338
|
+
}
|
|
2339
|
+
const validatedExercise = exercise;
|
|
2340
|
+
switch (validatedExercise.validation.mode) {
|
|
2098
2341
|
case VALIDATION_MODES.SIMPLE:
|
|
2099
|
-
return validateSimple(
|
|
2342
|
+
return validateSimple(validatedExercise, userInputs);
|
|
2100
2343
|
case VALIDATION_MODES.EXPRESSION:
|
|
2101
|
-
return validateExpression(
|
|
2344
|
+
return validateExpression(validatedExercise, userInputs);
|
|
2102
2345
|
default:
|
|
2103
2346
|
return {
|
|
2104
2347
|
isCorrect: false,
|
|
@@ -2109,6 +2352,9 @@ function validate(exercise, userInputs) {
|
|
|
2109
2352
|
}
|
|
2110
2353
|
function getMissingInputs(exercise, userInputs) {
|
|
2111
2354
|
const missing = [];
|
|
2355
|
+
if (!exercise.validation) {
|
|
2356
|
+
return missing;
|
|
2357
|
+
}
|
|
2112
2358
|
if (exercise.validation.mode === VALIDATION_MODES.SIMPLE && exercise.validation.answers) {
|
|
2113
2359
|
for (const widgetId of Object.keys(exercise.validation.answers)) {
|
|
2114
2360
|
if (!(widgetId in userInputs) || userInputs[widgetId] === "") {
|
|
@@ -2148,6 +2394,8 @@ function validateInputFormats(exercise, userInputs) {
|
|
|
2148
2394
|
const DragItemTypeSchema = zod.z.enum([
|
|
2149
2395
|
"drag-option",
|
|
2150
2396
|
// Draggable answer options
|
|
2397
|
+
"placed-chip",
|
|
2398
|
+
// Chip already placed on a number-line axis (re-drag)
|
|
2151
2399
|
"ruler-handle",
|
|
2152
2400
|
// Future: ruler endpoints
|
|
2153
2401
|
"shape",
|
|
@@ -5859,7 +6107,8 @@ const ExerciseDndProvider = ({
|
|
|
5859
6107
|
}, [dropHandlers]);
|
|
5860
6108
|
const handleDragStart = (event) => {
|
|
5861
6109
|
const { active } = event;
|
|
5862
|
-
const
|
|
6110
|
+
const raw = active.data.current;
|
|
6111
|
+
const item = raw?.type === "placed-chip" ? { id: String(active.id), type: "placed-chip", data: { label: raw.label } } : raw;
|
|
5863
6112
|
setActiveItem(item);
|
|
5864
6113
|
};
|
|
5865
6114
|
const handleDragEnd = (event) => {
|
|
@@ -5910,6 +6159,10 @@ const DragOverlayContent = ({ item }) => {
|
|
|
5910
6159
|
const data = item.data;
|
|
5911
6160
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "drag-overlay-option", children: data.label });
|
|
5912
6161
|
}
|
|
6162
|
+
case "placed-chip": {
|
|
6163
|
+
const label = item.data?.label ?? "";
|
|
6164
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "drag-overlay-option", children: label });
|
|
6165
|
+
}
|
|
5913
6166
|
default:
|
|
5914
6167
|
return null;
|
|
5915
6168
|
}
|
|
@@ -6368,6 +6621,334 @@ const WidgetRenderer = ({
|
|
|
6368
6621
|
}
|
|
6369
6622
|
return null;
|
|
6370
6623
|
};
|
|
6624
|
+
const cleanFloat = (n) => parseFloat(n.toFixed(10));
|
|
6625
|
+
const toPercent = (value, min, max) => {
|
|
6626
|
+
const raw = SIDE_PAD + (value - min) / (max - min) * (100 - 2 * SIDE_PAD);
|
|
6627
|
+
return Math.max(SIDE_PAD, Math.min(100 - SIDE_PAD, raw));
|
|
6628
|
+
};
|
|
6629
|
+
const pixelToValue = (chipCenterX, axisLeft, axisWidth, min, max) => {
|
|
6630
|
+
const relX = (chipCenterX - axisLeft) / axisWidth;
|
|
6631
|
+
const fraction = (relX - SIDE_PAD / 100) / (1 - 2 * SIDE_PAD / 100);
|
|
6632
|
+
const clamped = Math.max(0, Math.min(1, fraction));
|
|
6633
|
+
return min + clamped * (max - min);
|
|
6634
|
+
};
|
|
6635
|
+
const getMajorTicks = (min, max, step) => {
|
|
6636
|
+
if (step <= 0 || max <= min) return [min, max];
|
|
6637
|
+
const count = Math.floor((max - min) / step);
|
|
6638
|
+
const ticks = Array.from({ length: count + 1 }, (_, i) => cleanFloat(min + i * step));
|
|
6639
|
+
const lastTick = ticks[ticks.length - 1];
|
|
6640
|
+
if (Math.abs(lastTick - max) > 1e-9) ticks.push(max);
|
|
6641
|
+
return ticks;
|
|
6642
|
+
};
|
|
6643
|
+
const getMinorTickValues = (majorTicks, minorTicks) => {
|
|
6644
|
+
if (minorTicks <= 0) return [];
|
|
6645
|
+
const result = [];
|
|
6646
|
+
majorTicks.slice(0, -1).forEach((tick, idx) => {
|
|
6647
|
+
const next = majorTicks[idx + 1];
|
|
6648
|
+
const interval = next - tick;
|
|
6649
|
+
for (let j = 1; j <= minorTicks; j++) {
|
|
6650
|
+
result.push(cleanFloat(tick + j / (minorTicks + 1) * interval));
|
|
6651
|
+
}
|
|
6652
|
+
});
|
|
6653
|
+
return result;
|
|
6654
|
+
};
|
|
6655
|
+
const getSnapTicks = (majorTicks, minorTicks) => [
|
|
6656
|
+
...majorTicks,
|
|
6657
|
+
...getMinorTickValues(majorTicks, minorTicks)
|
|
6658
|
+
];
|
|
6659
|
+
const snapToNearest = (raw, ticks) => {
|
|
6660
|
+
if (ticks.length === 0) return cleanFloat(raw);
|
|
6661
|
+
const snapped = ticks.reduce(
|
|
6662
|
+
(best, t) => Math.abs(t - raw) < Math.abs(best - raw) ? t : best
|
|
6663
|
+
);
|
|
6664
|
+
return cleanFloat(snapped);
|
|
6665
|
+
};
|
|
6666
|
+
const isWidgetRef = (item) => item.type === JSON_TYPES.WIDGET_REF;
|
|
6667
|
+
const getPositionWidgetId = (content) => {
|
|
6668
|
+
for (const item of content) {
|
|
6669
|
+
if (isWidgetRef(item)) return item.widgetId;
|
|
6670
|
+
}
|
|
6671
|
+
return null;
|
|
6672
|
+
};
|
|
6673
|
+
const PlacedChip = ({ widgetId, label, leftPercent, onRemove }) => {
|
|
6674
|
+
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
|
|
6675
|
+
id: widgetId,
|
|
6676
|
+
data: { type: "placed-chip", widgetId, label }
|
|
6677
|
+
});
|
|
6678
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
6679
|
+
"div",
|
|
6680
|
+
{
|
|
6681
|
+
className: "number-line-placed-chip",
|
|
6682
|
+
style: { left: `${leftPercent}%`, opacity: isDragging ? 0.3 : 1 },
|
|
6683
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
6684
|
+
"button",
|
|
6685
|
+
{
|
|
6686
|
+
ref: setNodeRef,
|
|
6687
|
+
className: "number-line-placed-chip-btn",
|
|
6688
|
+
title: "Drag to move · Click to remove",
|
|
6689
|
+
onClick: onRemove,
|
|
6690
|
+
...listeners,
|
|
6691
|
+
...attributes,
|
|
6692
|
+
children: label
|
|
6693
|
+
}
|
|
6694
|
+
)
|
|
6695
|
+
}
|
|
6696
|
+
);
|
|
6697
|
+
};
|
|
6698
|
+
const FreeDragAxis = ({
|
|
6699
|
+
axisDropId,
|
|
6700
|
+
min,
|
|
6701
|
+
max,
|
|
6702
|
+
minorTicks,
|
|
6703
|
+
majorTicks,
|
|
6704
|
+
showArrowLeft,
|
|
6705
|
+
showArrowRight,
|
|
6706
|
+
positions,
|
|
6707
|
+
values,
|
|
6708
|
+
onChange
|
|
6709
|
+
}) => {
|
|
6710
|
+
const axisRef = React.useRef(null);
|
|
6711
|
+
const snapTicks = React.useMemo(
|
|
6712
|
+
() => getSnapTicks(majorTicks, minorTicks),
|
|
6713
|
+
[majorTicks, minorTicks]
|
|
6714
|
+
);
|
|
6715
|
+
const [dropLabels, setDropLabels] = React.useState(/* @__PURE__ */ new Map());
|
|
6716
|
+
const [dropSources, setDropSources] = React.useState(/* @__PURE__ */ new Map());
|
|
6717
|
+
React.useEffect(() => {
|
|
6718
|
+
setDropLabels((prev) => {
|
|
6719
|
+
let changed = false;
|
|
6720
|
+
const next = new Map(prev);
|
|
6721
|
+
for (const [wid] of next) {
|
|
6722
|
+
if (!values[wid]) {
|
|
6723
|
+
next.delete(wid);
|
|
6724
|
+
changed = true;
|
|
6725
|
+
}
|
|
6726
|
+
}
|
|
6727
|
+
return changed ? next : prev;
|
|
6728
|
+
});
|
|
6729
|
+
setDropSources((prev) => {
|
|
6730
|
+
let changed = false;
|
|
6731
|
+
const next = new Map(prev);
|
|
6732
|
+
for (const [wid] of next) {
|
|
6733
|
+
if (!values[wid]) {
|
|
6734
|
+
next.delete(wid);
|
|
6735
|
+
changed = true;
|
|
6736
|
+
}
|
|
6737
|
+
}
|
|
6738
|
+
return changed ? next : prev;
|
|
6739
|
+
});
|
|
6740
|
+
}, [values]);
|
|
6741
|
+
const { setNodeRef, isOver } = useDroppable({ id: axisDropId });
|
|
6742
|
+
const mergedRef = React.useCallback((node) => {
|
|
6743
|
+
axisRef.current = node;
|
|
6744
|
+
setNodeRef(node);
|
|
6745
|
+
}, [setNodeRef]);
|
|
6746
|
+
const snapFromEvent = React.useCallback((translated) => {
|
|
6747
|
+
if (!axisRef.current) return null;
|
|
6748
|
+
const axisRect = axisRef.current.getBoundingClientRect();
|
|
6749
|
+
const chipCenterX = translated.left + translated.width / 2;
|
|
6750
|
+
const raw = pixelToValue(chipCenterX, axisRect.left, axisRect.width, min, max);
|
|
6751
|
+
return snapToNearest(raw, snapTicks);
|
|
6752
|
+
}, [min, max, snapTicks]);
|
|
6753
|
+
useDndMonitor({
|
|
6754
|
+
onDragEnd(event) {
|
|
6755
|
+
if (!event.over || event.over.id !== axisDropId) return;
|
|
6756
|
+
if (!axisRef.current) return;
|
|
6757
|
+
const translated = event.active.rect.current.translated;
|
|
6758
|
+
if (!translated) return;
|
|
6759
|
+
const placed = snapFromEvent(translated);
|
|
6760
|
+
if (placed === null) return;
|
|
6761
|
+
const data = event.active.data.current;
|
|
6762
|
+
if (data?.type === "placed-chip") {
|
|
6763
|
+
const widgetId = data.widgetId;
|
|
6764
|
+
const label = data.label;
|
|
6765
|
+
const originalSourceId = dropSources.get(widgetId) ?? widgetId;
|
|
6766
|
+
onChange(widgetId, String(placed), true, originalSourceId);
|
|
6767
|
+
setDropLabels((prev) => new Map(prev).set(widgetId, label));
|
|
6768
|
+
return;
|
|
6769
|
+
}
|
|
6770
|
+
const draggedItem = event.active.data.current;
|
|
6771
|
+
if (draggedItem?.type !== "drag-option") return;
|
|
6772
|
+
const targetPos = positions.find((pos) => {
|
|
6773
|
+
const wid = getPositionWidgetId(pos.content);
|
|
6774
|
+
return wid && !values[wid];
|
|
6775
|
+
});
|
|
6776
|
+
if (!targetPos) return;
|
|
6777
|
+
const targetWidgetId = getPositionWidgetId(targetPos.content);
|
|
6778
|
+
if (!targetWidgetId) return;
|
|
6779
|
+
const chipLabel = draggedItem.data.value;
|
|
6780
|
+
const sourceOptId = String(event.active.id);
|
|
6781
|
+
onChange(targetWidgetId, String(placed), true, sourceOptId);
|
|
6782
|
+
setDropLabels((prev) => new Map(prev).set(targetWidgetId, chipLabel));
|
|
6783
|
+
setDropSources((prev) => new Map(prev).set(targetWidgetId, sourceOptId));
|
|
6784
|
+
}
|
|
6785
|
+
});
|
|
6786
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
6787
|
+
"div",
|
|
6788
|
+
{
|
|
6789
|
+
ref: mergedRef,
|
|
6790
|
+
className: `number-line-axis-row nl-axis-drag-mode${isOver ? " nl-axis-drag-over" : ""}`,
|
|
6791
|
+
children: [
|
|
6792
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-track" }),
|
|
6793
|
+
showArrowLeft && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-left" }),
|
|
6794
|
+
showArrowRight && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-right" }),
|
|
6795
|
+
getMinorTickValues(majorTicks, minorTicks).map((v) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
6796
|
+
"div",
|
|
6797
|
+
{
|
|
6798
|
+
className: "number-line-minor-tick",
|
|
6799
|
+
style: { left: `${toPercent(v, min, max)}%` }
|
|
6800
|
+
},
|
|
6801
|
+
`minor-${v}`
|
|
6802
|
+
)),
|
|
6803
|
+
majorTicks.map((tick) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
6804
|
+
"div",
|
|
6805
|
+
{
|
|
6806
|
+
className: "number-line-major-tick",
|
|
6807
|
+
style: { left: `${toPercent(tick, min, max)}%` }
|
|
6808
|
+
},
|
|
6809
|
+
`major-${tick}`
|
|
6810
|
+
)),
|
|
6811
|
+
positions.map((pos) => {
|
|
6812
|
+
const wid = getPositionWidgetId(pos.content);
|
|
6813
|
+
const dropPos = wid ? parseFloat(values[wid] ?? "") : NaN;
|
|
6814
|
+
const label = wid ? dropLabels.get(wid) : void 0;
|
|
6815
|
+
if (!wid || isNaN(dropPos) || !label) return null;
|
|
6816
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
6817
|
+
PlacedChip,
|
|
6818
|
+
{
|
|
6819
|
+
widgetId: wid,
|
|
6820
|
+
label,
|
|
6821
|
+
leftPercent: toPercent(dropPos, min, max),
|
|
6822
|
+
onRemove: () => {
|
|
6823
|
+
const sourceId = dropSources.get(wid);
|
|
6824
|
+
onChange(wid, "", false, sourceId);
|
|
6825
|
+
setDropLabels((prev) => {
|
|
6826
|
+
const m = new Map(prev);
|
|
6827
|
+
m.delete(wid);
|
|
6828
|
+
return m;
|
|
6829
|
+
});
|
|
6830
|
+
setDropSources((prev) => {
|
|
6831
|
+
const m = new Map(prev);
|
|
6832
|
+
m.delete(wid);
|
|
6833
|
+
return m;
|
|
6834
|
+
});
|
|
6835
|
+
}
|
|
6836
|
+
},
|
|
6837
|
+
wid
|
|
6838
|
+
);
|
|
6839
|
+
})
|
|
6840
|
+
]
|
|
6841
|
+
}
|
|
6842
|
+
);
|
|
6843
|
+
};
|
|
6844
|
+
const NumberLineRenderer = ({
|
|
6845
|
+
item,
|
|
6846
|
+
widgets,
|
|
6847
|
+
values,
|
|
6848
|
+
droppedInputs,
|
|
6849
|
+
onChange,
|
|
6850
|
+
onInputFocus
|
|
6851
|
+
}) => {
|
|
6852
|
+
const { min, max, step, minorTicks = 0, style = "arrow-right", positions } = item;
|
|
6853
|
+
const majorTicks = React.useMemo(() => getMajorTicks(min, max, step), [min, max, step]);
|
|
6854
|
+
const minorValues = React.useMemo(() => getMinorTickValues(majorTicks, minorTicks), [majorTicks, minorTicks]);
|
|
6855
|
+
const showArrowRight = style === "arrow-right" || style === "arrow-both";
|
|
6856
|
+
const showArrowLeft = style === "arrow-left" || style === "arrow-both";
|
|
6857
|
+
const isDragMode = Boolean(widgets[DRAG_OPTIONS_WIDGET_ID]);
|
|
6858
|
+
const inputTickSet = React.useMemo(() => {
|
|
6859
|
+
if (isDragMode) return /* @__PURE__ */ new Set();
|
|
6860
|
+
return new Set(
|
|
6861
|
+
positions.filter((pos) => getPositionWidgetId(pos.content) !== null).map((pos) => pos.value)
|
|
6862
|
+
);
|
|
6863
|
+
}, [isDragMode, positions]);
|
|
6864
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
6865
|
+
"div",
|
|
6866
|
+
{
|
|
6867
|
+
className: `number-line${isDragMode ? " number-line--drag-mode" : ""}`,
|
|
6868
|
+
style: { [CSS_VAR_SIDE_PAD]: `${SIDE_PAD}%` },
|
|
6869
|
+
role: "img",
|
|
6870
|
+
"aria-label": `Number line from ${min} to ${max}`,
|
|
6871
|
+
children: [
|
|
6872
|
+
!isDragMode && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-content-row", children: positions.map((pos) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
6873
|
+
"div",
|
|
6874
|
+
{
|
|
6875
|
+
className: "number-line-position-content",
|
|
6876
|
+
style: { left: `${toPercent(pos.value, min, max)}%` },
|
|
6877
|
+
children: pos.content.map((subItem) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
6878
|
+
LayoutRenderer,
|
|
6879
|
+
{
|
|
6880
|
+
item: subItem,
|
|
6881
|
+
widgets,
|
|
6882
|
+
values,
|
|
6883
|
+
droppedInputs,
|
|
6884
|
+
onChange,
|
|
6885
|
+
onInputFocus
|
|
6886
|
+
},
|
|
6887
|
+
subItem.id
|
|
6888
|
+
))
|
|
6889
|
+
},
|
|
6890
|
+
pos.id
|
|
6891
|
+
)) }),
|
|
6892
|
+
isDragMode ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
6893
|
+
FreeDragAxis,
|
|
6894
|
+
{
|
|
6895
|
+
axisDropId: `${item.id}--nl-axis`,
|
|
6896
|
+
min,
|
|
6897
|
+
max,
|
|
6898
|
+
minorTicks,
|
|
6899
|
+
majorTicks,
|
|
6900
|
+
showArrowLeft,
|
|
6901
|
+
showArrowRight,
|
|
6902
|
+
positions,
|
|
6903
|
+
values,
|
|
6904
|
+
onChange
|
|
6905
|
+
}
|
|
6906
|
+
) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "number-line-axis-row", role: "presentation", children: [
|
|
6907
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-track", "aria-hidden": "true" }),
|
|
6908
|
+
showArrowLeft && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-left", "aria-hidden": "true" }),
|
|
6909
|
+
showArrowRight && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-arrow number-line-arrow-right", "aria-hidden": "true" }),
|
|
6910
|
+
minorValues.map((v) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
6911
|
+
"div",
|
|
6912
|
+
{
|
|
6913
|
+
className: "number-line-minor-tick",
|
|
6914
|
+
"aria-hidden": "true",
|
|
6915
|
+
style: { left: `${toPercent(v, min, max)}%` }
|
|
6916
|
+
},
|
|
6917
|
+
`minor-${v}`
|
|
6918
|
+
)),
|
|
6919
|
+
majorTicks.map((tick) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
6920
|
+
"div",
|
|
6921
|
+
{
|
|
6922
|
+
className: "number-line-major-tick",
|
|
6923
|
+
style: { left: `${toPercent(tick, min, max)}%` }
|
|
6924
|
+
},
|
|
6925
|
+
`major-${tick}`
|
|
6926
|
+
)),
|
|
6927
|
+
positions.map((pos) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
6928
|
+
"div",
|
|
6929
|
+
{
|
|
6930
|
+
className: "number-line-position-dot",
|
|
6931
|
+
style: { left: `${toPercent(pos.value, min, max)}%` }
|
|
6932
|
+
},
|
|
6933
|
+
`dot-${pos.id}`
|
|
6934
|
+
))
|
|
6935
|
+
] }),
|
|
6936
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "number-line-labels-row", "aria-hidden": "true", children: majorTicks.map((tick) => {
|
|
6937
|
+
const hasInputAtTick = inputTickSet.has(tick);
|
|
6938
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
6939
|
+
"span",
|
|
6940
|
+
{
|
|
6941
|
+
className: "number-line-tick-label",
|
|
6942
|
+
style: { left: `${toPercent(tick, min, max)}%`, visibility: hasInputAtTick ? "hidden" : "visible" },
|
|
6943
|
+
children: tick
|
|
6944
|
+
},
|
|
6945
|
+
`lbl-${tick}`
|
|
6946
|
+
);
|
|
6947
|
+
}) })
|
|
6948
|
+
]
|
|
6949
|
+
}
|
|
6950
|
+
);
|
|
6951
|
+
};
|
|
6371
6952
|
function isArabic(text) {
|
|
6372
6953
|
return /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/.test(text);
|
|
6373
6954
|
}
|
|
@@ -6535,6 +7116,18 @@ const LayoutRenderer = ({
|
|
|
6535
7116
|
},
|
|
6536
7117
|
cell.id
|
|
6537
7118
|
)) }, row.id)) }) }) });
|
|
7119
|
+
case JSON_TYPES.NUMBER_LINE:
|
|
7120
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
7121
|
+
NumberLineRenderer,
|
|
7122
|
+
{
|
|
7123
|
+
item,
|
|
7124
|
+
widgets,
|
|
7125
|
+
values,
|
|
7126
|
+
droppedInputs,
|
|
7127
|
+
onChange,
|
|
7128
|
+
onInputFocus
|
|
7129
|
+
}
|
|
7130
|
+
);
|
|
6538
7131
|
default:
|
|
6539
7132
|
return null;
|
|
6540
7133
|
}
|
|
@@ -6622,6 +7215,20 @@ const ExerciseRenderer = React.forwardRef(({
|
|
|
6622
7215
|
});
|
|
6623
7216
|
continue;
|
|
6624
7217
|
}
|
|
7218
|
+
if (item.type === JSON_TYPES.NUMBER_LINE) {
|
|
7219
|
+
if (currentGroup.length > 0) {
|
|
7220
|
+
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
7221
|
+
const subGroups = groupLineItems(currentGroup);
|
|
7222
|
+
groups.push({ items: currentGroup, hasArabic, subGroups });
|
|
7223
|
+
currentGroup = [];
|
|
7224
|
+
}
|
|
7225
|
+
groups.push({
|
|
7226
|
+
items: [item],
|
|
7227
|
+
hasArabic: false,
|
|
7228
|
+
subGroups: []
|
|
7229
|
+
});
|
|
7230
|
+
continue;
|
|
7231
|
+
}
|
|
6625
7232
|
if (item.type === JSON_TYPES.LINEBREAK) {
|
|
6626
7233
|
if (currentGroup.length > 0) {
|
|
6627
7234
|
const hasArabic = currentGroup.some((item2) => itemContainsArabic(item2));
|
|
@@ -6872,7 +7479,7 @@ exports.isTextItem = isTextItem$1;
|
|
|
6872
7479
|
exports.isToolNode = isToolNode;
|
|
6873
7480
|
exports.isValidExpression = isValidExpression;
|
|
6874
7481
|
exports.isValidationNode = isValidationNode;
|
|
6875
|
-
exports.isWidgetRef = isWidgetRef;
|
|
7482
|
+
exports.isWidgetRef = isWidgetRef$1;
|
|
6876
7483
|
exports.parse = parse;
|
|
6877
7484
|
exports.parseSafe = parseSafe;
|
|
6878
7485
|
exports.renderExercise = renderExercise;
|