@altnovatrail/canvas 0.0.0-stage → 0.1.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.
- package/LICENSE +201 -0
- package/README.md +59 -2
- package/lib/core/boxes.d.ts +54 -0
- package/lib/core/boxes.js +116 -0
- package/lib/core/clock.d.ts +25 -0
- package/lib/core/clock.js +39 -0
- package/lib/core/crop.d.ts +87 -0
- package/lib/core/crop.js +227 -0
- package/lib/core/eraser.d.ts +42 -0
- package/lib/core/eraser.js +127 -0
- package/lib/core/example.d.ts +66 -0
- package/lib/core/example.js +116 -0
- package/lib/core/fields.d.ts +29 -0
- package/lib/core/fields.js +78 -0
- package/lib/core/geometry.d.ts +38 -0
- package/lib/core/geometry.js +107 -0
- package/lib/core/grid.d.ts +30 -0
- package/lib/core/grid.js +47 -0
- package/lib/core/groups.d.ts +55 -0
- package/lib/core/groups.js +172 -0
- package/lib/core/hands.d.ts +120 -0
- package/lib/core/hands.js +273 -0
- package/lib/core/history.d.ts +42 -0
- package/lib/core/history.js +228 -0
- package/lib/core/index.d.ts +12 -0
- package/lib/core/index.js +15 -0
- package/lib/core/ink.d.ts +1 -0
- package/lib/core/ink.js +5 -0
- package/lib/core/manifest.d.ts +22 -0
- package/lib/core/manifest.js +50 -0
- package/lib/core/marks.d.ts +145 -0
- package/lib/core/marks.js +454 -0
- package/lib/core/outline.d.ts +22 -0
- package/lib/core/outline.js +50 -0
- package/lib/core/page.d.ts +62 -0
- package/lib/core/page.js +309 -0
- package/lib/core/png.d.ts +13 -0
- package/lib/core/png.js +231 -0
- package/lib/core/raster.d.ts +14 -0
- package/lib/core/raster.js +72 -0
- package/lib/core/recorder.d.ts +73 -0
- package/lib/core/recorder.js +127 -0
- package/lib/core/screen.d.ts +35 -0
- package/lib/core/screen.js +36 -0
- package/lib/core/sets.d.ts +10 -0
- package/lib/core/sets.js +31 -0
- package/lib/core/sheet.d.ts +9 -0
- package/lib/core/sheet.js +6 -0
- package/lib/core/submission.d.ts +41 -0
- package/lib/core/submission.js +63 -0
- package/lib/core/task.d.ts +214 -0
- package/lib/core/task.js +343 -0
- package/lib/core/widths.d.ts +57 -0
- package/lib/core/widths.js +218 -0
- package/lib/core/work.d.ts +77 -0
- package/lib/core/work.js +293 -0
- package/lib/element/attributes.d.ts +11 -0
- package/lib/element/attributes.js +22 -0
- package/lib/element/index.d.ts +219 -0
- package/lib/element/index.js +1898 -0
- package/lib/element/labels.d.ts +41 -0
- package/lib/element/labels.js +74 -0
- package/lib/element/parts.d.ts +63 -0
- package/lib/element/parts.js +191 -0
- package/lib/element/pictures.d.ts +45 -0
- package/lib/element/pictures.js +167 -0
- package/lib/element/probe.d.ts +33 -0
- package/lib/element/probe.js +18 -0
- package/lib/element/session.d.ts +3 -0
- package/lib/element/session.js +7 -0
- package/lib/element/statement.d.ts +35 -0
- package/lib/element/statement.js +116 -0
- package/lib/element/tools.d.ts +64 -0
- package/lib/element/tools.js +254 -0
- package/package.json +41 -4
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import type { Tool } from "@altnovatrail/spec/canvas";
|
|
2
|
+
import type * as open from "@altnovatrail/spec/formatv0/open";
|
|
3
|
+
import { type Grid, type GridKind } from "./grid.js";
|
|
4
|
+
import { type Page } from "./page.js";
|
|
5
|
+
/** The canvas kinds of the module format: the handwriting sheet and the workbook. */
|
|
6
|
+
export declare const SHEET_KIND = "handwriting_sheet";
|
|
7
|
+
export declare const WORKBOOK_KIND = "workbook";
|
|
8
|
+
/**
|
|
9
|
+
* What a press of the primary button does on the sheet: the pen writes, the eraser erases (R22). A
|
|
10
|
+
* pen's eraser erases whatever the tool, if the task allows the eraser (R28). spec's type (R74
|
|
11
|
+
* spec).
|
|
12
|
+
*/
|
|
13
|
+
export type { Tool };
|
|
14
|
+
/** The sheet's tools, in the order their buttons stand: the pen, the eraser. */
|
|
15
|
+
export declare const TOOLS: readonly Tool[];
|
|
16
|
+
export declare function isTool(value: unknown): value is Tool;
|
|
17
|
+
/** A paragraph of a task's statement, or a piece of text of a workbook's line. */
|
|
18
|
+
export interface TextPart {
|
|
19
|
+
readonly text: string;
|
|
20
|
+
}
|
|
21
|
+
/** A picture of a task's statement. */
|
|
22
|
+
export interface PicturePart {
|
|
23
|
+
/** Its path from the module's media/, which the consumer's `media` takes to give its file. */
|
|
24
|
+
readonly picture: string;
|
|
25
|
+
/**
|
|
26
|
+
* What the picture shows, in words: a screen reader reads it, and it stands in the picture's
|
|
27
|
+
* place until the picture comes, or if it never does. Empty for a picture that only decorates.
|
|
28
|
+
*/
|
|
29
|
+
readonly alt: string;
|
|
30
|
+
/** The picture's size in whole CSS pixels, which it keeps narrower when the statement is. */
|
|
31
|
+
readonly width: number;
|
|
32
|
+
readonly height: number;
|
|
33
|
+
}
|
|
34
|
+
/** A part of a task's statement: a paragraph, or a picture. */
|
|
35
|
+
export type StatementPart = TextPart | PicturePart;
|
|
36
|
+
/** A rectangle of the handwriting sheet, in its units from the top left corner. */
|
|
37
|
+
export interface Zone {
|
|
38
|
+
readonly left: number;
|
|
39
|
+
readonly top: number;
|
|
40
|
+
readonly width: number;
|
|
41
|
+
readonly height: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* An answer slot of an item on a handwriting sheet: anywhere on the sheet, the whole sheet, a zone
|
|
45
|
+
* of it, or a choice of options.
|
|
46
|
+
*/
|
|
47
|
+
export type AnswerSlot = {
|
|
48
|
+
readonly binding: "anywhere" | "sheet";
|
|
49
|
+
} | {
|
|
50
|
+
readonly binding: "zone";
|
|
51
|
+
readonly zone: Zone;
|
|
52
|
+
} | {
|
|
53
|
+
readonly binding: "choice";
|
|
54
|
+
readonly options: readonly string[];
|
|
55
|
+
};
|
|
56
|
+
/** An item of a handwriting sheet: its answer slots, and what it asks, "a)". */
|
|
57
|
+
export interface SheetItem {
|
|
58
|
+
readonly text?: string;
|
|
59
|
+
readonly answer_slots: readonly AnswerSlot[];
|
|
60
|
+
}
|
|
61
|
+
/** A piece of a workbook's line: a piece of text, or a box, an answer slot. */
|
|
62
|
+
export type LinePart = TextPart | {
|
|
63
|
+
readonly binding: "box";
|
|
64
|
+
};
|
|
65
|
+
/** An item of a workbook: a line of text and boxes, as in 7 × [ ] = 56. */
|
|
66
|
+
export interface WorkbookItem {
|
|
67
|
+
readonly line: readonly LinePart[];
|
|
68
|
+
}
|
|
69
|
+
/** The canvas of a handwriting sheet: its grid, and the tools the task allows. */
|
|
70
|
+
export interface SheetCanvas {
|
|
71
|
+
readonly kind: typeof SHEET_KIND;
|
|
72
|
+
/** Squares unless given. */
|
|
73
|
+
readonly grid?: GridKind;
|
|
74
|
+
/** The grid's step in sheet units, a whole number from 20 to 500: 50 unless given. */
|
|
75
|
+
readonly grid_step?: number;
|
|
76
|
+
/**
|
|
77
|
+
* The tools the task allows (concepts 8 spec; R28), the element's own field until the format has
|
|
78
|
+
* one: the canvas takes "pen" and "eraser", and passes over the names it does not know — a
|
|
79
|
+
* ruler, a calculator — which are app's to show or come with new versions of the canvas. The
|
|
80
|
+
* pen and the eraser unless given; a list without the pen is a TypeError, for the canvas writes
|
|
81
|
+
* with it.
|
|
82
|
+
*/
|
|
83
|
+
readonly tools?: readonly string[];
|
|
84
|
+
}
|
|
85
|
+
/** The canvas of a workbook, which lays out its lines itself, and the tools the task allows. */
|
|
86
|
+
export interface WorkbookCanvas {
|
|
87
|
+
readonly kind: typeof WORKBOOK_KIND;
|
|
88
|
+
readonly tools?: readonly string[];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The canvas of a task as a consumer gives it: spec's, and the tools the task allows (concepts 8
|
|
92
|
+
* spec; R28), the element's own field until the format has one.
|
|
93
|
+
*/
|
|
94
|
+
export interface Canvas extends open.Canvas {
|
|
95
|
+
readonly tools?: readonly string[];
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The open view of a task as a consumer gives it, what the learner's app receives: spec's type
|
|
99
|
+
* (R73 spec), its canvas with the element's own tools. A type gives the shape and nothing more: the
|
|
100
|
+
* element checks the value itself.
|
|
101
|
+
*/
|
|
102
|
+
export interface Task extends open.Task {
|
|
103
|
+
readonly canvas: Canvas;
|
|
104
|
+
}
|
|
105
|
+
/** What a task of either canvas kind has, as the element checked it. */
|
|
106
|
+
interface CheckedFields {
|
|
107
|
+
/** The task's local ID. */
|
|
108
|
+
readonly id: string;
|
|
109
|
+
/** What the learner reads, in the module's explanation language, which the manifest names. */
|
|
110
|
+
readonly statement: readonly StatementPart[];
|
|
111
|
+
/**
|
|
112
|
+
* What the statement gives, short texts all different, and what the task asks to find: the chips
|
|
113
|
+
* the learner ticks at a solving step, which app shows, not the element (R57 spec).
|
|
114
|
+
*/
|
|
115
|
+
readonly givens?: readonly string[];
|
|
116
|
+
readonly unknown?: string;
|
|
117
|
+
}
|
|
118
|
+
/** A task on a handwriting sheet, as the element checked it. */
|
|
119
|
+
export interface SheetTask extends CheckedFields {
|
|
120
|
+
readonly canvas: SheetCanvas;
|
|
121
|
+
readonly items: readonly SheetItem[];
|
|
122
|
+
}
|
|
123
|
+
/** A task on a workbook, as the element checked it: a line for each item. */
|
|
124
|
+
export interface WorkbookTask extends CheckedFields {
|
|
125
|
+
readonly canvas: WorkbookCanvas;
|
|
126
|
+
readonly items: readonly WorkbookItem[];
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* A task as the element checked it, of either canvas kind: its ID, its statement, what the
|
|
130
|
+
* statement gives and asks, its canvas and its items — no key, solution or trap, nor its skills,
|
|
131
|
+
* band, difficulty and age categories, nor the task template it may be an instance of (R05 spec;
|
|
132
|
+
* R51 spec; R60 spec). It is a Task too, which task.check.ts holds to.
|
|
133
|
+
*/
|
|
134
|
+
export type CheckedTask = SheetTask | WorkbookTask;
|
|
135
|
+
/**
|
|
136
|
+
* What the sheet makes of a task: the grid on the sheet, the statement above it and the tools it
|
|
137
|
+
* allows, in the order of `TOOLS`.
|
|
138
|
+
*/
|
|
139
|
+
export interface SheetTemplate {
|
|
140
|
+
readonly grid: Grid;
|
|
141
|
+
readonly statement: readonly StatementPart[];
|
|
142
|
+
readonly tools: readonly Tool[];
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* What the workbook makes of a task: the layout of its lines on the page (R35), the statement above
|
|
146
|
+
* it and the tools it allows.
|
|
147
|
+
*/
|
|
148
|
+
export interface WorkbookTemplate {
|
|
149
|
+
readonly page: Page;
|
|
150
|
+
readonly statement: readonly StatementPart[];
|
|
151
|
+
readonly tools: readonly Tool[];
|
|
152
|
+
}
|
|
153
|
+
/** What the canvas makes of a task, of either kind: a workbook's has its page. */
|
|
154
|
+
export type Template = SheetTemplate | WorkbookTemplate;
|
|
155
|
+
/** The template of a sheet without a task: squares of 50 units, no statement, both tools. */
|
|
156
|
+
export declare const NO_TASK: SheetTemplate;
|
|
157
|
+
/**
|
|
158
|
+
* A task's open view, checked as the format's schema checks it, and a copy of it with the fields
|
|
159
|
+
* the schema names: the element reads each field once, and what it shows is what it checked. What
|
|
160
|
+
* the schema refuses is a TypeError that names the field by its path, a field the open view has not
|
|
161
|
+
* among it. A field is left out by `undefined` alone: `null` is a value, and a wrong one.
|
|
162
|
+
*/
|
|
163
|
+
export declare function taskOf(value: unknown): CheckedTask;
|
|
164
|
+
/** Whether a task is on a handwriting sheet. */
|
|
165
|
+
export declare function isSheet(task: CheckedTask): task is SheetTask;
|
|
166
|
+
/**
|
|
167
|
+
* Whether a task's one answer slot is anywhere on the sheet or the whole sheet, which the strokes
|
|
168
|
+
* on the sheet fill (R42): not a sheet of choices, whose options app marks, nor a workbook.
|
|
169
|
+
*/
|
|
170
|
+
export declare function wholeSheet(task: CheckedTask | undefined): boolean;
|
|
171
|
+
/**
|
|
172
|
+
* The template of a task `taskOf` gave, or of no task for `undefined`. A workbook's lines are laid
|
|
173
|
+
* out on its page, and a task whose lines do not fit is a TypeError at the first item that does not
|
|
174
|
+
* (R34, R35). A zone on the sheet is a TypeError until an epic of its own; a choice the sheet passes
|
|
175
|
+
* over, and an item's text too: app shows them (R32).
|
|
176
|
+
*/
|
|
177
|
+
export declare function templateOf(task: CheckedTask | undefined): Template;
|
|
178
|
+
/** Whether a template is a workbook's. */
|
|
179
|
+
export declare function isWorkbook(template: Template): template is WorkbookTemplate;
|
|
180
|
+
/** Whether two templates show the same: the same layer, the same statement, the same tools. */
|
|
181
|
+
export declare function sameTemplate(a: Template, b: Template): boolean;
|
|
182
|
+
/** Whether two templates allow the same tools. */
|
|
183
|
+
export declare function sameTools(a: Template, b: Template): boolean;
|
|
184
|
+
/**
|
|
185
|
+
* Whether two templates draw the same on the layer of the template: the same grid on a sheet, the
|
|
186
|
+
* same lines on a workbook's page.
|
|
187
|
+
*/
|
|
188
|
+
export declare function sameLayer(a: Template, b: Template): boolean;
|
|
189
|
+
/** Whether two templates show the same statement. */
|
|
190
|
+
export declare function sameStatement(a: Template, b: Template): boolean;
|
|
191
|
+
/** A part of a statement — a task's or a worked example's: a paragraph, or a picture. */
|
|
192
|
+
export declare function partOf(value: unknown, path: string): StatementPart;
|
|
193
|
+
/**
|
|
194
|
+
* A part of a text the learner reads, `of` naming the text: a paragraph, or a picture with what it
|
|
195
|
+
* shows and the size it is shown at — of a task's statement, a worked example's, or its demo.
|
|
196
|
+
*/
|
|
197
|
+
export declare function textOrPictureOf(value: unknown, path: string, of: string): StatementPart;
|
|
198
|
+
/** What a picture shows, in words, which a screen reader reads: "" for one that only decorates. */
|
|
199
|
+
export declare function altOf(value: unknown, path: string): string;
|
|
200
|
+
/**
|
|
201
|
+
* A picture's path from the module's media/: names in kebab-case, none a Windows device, joined by
|
|
202
|
+
* /, and the extension of a PNG, JPEG or WebP file, at most 128 characters (picture_path).
|
|
203
|
+
*/
|
|
204
|
+
export declare function pictureOf(value: unknown, path: string): string;
|
|
205
|
+
/** The grid a handwriting sheet's canvas draws, the defaults filled in: squares of 50 units. */
|
|
206
|
+
export declare function gridFrom({ grid, grid_step, }: Pick<SheetCanvas, "grid" | "grid_step">): Grid;
|
|
207
|
+
/** A handwriting sheet's grid as its canvas gives it: its kind and its step, each if given. */
|
|
208
|
+
export declare function gridOf(canvas: Record<string, unknown>, path: string): Pick<SheetCanvas, "grid" | "grid_step">;
|
|
209
|
+
export declare function zoneOf(value: unknown, path: string): Zone;
|
|
210
|
+
/**
|
|
211
|
+
* Whether two parts of a text the learner reads show the same: the same paragraph, or the same
|
|
212
|
+
* picture with the same description and size.
|
|
213
|
+
*/
|
|
214
|
+
export declare function samePart(a: StatementPart, b: StatementPart): boolean;
|
package/lib/core/task.js
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// The open view of a task of the module format 0.2 (R51 spec). What a consumer gives is of spec's
|
|
2
|
+
// type, from its npm package at the version of the spec/ submodule (R73 spec; R41), with one field
|
|
3
|
+
// of the element's own, `canvas.tools`, until the format has one (R28). The element checks a task as
|
|
4
|
+
// the schema does, every object closed: a field the open view has not — an old name, a closed field
|
|
5
|
+
// — is a TypeError (R32). What the check gives is the element's own shape of the same task, its
|
|
6
|
+
// canvas kinds apart, which the rest of the element reads.
|
|
7
|
+
import { closed, listOf, sameItems, shown, text, whole } from "./fields.js";
|
|
8
|
+
import { DEFAULT_GRID, GRID_KINDS, GRID_STEP, GRID_STEP_MAX, GRID_STEP_MIN, } from "./grid.js";
|
|
9
|
+
import { pageOf, samePage } from "./page.js";
|
|
10
|
+
/** The canvas kinds of the module format: the handwriting sheet and the workbook. */
|
|
11
|
+
export const SHEET_KIND = "handwriting_sheet";
|
|
12
|
+
export const WORKBOOK_KIND = "workbook";
|
|
13
|
+
/** The sheet's tools, in the order their buttons stand: the pen, the eraser. */
|
|
14
|
+
export const TOOLS = Object.freeze(["pen", "eraser"]);
|
|
15
|
+
export function isTool(value) {
|
|
16
|
+
return TOOLS.includes(value);
|
|
17
|
+
}
|
|
18
|
+
/** The template of a sheet without a task: squares of 50 units, no statement, both tools. */
|
|
19
|
+
export const NO_TASK = Object.freeze({
|
|
20
|
+
grid: DEFAULT_GRID,
|
|
21
|
+
statement: Object.freeze([]),
|
|
22
|
+
tools: TOOLS,
|
|
23
|
+
});
|
|
24
|
+
/** The fields of a task's open view (R57 spec): the rest of a task is closed. */
|
|
25
|
+
const OPEN = ["id", "statement", "givens", "unknown", "canvas", "items"];
|
|
26
|
+
/**
|
|
27
|
+
* A task's open view, checked as the format's schema checks it, and a copy of it with the fields
|
|
28
|
+
* the schema names: the element reads each field once, and what it shows is what it checked. What
|
|
29
|
+
* the schema refuses is a TypeError that names the field by its path, a field the open view has not
|
|
30
|
+
* among it. A field is left out by `undefined` alone: `null` is a value, and a wrong one.
|
|
31
|
+
*/
|
|
32
|
+
export function taskOf(value) {
|
|
33
|
+
const task = closed(value, "task", OPEN, "a task's open view");
|
|
34
|
+
const id = idOf(task.id, "task.id");
|
|
35
|
+
const statement = listOf(task.statement, "task.statement", 1, "parts", partOf);
|
|
36
|
+
const canvas = canvasOf(task.canvas, "task.canvas");
|
|
37
|
+
if (canvas.kind === WORKBOOK_KIND) {
|
|
38
|
+
const items = listOf(task.items, "task.items", 1, "items", workbookItemOf);
|
|
39
|
+
return { id, statement, ...askedOf(task), canvas, items };
|
|
40
|
+
}
|
|
41
|
+
const items = sheetItemsOf(task.items);
|
|
42
|
+
return { id, statement, ...askedOf(task), canvas, items };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* What the statement gives and what the task asks to find, as the task has them: the element checks
|
|
46
|
+
* them and shows neither, for the chips are app's to show (architecture 3.1 spec).
|
|
47
|
+
*/
|
|
48
|
+
function askedOf({ givens, unknown, }) {
|
|
49
|
+
return {
|
|
50
|
+
...(givens === undefined
|
|
51
|
+
? {}
|
|
52
|
+
: { givens: textsOf(givens, "task.givens", 1, "givens", "a given") }),
|
|
53
|
+
...(unknown === undefined
|
|
54
|
+
? {}
|
|
55
|
+
: { unknown: text(unknown, "task.unknown", "what the task asks to find") }),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Whether a task is on a handwriting sheet. */
|
|
59
|
+
export function isSheet(task) {
|
|
60
|
+
return task.canvas.kind === SHEET_KIND;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Whether a task's one answer slot is anywhere on the sheet or the whole sheet, which the strokes
|
|
64
|
+
* on the sheet fill (R42): not a sheet of choices, whose options app marks, nor a workbook.
|
|
65
|
+
*/
|
|
66
|
+
export function wholeSheet(task) {
|
|
67
|
+
return (task !== undefined &&
|
|
68
|
+
isSheet(task) &&
|
|
69
|
+
task.items.some(({ answer_slots }) => answer_slots.some(isWhole)));
|
|
70
|
+
}
|
|
71
|
+
/** Whether an answer slot is anywhere on the sheet or the whole sheet: its task's only one. */
|
|
72
|
+
function isWhole({ binding }) {
|
|
73
|
+
return binding === "anywhere" || binding === "sheet";
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The template of a task `taskOf` gave, or of no task for `undefined`. A workbook's lines are laid
|
|
77
|
+
* out on its page, and a task whose lines do not fit is a TypeError at the first item that does not
|
|
78
|
+
* (R34, R35). A zone on the sheet is a TypeError until an epic of its own; a choice the sheet passes
|
|
79
|
+
* over, and an item's text too: app shows them (R32).
|
|
80
|
+
*/
|
|
81
|
+
export function templateOf(task) {
|
|
82
|
+
if (task === undefined)
|
|
83
|
+
return NO_TASK;
|
|
84
|
+
if (!isSheet(task)) {
|
|
85
|
+
return { page: pageOf(task.items), statement: task.statement, tools: allowedOf(task.canvas) };
|
|
86
|
+
}
|
|
87
|
+
for (const [i, item] of task.items.entries()) {
|
|
88
|
+
for (const [j, slot] of item.answer_slots.entries()) {
|
|
89
|
+
if (slot.binding === "zone") {
|
|
90
|
+
throw new TypeError(`task.items[${i}].answer_slots[${j}].binding: the handwriting sheet takes no "zone" yet, only "anywhere", "sheet" and "choice"`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
grid: gridFrom(task.canvas),
|
|
96
|
+
statement: task.statement,
|
|
97
|
+
tools: allowedOf(task.canvas),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** Whether a template is a workbook's. */
|
|
101
|
+
export function isWorkbook(template) {
|
|
102
|
+
return "page" in template;
|
|
103
|
+
}
|
|
104
|
+
/** The tools a task allows, of those the canvas has, in the order of TOOLS (R28). */
|
|
105
|
+
function allowedOf({ tools }) {
|
|
106
|
+
return tools === undefined ? TOOLS : TOOLS.filter((tool) => tools.includes(tool));
|
|
107
|
+
}
|
|
108
|
+
/** Whether two templates show the same: the same layer, the same statement, the same tools. */
|
|
109
|
+
export function sameTemplate(a, b) {
|
|
110
|
+
return sameLayer(a, b) && sameStatement(a, b) && sameTools(a, b);
|
|
111
|
+
}
|
|
112
|
+
/** Whether two templates allow the same tools. */
|
|
113
|
+
export function sameTools(a, b) {
|
|
114
|
+
return a.tools.length === b.tools.length && a.tools.every((tool, i) => tool === b.tools[i]);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Whether two templates draw the same on the layer of the template: the same grid on a sheet, the
|
|
118
|
+
* same lines on a workbook's page.
|
|
119
|
+
*/
|
|
120
|
+
export function sameLayer(a, b) {
|
|
121
|
+
if (isWorkbook(a) || isWorkbook(b)) {
|
|
122
|
+
return isWorkbook(a) && isWorkbook(b) && samePage(a.page, b.page);
|
|
123
|
+
}
|
|
124
|
+
return a.grid.kind === b.grid.kind && a.grid.step === b.grid.step;
|
|
125
|
+
}
|
|
126
|
+
/** Whether two templates show the same statement. */
|
|
127
|
+
export function sameStatement(a, b) {
|
|
128
|
+
return sameItems(a.statement, b.statement, samePart);
|
|
129
|
+
}
|
|
130
|
+
/** The names of Windows devices, which no ID or file of a module has (common.schema.json). */
|
|
131
|
+
const DEVICES = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/;
|
|
132
|
+
/** The file formats of a picture, by their extensions: PNG, JPEG and WebP. */
|
|
133
|
+
const PICTURES = ["png", "jpg", "webp"];
|
|
134
|
+
/**
|
|
135
|
+
* Whether a name is in kebab-case from a letter, as a module's names are: counting-gaps. The name
|
|
136
|
+
* is split at its hyphens rather than matched by a pattern of nested repeats: the check is linear.
|
|
137
|
+
*/
|
|
138
|
+
function kebab(name) {
|
|
139
|
+
const [first = "", ...rest] = name.split("-");
|
|
140
|
+
return /^[a-z][a-z0-9]*$/.test(first) && rest.every((word) => /^[a-z0-9]+$/.test(word));
|
|
141
|
+
}
|
|
142
|
+
/** The task's local ID: kebab-case, at most 64 characters, no Windows device (local_id). */
|
|
143
|
+
function idOf(value, path) {
|
|
144
|
+
if (typeof value !== "string" || value.length > 64 || !kebab(value) || DEVICES.test(value)) {
|
|
145
|
+
throw new TypeError(`${path}: the task's local ID, in kebab-case from a letter, at most 64 characters, no name of a Windows device, not ${shown(value)}`);
|
|
146
|
+
}
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
/** A part of a statement — a task's or a worked example's: a paragraph, or a picture. */
|
|
150
|
+
export function partOf(value, path) {
|
|
151
|
+
return textOrPictureOf(value, path, "the statement");
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* A part of a text the learner reads, `of` naming the text: a paragraph, or a picture with what it
|
|
155
|
+
* shows and the size it is shown at — of a task's statement, a worked example's, or its demo.
|
|
156
|
+
*/
|
|
157
|
+
export function textOrPictureOf(value, path, of) {
|
|
158
|
+
const part = closed(value, path, ["text", "picture", "alt", "width", "height"], "a part");
|
|
159
|
+
if (part.picture === undefined) {
|
|
160
|
+
closed(part, path, ["text"], `a paragraph of ${of}`);
|
|
161
|
+
return { text: text(part.text, `${path}.text`, "a paragraph") };
|
|
162
|
+
}
|
|
163
|
+
closed(part, path, ["picture", "alt", "width", "height"], `a picture of ${of}`);
|
|
164
|
+
const picture = pictureOf(part.picture, `${path}.picture`);
|
|
165
|
+
const alt = altOf(part.alt, `${path}.alt`);
|
|
166
|
+
const width = whole(part.width, `${path}.width`, 1);
|
|
167
|
+
return { picture, alt, width, height: whole(part.height, `${path}.height`, 1) };
|
|
168
|
+
}
|
|
169
|
+
/** What a picture shows, in words, which a screen reader reads: "" for one that only decorates. */
|
|
170
|
+
export function altOf(value, path) {
|
|
171
|
+
if (typeof value !== "string") {
|
|
172
|
+
throw new TypeError(`${path}: what the picture shows, in words, or "" for a picture that only decorates, not ${shown(value)}`);
|
|
173
|
+
}
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* A picture's path from the module's media/: names in kebab-case, none a Windows device, joined by
|
|
178
|
+
* /, and the extension of a PNG, JPEG or WebP file, at most 128 characters (picture_path).
|
|
179
|
+
*/
|
|
180
|
+
export function pictureOf(value, path) {
|
|
181
|
+
if (typeof value === "string" && value.length <= 128) {
|
|
182
|
+
const names = value.split("/");
|
|
183
|
+
const file = names.pop() ?? "";
|
|
184
|
+
const dot = file.lastIndexOf(".");
|
|
185
|
+
names.push(file.slice(0, dot));
|
|
186
|
+
if (dot > 0 &&
|
|
187
|
+
PICTURES.includes(file.slice(dot + 1)) &&
|
|
188
|
+
names.every((name) => kebab(name) && !DEVICES.test(name))) {
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
throw new TypeError(`${path}: a picture's path from media/, in kebab-case, of a .png, .jpg or .webp file, not ${shown(value)}`);
|
|
193
|
+
}
|
|
194
|
+
/** The canvas of a task: its kind, and the fields of that kind. */
|
|
195
|
+
function canvasOf(value, path) {
|
|
196
|
+
const canvas = closed(value, path, ["kind", "grid", "grid_step", "tools"], "a task's canvas");
|
|
197
|
+
if (canvas.kind === WORKBOOK_KIND) {
|
|
198
|
+
closed(canvas, path, ["kind", "tools"], "a workbook's canvas");
|
|
199
|
+
if (canvas.tools === undefined)
|
|
200
|
+
return { kind: WORKBOOK_KIND };
|
|
201
|
+
return { kind: WORKBOOK_KIND, tools: toolsOf(canvas.tools, `${path}.tools`) };
|
|
202
|
+
}
|
|
203
|
+
if (canvas.kind !== SHEET_KIND) {
|
|
204
|
+
throw new TypeError(`${path}.kind: "${SHEET_KIND}" or "${WORKBOOK_KIND}", not ${shown(canvas.kind)}`);
|
|
205
|
+
}
|
|
206
|
+
const sheet = {
|
|
207
|
+
kind: SHEET_KIND,
|
|
208
|
+
...gridOf(canvas, path),
|
|
209
|
+
};
|
|
210
|
+
if (canvas.tools !== undefined)
|
|
211
|
+
sheet.tools = toolsOf(canvas.tools, `${path}.tools`);
|
|
212
|
+
return sheet;
|
|
213
|
+
}
|
|
214
|
+
/** The grid a handwriting sheet's canvas draws, the defaults filled in: squares of 50 units. */
|
|
215
|
+
export function gridFrom({ grid = DEFAULT_GRID.kind, grid_step = GRID_STEP, }) {
|
|
216
|
+
return { kind: grid, step: grid_step };
|
|
217
|
+
}
|
|
218
|
+
/** A handwriting sheet's grid as its canvas gives it: its kind and its step, each if given. */
|
|
219
|
+
export function gridOf(canvas, path) {
|
|
220
|
+
const grid = {};
|
|
221
|
+
if (canvas.grid !== undefined) {
|
|
222
|
+
if (!GRID_KINDS.includes(canvas.grid)) {
|
|
223
|
+
throw new TypeError(`${path}.grid: "squares", "lines" or "dots", not ${shown(canvas.grid)}`);
|
|
224
|
+
}
|
|
225
|
+
grid.grid = canvas.grid;
|
|
226
|
+
}
|
|
227
|
+
if (canvas.grid_step !== undefined) {
|
|
228
|
+
grid.grid_step = whole(canvas.grid_step, `${path}.grid_step`, GRID_STEP_MIN, GRID_STEP_MAX);
|
|
229
|
+
}
|
|
230
|
+
return grid;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* The names of the tools a task allows (R28): strings, the pen among them. A name the canvas does
|
|
234
|
+
* not know it passes over when it makes its template.
|
|
235
|
+
*/
|
|
236
|
+
function toolsOf(value, path) {
|
|
237
|
+
const names = listOf(value, path, 0, "tools", (name, at) => {
|
|
238
|
+
if (typeof name !== "string") {
|
|
239
|
+
throw new TypeError(`${at}: a tool's name, a string, not ${shown(name)}`);
|
|
240
|
+
}
|
|
241
|
+
return name;
|
|
242
|
+
});
|
|
243
|
+
if (!names.includes("pen")) {
|
|
244
|
+
throw new TypeError(`${path}: the canvas writes with "pen", which ${shown(value)} lacks`);
|
|
245
|
+
}
|
|
246
|
+
return names;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The items of a handwriting sheet. An answer anywhere on the sheet, or the whole sheet, is the
|
|
250
|
+
* only answer slot of its task: AI could not tell two of them apart.
|
|
251
|
+
*/
|
|
252
|
+
function sheetItemsOf(value) {
|
|
253
|
+
const items = listOf(value, "task.items", 1, "items", sheetItemOf);
|
|
254
|
+
if (!items.some(({ answer_slots }) => answer_slots.some(isWhole)))
|
|
255
|
+
return items;
|
|
256
|
+
if (items.length > 1) {
|
|
257
|
+
throw new TypeError(`task.items: one item, for an answer anywhere on the sheet or the whole sheet is its task's only answer slot, not ${items.length}`);
|
|
258
|
+
}
|
|
259
|
+
const slots = items[0]?.answer_slots.length ?? 0;
|
|
260
|
+
if (slots > 1) {
|
|
261
|
+
throw new TypeError(`task.items[0].answer_slots: one answer slot, for an answer anywhere on the sheet or the whole sheet is its task's only one, not ${slots}`);
|
|
262
|
+
}
|
|
263
|
+
return items;
|
|
264
|
+
}
|
|
265
|
+
function sheetItemOf(value, path) {
|
|
266
|
+
const item = closed(value, path, ["text", "answer_slots"], "an item of a handwriting sheet");
|
|
267
|
+
const asks = item.text === undefined ? undefined : text(item.text, `${path}.text`, "what the item asks");
|
|
268
|
+
const slots = listOf(item.answer_slots, `${path}.answer_slots`, 1, "answer slots", slotOf);
|
|
269
|
+
return asks === undefined ? { answer_slots: slots } : { text: asks, answer_slots: slots };
|
|
270
|
+
}
|
|
271
|
+
function slotOf(value, path) {
|
|
272
|
+
const slot = closed(value, path, ["binding", "zone", "options"], "an answer slot");
|
|
273
|
+
const what = `an answer slot bound ${shown(slot.binding)}`;
|
|
274
|
+
switch (slot.binding) {
|
|
275
|
+
case "anywhere":
|
|
276
|
+
case "sheet":
|
|
277
|
+
closed(slot, path, ["binding"], what);
|
|
278
|
+
return { binding: slot.binding };
|
|
279
|
+
case "zone":
|
|
280
|
+
closed(slot, path, ["binding", "zone"], what);
|
|
281
|
+
return { binding: "zone", zone: zoneOf(slot.zone, `${path}.zone`) };
|
|
282
|
+
case "choice":
|
|
283
|
+
closed(slot, path, ["binding", "options"], what);
|
|
284
|
+
return { binding: "choice", options: optionsOf(slot.options, `${path}.options`) };
|
|
285
|
+
default:
|
|
286
|
+
throw new TypeError(`${path}.binding: "anywhere", "sheet", "zone" or "choice", not ${shown(slot.binding)}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
export function zoneOf(value, path) {
|
|
290
|
+
const zone = closed(value, path, ["left", "top", "width", "height"], "a zone");
|
|
291
|
+
return {
|
|
292
|
+
left: whole(zone.left, `${path}.left`, 0, 999),
|
|
293
|
+
top: whole(zone.top, `${path}.top`, 0, 1399),
|
|
294
|
+
width: whole(zone.width, `${path}.width`, 1, 1000),
|
|
295
|
+
height: whole(zone.height, `${path}.height`, 1, 1400),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
function optionsOf(value, path) {
|
|
299
|
+
return textsOf(value, path, 2, "options", "an option");
|
|
300
|
+
}
|
|
301
|
+
/** A list of at least `least` texts, all different, as a choice's options and a task's givens are. */
|
|
302
|
+
function textsOf(value, path, least, what, one) {
|
|
303
|
+
const texts = listOf(value, path, least, what, (item, at) => text(item, at, one));
|
|
304
|
+
if (new Set(texts).size < texts.length) {
|
|
305
|
+
throw new TypeError(`${path}: ${what} all different, not ${shown(value)}`);
|
|
306
|
+
}
|
|
307
|
+
return texts;
|
|
308
|
+
}
|
|
309
|
+
/** An item of a workbook: a line with at least one box. */
|
|
310
|
+
function workbookItemOf(value, path) {
|
|
311
|
+
const item = closed(value, path, ["line"], "an item of a workbook");
|
|
312
|
+
const line = listOf(item.line, `${path}.line`, 1, "pieces of text and boxes", linePartOf);
|
|
313
|
+
if (!line.some((part) => "binding" in part)) {
|
|
314
|
+
throw new TypeError(`${path}.line: at least one box, { binding: "box" }, not ${shown(item.line)}`);
|
|
315
|
+
}
|
|
316
|
+
return { line };
|
|
317
|
+
}
|
|
318
|
+
/** A piece of a workbook's line: a piece of text, or a box. */
|
|
319
|
+
function linePartOf(value, path) {
|
|
320
|
+
const part = closed(value, path, ["text", "binding"], "a piece of a line");
|
|
321
|
+
if (part.binding === undefined) {
|
|
322
|
+
closed(part, path, ["text"], "a piece of a line's text");
|
|
323
|
+
return { text: text(part.text, `${path}.text`, "a piece of the line's text") };
|
|
324
|
+
}
|
|
325
|
+
closed(part, path, ["binding"], "a box of a line");
|
|
326
|
+
if (part.binding !== "box") {
|
|
327
|
+
throw new TypeError(`${path}.binding: "box", a box of a workbook, not ${shown(part.binding)}`);
|
|
328
|
+
}
|
|
329
|
+
return { binding: "box" };
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Whether two parts of a text the learner reads show the same: the same paragraph, or the same
|
|
333
|
+
* picture with the same description and size.
|
|
334
|
+
*/
|
|
335
|
+
export function samePart(a, b) {
|
|
336
|
+
if ("text" in a)
|
|
337
|
+
return "text" in b && a.text === b.text;
|
|
338
|
+
return ("picture" in b &&
|
|
339
|
+
a.picture === b.picture &&
|
|
340
|
+
a.alt === b.alt &&
|
|
341
|
+
a.width === b.width &&
|
|
342
|
+
a.height === b.height);
|
|
343
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** A digit: they are all as wide, and the page asks for figures of one width (R35). */
|
|
2
|
+
export declare const DIGIT = 53;
|
|
3
|
+
/** A sign of an operation or a comparison: one width for all of them, the mean of theirs. */
|
|
4
|
+
export declare const SIGN = 56;
|
|
5
|
+
/** The signs of operations and comparisons, one width for all (R35). */
|
|
6
|
+
export declare const SIGNS = "+\u2212\u00D7\u00F7=<>\u2264\u2265\u2260\u00B1\u2248";
|
|
7
|
+
/** The space between two words, which the layout sets itself. */
|
|
8
|
+
export declare const SPACE = 26;
|
|
9
|
+
/** A wide character — a Chinese, Japanese or Korean one, an emoji: a square. */
|
|
10
|
+
export declare const WIDE = 100;
|
|
11
|
+
/** A character the table does not know, a Greek or a Hebrew letter among them. */
|
|
12
|
+
export declare const UNKNOWN = 60;
|
|
13
|
+
/**
|
|
14
|
+
* The characters of their own width: printable ASCII, the Russian alphabet and the marks a school
|
|
15
|
+
* page has, by width. ⋅ and ∶ the fonts lack: they take the width of · and of :. The space is
|
|
16
|
+
* SPACE, set by the layout.
|
|
17
|
+
*/
|
|
18
|
+
export declare const BY_WIDTH: readonly (readonly [number, string])[];
|
|
19
|
+
/**
|
|
20
|
+
* Code points of no width, each joining the character before it: controls, the soft hyphen,
|
|
21
|
+
* combining marks, the zero-width space and joiners, the marks of direction, variation selectors,
|
|
22
|
+
* the skin tones of emoji and tags.
|
|
23
|
+
*/
|
|
24
|
+
export declare const ZERO: readonly (readonly [number, number])[];
|
|
25
|
+
/** Code points of a square's width: Hangul, CJK, the full-width forms and emoji. */
|
|
26
|
+
export declare const WIDE_RANGES: readonly (readonly [number, number])[];
|
|
27
|
+
/**
|
|
28
|
+
* The white space a line breaks at: tab, the line and paragraph ends and the space. A run of it is
|
|
29
|
+
* one space between two words, and none at the ends of a piece. Any other space — a no-break one
|
|
30
|
+
* among them — is a character of its word.
|
|
31
|
+
*/
|
|
32
|
+
export declare const BREAKS: readonly number[];
|
|
33
|
+
/**
|
|
34
|
+
* The spaces that do not break, by code point, for they do not show: the no-break space, the figure
|
|
35
|
+
* space, a digit wide, and the narrow no-break space.
|
|
36
|
+
*/
|
|
37
|
+
export declare const SPACES: readonly (readonly [number, number])[];
|
|
38
|
+
/** The width of a code point, in hundredths of the type size. */
|
|
39
|
+
export declare function widthOf(code: number): number;
|
|
40
|
+
/** Whether a line may break at this code point, white space that collapses. */
|
|
41
|
+
export declare function breaks(code: number): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* The words of a piece of text: what lies between its runs of white space. A piece of white space
|
|
44
|
+
* alone has none.
|
|
45
|
+
*/
|
|
46
|
+
export declare function wordsOf(text: string): string[];
|
|
47
|
+
/**
|
|
48
|
+
* The clusters of a word, which a word too long for a line is broken between: a code point of no
|
|
49
|
+
* width, and whatever follows a joiner, go with the one before them, and so does the second
|
|
50
|
+
* regional indicator of a flag. The code points come by `for…of`, which pairs surrogates by the
|
|
51
|
+
* language's own rule, not by Unicode's tables.
|
|
52
|
+
*/
|
|
53
|
+
export declare function clustersOf(word: string): string[];
|
|
54
|
+
/** The width of a cluster: that of its first code point. */
|
|
55
|
+
export declare function clusterWidth(cluster: string): number;
|
|
56
|
+
/** The width of a word, in hundredths of the type size: the sum of its clusters'. */
|
|
57
|
+
export declare function wordWidth(word: string): number;
|