@docentjs/core 0.5.2 → 0.7.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/dist/index.cjs +36 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -8
- package/dist/index.d.ts +38 -8
- package/dist/index.js +36 -2
- package/dist/index.js.map +1 -1
- package/dist/validate.cjs +861 -0
- package/dist/validate.cjs.map +1 -0
- package/dist/validate.d.cts +57 -0
- package/dist/validate.d.ts +57 -0
- package/dist/validate.js +847 -0
- package/dist/validate.js.map +1 -0
- package/package.json +11 -1
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/schema/spec.ts
|
|
3
|
+
const field = (spec, doc, extra = {}) => ({
|
|
4
|
+
spec,
|
|
5
|
+
...doc === void 0 ? {} : { doc },
|
|
6
|
+
...extra
|
|
7
|
+
});
|
|
8
|
+
const required = (spec, doc) => field(spec, doc, { required: true });
|
|
9
|
+
const ref = (name) => ({
|
|
10
|
+
kind: "ref",
|
|
11
|
+
name
|
|
12
|
+
});
|
|
13
|
+
const str = { kind: "string" };
|
|
14
|
+
const num = { kind: "number" };
|
|
15
|
+
const bool = { kind: "boolean" };
|
|
16
|
+
const enums = (...values) => ({
|
|
17
|
+
kind: "enum",
|
|
18
|
+
values
|
|
19
|
+
});
|
|
20
|
+
/** A size, time or opacity token: a CSS string, or a number in the token's unit. */
|
|
21
|
+
const themeValue = {
|
|
22
|
+
kind: "union",
|
|
23
|
+
of: [str, num]
|
|
24
|
+
};
|
|
25
|
+
const ARROW_STYLES = [
|
|
26
|
+
"caret",
|
|
27
|
+
"none",
|
|
28
|
+
"line",
|
|
29
|
+
"dashed",
|
|
30
|
+
"dotted",
|
|
31
|
+
"curve",
|
|
32
|
+
"curve-dashed",
|
|
33
|
+
"squiggle",
|
|
34
|
+
"loop",
|
|
35
|
+
"elbow",
|
|
36
|
+
"sketch",
|
|
37
|
+
"pin"
|
|
38
|
+
];
|
|
39
|
+
const SPOTLIGHT_SHAPES = [
|
|
40
|
+
"rounded",
|
|
41
|
+
"rect",
|
|
42
|
+
"pill",
|
|
43
|
+
"circle"
|
|
44
|
+
];
|
|
45
|
+
const SPOTLIGHT_RINGS = [
|
|
46
|
+
"hairline",
|
|
47
|
+
"none",
|
|
48
|
+
"glow",
|
|
49
|
+
"pulse",
|
|
50
|
+
"dashed",
|
|
51
|
+
"solid"
|
|
52
|
+
];
|
|
53
|
+
const OVERLAY_STYLES = [
|
|
54
|
+
"dim",
|
|
55
|
+
"blur",
|
|
56
|
+
"vignette",
|
|
57
|
+
"none"
|
|
58
|
+
];
|
|
59
|
+
const THEME_NAMES = [
|
|
60
|
+
"light",
|
|
61
|
+
"dark",
|
|
62
|
+
"minimal",
|
|
63
|
+
"contrast"
|
|
64
|
+
];
|
|
65
|
+
const PLACEMENTS = [
|
|
66
|
+
"auto",
|
|
67
|
+
"top",
|
|
68
|
+
"right",
|
|
69
|
+
"bottom",
|
|
70
|
+
"left",
|
|
71
|
+
"top-start",
|
|
72
|
+
"top-end",
|
|
73
|
+
"right-start",
|
|
74
|
+
"right-end",
|
|
75
|
+
"bottom-start",
|
|
76
|
+
"bottom-end",
|
|
77
|
+
"left-start",
|
|
78
|
+
"left-end"
|
|
79
|
+
];
|
|
80
|
+
const TRAIT_OPERATORS = [
|
|
81
|
+
"eq",
|
|
82
|
+
"neq",
|
|
83
|
+
"gt",
|
|
84
|
+
"gte",
|
|
85
|
+
"lt",
|
|
86
|
+
"lte",
|
|
87
|
+
"in",
|
|
88
|
+
"nin",
|
|
89
|
+
"contains",
|
|
90
|
+
"exists",
|
|
91
|
+
"missing"
|
|
92
|
+
];
|
|
93
|
+
const TOUR_STATES = [
|
|
94
|
+
"not-started",
|
|
95
|
+
"in-progress",
|
|
96
|
+
"completed",
|
|
97
|
+
"skipped"
|
|
98
|
+
];
|
|
99
|
+
const target = {
|
|
100
|
+
kind: "union",
|
|
101
|
+
of: [{
|
|
102
|
+
kind: "string",
|
|
103
|
+
doc: "A CSS selector."
|
|
104
|
+
}, {
|
|
105
|
+
kind: "object",
|
|
106
|
+
fields: {
|
|
107
|
+
name: field(str, "Logical name, matching `data-docent=\"<name>\"`. The sturdiest anchor."),
|
|
108
|
+
selectors: field({
|
|
109
|
+
kind: "array",
|
|
110
|
+
items: str
|
|
111
|
+
}, "CSS fallbacks, tried in order."),
|
|
112
|
+
native: field(str, "Native identifier, when it differs from the name."),
|
|
113
|
+
within: field(str, "Selector of a container to search inside."),
|
|
114
|
+
nth: field(num, "Which match to use, counting from 0.")
|
|
115
|
+
}
|
|
116
|
+
}]
|
|
117
|
+
};
|
|
118
|
+
const advance = {
|
|
119
|
+
kind: "union",
|
|
120
|
+
by: "on",
|
|
121
|
+
of: [
|
|
122
|
+
{
|
|
123
|
+
kind: "enum",
|
|
124
|
+
values: ["button"],
|
|
125
|
+
doc: "The reader presses Next (the default)."
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
kind: "object",
|
|
129
|
+
doc: "The reader clicks the target.",
|
|
130
|
+
fields: {
|
|
131
|
+
on: required(enums("click")),
|
|
132
|
+
target: field(ref("target"))
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
kind: "object",
|
|
137
|
+
doc: "What the reader types matches a regular expression.",
|
|
138
|
+
fields: {
|
|
139
|
+
on: required(enums("input")),
|
|
140
|
+
target: field(ref("target")),
|
|
141
|
+
match: field(str, "Regular expression the value must match.")
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
kind: "object",
|
|
146
|
+
doc: "Your code reports a named event.",
|
|
147
|
+
fields: {
|
|
148
|
+
on: required(enums("event")),
|
|
149
|
+
name: required(str)
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
kind: "object",
|
|
154
|
+
doc: "An element appears on the page.",
|
|
155
|
+
fields: {
|
|
156
|
+
on: required(enums("element")),
|
|
157
|
+
target: required(ref("target"))
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
kind: "object",
|
|
162
|
+
doc: "After a delay.",
|
|
163
|
+
fields: {
|
|
164
|
+
on: required(enums("delay")),
|
|
165
|
+
ms: required({
|
|
166
|
+
kind: "number",
|
|
167
|
+
min: 0
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
]
|
|
172
|
+
};
|
|
173
|
+
const trigger = {
|
|
174
|
+
kind: "union",
|
|
175
|
+
by: "type",
|
|
176
|
+
of: [
|
|
177
|
+
{
|
|
178
|
+
kind: "object",
|
|
179
|
+
doc: "Only starts from code.",
|
|
180
|
+
fields: { type: required(enums("manual")) }
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
kind: "object",
|
|
184
|
+
doc: "When the page loads.",
|
|
185
|
+
fields: {
|
|
186
|
+
type: required(enums("auto")),
|
|
187
|
+
delay: field({
|
|
188
|
+
kind: "number",
|
|
189
|
+
min: 0
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
kind: "object",
|
|
195
|
+
doc: "On a matching path.",
|
|
196
|
+
fields: {
|
|
197
|
+
type: required(enums("route")),
|
|
198
|
+
pattern: required(str, "Path pattern, such as `/invoices/**`."),
|
|
199
|
+
delay: field({
|
|
200
|
+
kind: "number",
|
|
201
|
+
min: 0
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
kind: "object",
|
|
207
|
+
doc: "When an element appears.",
|
|
208
|
+
fields: {
|
|
209
|
+
type: required(enums("element")),
|
|
210
|
+
target: required(ref("target")),
|
|
211
|
+
delay: field({
|
|
212
|
+
kind: "number",
|
|
213
|
+
min: 0
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
kind: "object",
|
|
219
|
+
doc: "When your code reports an event.",
|
|
220
|
+
fields: {
|
|
221
|
+
type: required(enums("event")),
|
|
222
|
+
name: required(str)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
]
|
|
226
|
+
};
|
|
227
|
+
const traitValue = {
|
|
228
|
+
kind: "union",
|
|
229
|
+
of: [
|
|
230
|
+
str,
|
|
231
|
+
num,
|
|
232
|
+
bool,
|
|
233
|
+
{
|
|
234
|
+
kind: "array",
|
|
235
|
+
items: str
|
|
236
|
+
}
|
|
237
|
+
]
|
|
238
|
+
};
|
|
239
|
+
const condition = {
|
|
240
|
+
kind: "union",
|
|
241
|
+
by: "type",
|
|
242
|
+
of: [
|
|
243
|
+
{
|
|
244
|
+
kind: "object",
|
|
245
|
+
doc: "Compare one of the user's traits.",
|
|
246
|
+
fields: {
|
|
247
|
+
type: required(enums("trait")),
|
|
248
|
+
key: required(str),
|
|
249
|
+
op: required({
|
|
250
|
+
kind: "enum",
|
|
251
|
+
values: TRAIT_OPERATORS
|
|
252
|
+
}),
|
|
253
|
+
value: field(traitValue)
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
kind: "object",
|
|
258
|
+
doc: "The current path matches.",
|
|
259
|
+
fields: {
|
|
260
|
+
type: required(enums("route")),
|
|
261
|
+
pattern: required(str)
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
kind: "object",
|
|
266
|
+
doc: "An element is, or is not, on the page.",
|
|
267
|
+
fields: {
|
|
268
|
+
type: required(enums("element")),
|
|
269
|
+
target: required(ref("target")),
|
|
270
|
+
exists: field(bool, "Set false to require the element to be absent.")
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
kind: "object",
|
|
275
|
+
doc: "Another tour's progress for this user.",
|
|
276
|
+
fields: {
|
|
277
|
+
type: required(enums("tour")),
|
|
278
|
+
id: required(str),
|
|
279
|
+
state: required({
|
|
280
|
+
kind: "enum",
|
|
281
|
+
values: TOUR_STATES
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
kind: "object",
|
|
287
|
+
doc: "Every condition holds.",
|
|
288
|
+
fields: {
|
|
289
|
+
type: required(enums("all")),
|
|
290
|
+
conditions: required({
|
|
291
|
+
kind: "array",
|
|
292
|
+
items: ref("condition")
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
kind: "object",
|
|
298
|
+
doc: "At least one condition holds.",
|
|
299
|
+
fields: {
|
|
300
|
+
type: required(enums("any")),
|
|
301
|
+
conditions: required({
|
|
302
|
+
kind: "array",
|
|
303
|
+
items: ref("condition")
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
kind: "object",
|
|
309
|
+
doc: "The inner condition does not hold.",
|
|
310
|
+
fields: {
|
|
311
|
+
type: required(enums("not")),
|
|
312
|
+
condition: required(ref("condition"))
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
kind: "object",
|
|
317
|
+
doc: "A predicate your app registered by name.",
|
|
318
|
+
fields: {
|
|
319
|
+
type: required(enums("custom")),
|
|
320
|
+
name: required(str),
|
|
321
|
+
args: field({
|
|
322
|
+
kind: "record",
|
|
323
|
+
values: traitValue
|
|
324
|
+
})
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
]
|
|
328
|
+
};
|
|
329
|
+
const theme = {
|
|
330
|
+
kind: "object",
|
|
331
|
+
doc: "Visual tokens. Each becomes a `--docent-*` custom property.",
|
|
332
|
+
fields: {
|
|
333
|
+
preset: field({
|
|
334
|
+
kind: "enum",
|
|
335
|
+
values: THEME_NAMES
|
|
336
|
+
}, "Start from a built-in preset, then override tokens below."),
|
|
337
|
+
background: field(str),
|
|
338
|
+
foreground: field(str),
|
|
339
|
+
muted: field(str, "Secondary text."),
|
|
340
|
+
accent: field(str, "The Next button. Its text colour is chosen for you unless you set one."),
|
|
341
|
+
accentForeground: field(str),
|
|
342
|
+
connector: field(str, "Colour of drawn arrows."),
|
|
343
|
+
ring: field(str, "Colour of the spotlight ring."),
|
|
344
|
+
radius: field(themeValue, "Popover corners. A number means px."),
|
|
345
|
+
shadow: field(str),
|
|
346
|
+
font: field(str, "Defaults to the page's own font."),
|
|
347
|
+
width: field(themeValue, "Popover width. A number means px."),
|
|
348
|
+
overlay: field(str, "Backdrop colour.", { deprecated: "Prefer `options.overlay.color`." }),
|
|
349
|
+
overlayOpacity: field(themeValue, "Backdrop opacity, 0 to 1.", { deprecated: "Prefer `options.overlay.opacity`." }),
|
|
350
|
+
duration: field(themeValue, "Transition time. A number means ms."),
|
|
351
|
+
zIndex: field(themeValue)
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
const themeSpec = {
|
|
355
|
+
kind: "union",
|
|
356
|
+
of: [{
|
|
357
|
+
kind: "enum",
|
|
358
|
+
values: THEME_NAMES,
|
|
359
|
+
doc: "A built-in preset by name."
|
|
360
|
+
}, theme]
|
|
361
|
+
};
|
|
362
|
+
const spotlight = {
|
|
363
|
+
kind: "object",
|
|
364
|
+
fields: {
|
|
365
|
+
padding: field(num, "Space around the target, in px."),
|
|
366
|
+
radius: field(num, "Corner radius of the cutout, in px."),
|
|
367
|
+
shape: field({
|
|
368
|
+
kind: "enum",
|
|
369
|
+
values: SPOTLIGHT_SHAPES
|
|
370
|
+
}),
|
|
371
|
+
ring: field({
|
|
372
|
+
kind: "enum",
|
|
373
|
+
values: SPOTLIGHT_RINGS
|
|
374
|
+
}),
|
|
375
|
+
animate: field(bool, "Animate the cutout as it moves between steps.")
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
const overlay = {
|
|
379
|
+
kind: "object",
|
|
380
|
+
fields: {
|
|
381
|
+
style: field({
|
|
382
|
+
kind: "enum",
|
|
383
|
+
values: OVERLAY_STYLES
|
|
384
|
+
}),
|
|
385
|
+
color: field(str),
|
|
386
|
+
opacity: field({
|
|
387
|
+
kind: "number",
|
|
388
|
+
min: 0,
|
|
389
|
+
max: 1
|
|
390
|
+
}),
|
|
391
|
+
blur: field({
|
|
392
|
+
kind: "number",
|
|
393
|
+
min: 0
|
|
394
|
+
}, "Blur radius for the `blur` style, in px.")
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
const scroll = {
|
|
398
|
+
kind: "object",
|
|
399
|
+
fields: {
|
|
400
|
+
enabled: field(bool),
|
|
401
|
+
behavior: field(enums("auto", "smooth")),
|
|
402
|
+
block: field(enums("start", "center", "end", "nearest"))
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
const labels = {
|
|
406
|
+
kind: "object",
|
|
407
|
+
fields: {
|
|
408
|
+
next: field(str),
|
|
409
|
+
back: field(str),
|
|
410
|
+
skip: field(str),
|
|
411
|
+
done: field(str),
|
|
412
|
+
close: field(str),
|
|
413
|
+
progress: field(str, "Supports `{current}` and `{total}`.")
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
const options = {
|
|
417
|
+
kind: "object",
|
|
418
|
+
fields: {
|
|
419
|
+
persist: field(bool, "Remember the current step so `resume()` can continue."),
|
|
420
|
+
frequency: field(enums("once", "until-completed", "always")),
|
|
421
|
+
showProgress: field(bool),
|
|
422
|
+
allowClose: field(bool, "Allow Escape and the close button."),
|
|
423
|
+
closeOnOverlayClick: field(bool),
|
|
424
|
+
keyboard: field(bool, "Arrow-key navigation."),
|
|
425
|
+
arrow: field({
|
|
426
|
+
kind: "enum",
|
|
427
|
+
values: ARROW_STYLES
|
|
428
|
+
}),
|
|
429
|
+
spotlight: field(spotlight),
|
|
430
|
+
overlay: field(overlay),
|
|
431
|
+
scroll: field(scroll),
|
|
432
|
+
labels: field(labels),
|
|
433
|
+
theme: field(themeSpec),
|
|
434
|
+
appearance: field(enums("light", "dark", "auto"), "`auto` follows the reader's system setting."),
|
|
435
|
+
template: field(str, "A built-in look ('spotlight', 'hint' or 'announcement') or a template the app registered.")
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
const step = {
|
|
439
|
+
kind: "object",
|
|
440
|
+
fields: {
|
|
441
|
+
id: required(str, "Unique within the tour. Keep it stable once shipped."),
|
|
442
|
+
target: field(ref("target"), "Leave out for a centred card."),
|
|
443
|
+
title: field(str),
|
|
444
|
+
body: field(str),
|
|
445
|
+
format: field(enums("text", "markdown"), "Markdown is a safe subset; HTML is never injected."),
|
|
446
|
+
media: field({
|
|
447
|
+
kind: "object",
|
|
448
|
+
fields: {
|
|
449
|
+
type: required(enums("image", "video")),
|
|
450
|
+
src: required(str),
|
|
451
|
+
alt: field(str)
|
|
452
|
+
}
|
|
453
|
+
}),
|
|
454
|
+
placement: field({
|
|
455
|
+
kind: "enum",
|
|
456
|
+
values: PLACEMENTS
|
|
457
|
+
}),
|
|
458
|
+
arrow: field({
|
|
459
|
+
kind: "enum",
|
|
460
|
+
values: ARROW_STYLES
|
|
461
|
+
}),
|
|
462
|
+
spotlight: field(spotlight),
|
|
463
|
+
overlay: field(overlay),
|
|
464
|
+
advance: field(advance, "How the step finishes."),
|
|
465
|
+
interaction: field(enums("block", "allow"), "Whether the target can be used during the step."),
|
|
466
|
+
condition: field(ref("condition"), "Skip this step when the condition is false."),
|
|
467
|
+
onMissing: field(enums("skip", "wait", "abort"), "When the target is not on the page."),
|
|
468
|
+
waitFor: field({
|
|
469
|
+
kind: "number",
|
|
470
|
+
min: 0
|
|
471
|
+
}, "How long to wait for the target, in ms."),
|
|
472
|
+
route: field(str, "Path pattern this step belongs to."),
|
|
473
|
+
buttons: field({
|
|
474
|
+
kind: "object",
|
|
475
|
+
fields: {
|
|
476
|
+
back: field(bool),
|
|
477
|
+
next: field(bool),
|
|
478
|
+
skip: field(bool),
|
|
479
|
+
close: field(bool)
|
|
480
|
+
}
|
|
481
|
+
}),
|
|
482
|
+
scroll: field(scroll),
|
|
483
|
+
meta: field({
|
|
484
|
+
kind: "record",
|
|
485
|
+
values: { kind: "any" }
|
|
486
|
+
})
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
const SPECS = {
|
|
490
|
+
tour: {
|
|
491
|
+
kind: "object",
|
|
492
|
+
doc: "A guided tour: what to show, to whom, and when.",
|
|
493
|
+
fields: {
|
|
494
|
+
$schema: field(str, "Optional link to this schema, for editors: https://docentjs.dev/schema/tour-v1.json"),
|
|
495
|
+
schemaVersion: field({
|
|
496
|
+
kind: "number",
|
|
497
|
+
min: 1,
|
|
498
|
+
max: 1
|
|
499
|
+
}, "`defineTour` sets this for you."),
|
|
500
|
+
id: required(str, "Stable identifier, used for progress and analytics."),
|
|
501
|
+
version: field(num, "Bump to show the tour again to people who saw an older one."),
|
|
502
|
+
name: field(str),
|
|
503
|
+
description: field(str),
|
|
504
|
+
steps: required({
|
|
505
|
+
kind: "array",
|
|
506
|
+
items: ref("step")
|
|
507
|
+
}),
|
|
508
|
+
trigger: field(ref("trigger"), "What starts the tour. Without one, only code can."),
|
|
509
|
+
conditions: field({
|
|
510
|
+
kind: "array",
|
|
511
|
+
items: ref("condition")
|
|
512
|
+
}, "All must hold."),
|
|
513
|
+
options: field(options),
|
|
514
|
+
meta: field({
|
|
515
|
+
kind: "record",
|
|
516
|
+
values: { kind: "any" }
|
|
517
|
+
})
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
step,
|
|
521
|
+
target,
|
|
522
|
+
advance,
|
|
523
|
+
trigger,
|
|
524
|
+
condition,
|
|
525
|
+
theme
|
|
526
|
+
};
|
|
527
|
+
//#endregion
|
|
528
|
+
//#region src/schema/json-schema.ts
|
|
529
|
+
const SCHEMA_ID = "https://docentjs.dev/schema/tour-v1.json";
|
|
530
|
+
/** The whole schema document, ready to write to a file. */
|
|
531
|
+
function tourJsonSchema() {
|
|
532
|
+
const definitions = {};
|
|
533
|
+
for (const name of Object.keys(SPECS)) {
|
|
534
|
+
if (name === "tour") continue;
|
|
535
|
+
definitions[name] = convert(SPECS[name]);
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
539
|
+
$id: SCHEMA_ID,
|
|
540
|
+
title: "Docent tour",
|
|
541
|
+
description: "A guided product tour. See https://docentjs.dev/reference/schema/.",
|
|
542
|
+
...convert(SPECS.tour),
|
|
543
|
+
$defs: definitions
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
function convert(spec) {
|
|
547
|
+
const doc = spec.doc ? { description: spec.doc } : {};
|
|
548
|
+
switch (spec.kind) {
|
|
549
|
+
case "ref": return {
|
|
550
|
+
$ref: `#/$defs/${spec.name}`,
|
|
551
|
+
...doc
|
|
552
|
+
};
|
|
553
|
+
case "any": return { ...doc };
|
|
554
|
+
case "string": return {
|
|
555
|
+
type: "string",
|
|
556
|
+
...doc
|
|
557
|
+
};
|
|
558
|
+
case "boolean": return {
|
|
559
|
+
type: "boolean",
|
|
560
|
+
...doc
|
|
561
|
+
};
|
|
562
|
+
case "number": return {
|
|
563
|
+
type: "number",
|
|
564
|
+
...spec.min === void 0 ? {} : { minimum: spec.min },
|
|
565
|
+
...spec.max === void 0 ? {} : { maximum: spec.max },
|
|
566
|
+
...doc
|
|
567
|
+
};
|
|
568
|
+
case "enum": return {
|
|
569
|
+
enum: [...spec.values],
|
|
570
|
+
...doc
|
|
571
|
+
};
|
|
572
|
+
case "array": return {
|
|
573
|
+
type: "array",
|
|
574
|
+
items: convert(spec.items),
|
|
575
|
+
...doc
|
|
576
|
+
};
|
|
577
|
+
case "record": return {
|
|
578
|
+
type: "object",
|
|
579
|
+
additionalProperties: convert(spec.values),
|
|
580
|
+
...doc
|
|
581
|
+
};
|
|
582
|
+
case "union": return {
|
|
583
|
+
anyOf: spec.of.map(convert),
|
|
584
|
+
...doc
|
|
585
|
+
};
|
|
586
|
+
case "object": {
|
|
587
|
+
const properties = {};
|
|
588
|
+
const requiredKeys = [];
|
|
589
|
+
for (const [key, field] of Object.entries(spec.fields)) {
|
|
590
|
+
const description = field.deprecated ? `${field.doc ? `${field.doc} ` : ""}${field.deprecated}` : field.doc;
|
|
591
|
+
properties[key] = {
|
|
592
|
+
...convert(field.spec),
|
|
593
|
+
...description ? { description } : {},
|
|
594
|
+
...field.deprecated ? { deprecated: true } : {}
|
|
595
|
+
};
|
|
596
|
+
if (field.required) requiredKeys.push(key);
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
type: "object",
|
|
600
|
+
properties,
|
|
601
|
+
...requiredKeys.length > 0 ? { required: requiredKeys } : {},
|
|
602
|
+
...doc
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
//#endregion
|
|
608
|
+
//#region src/schema/validate.ts
|
|
609
|
+
/**
|
|
610
|
+
* Check a tour. An empty result means it matches the schema; it does not mean
|
|
611
|
+
* the targets exist on the page, which the devtools Audit tab checks.
|
|
612
|
+
*/
|
|
613
|
+
function validateTour(tour, options = {}) {
|
|
614
|
+
const issues = [];
|
|
615
|
+
check(tour, {
|
|
616
|
+
kind: "ref",
|
|
617
|
+
name: "tour"
|
|
618
|
+
}, "", issues, options.unknownFields !== false);
|
|
619
|
+
if (isObject(tour) && Array.isArray(tour.steps)) {
|
|
620
|
+
const seen = /* @__PURE__ */ new Set();
|
|
621
|
+
tour.steps.forEach((step, i) => {
|
|
622
|
+
const id = isObject(step) ? step.id : void 0;
|
|
623
|
+
if (typeof id !== "string") return;
|
|
624
|
+
if (seen.has(id)) issues.push({
|
|
625
|
+
level: "error",
|
|
626
|
+
path: `steps[${i}].id`,
|
|
627
|
+
message: `Duplicate step id "${id}". Ids must be unique within a tour.`
|
|
628
|
+
});
|
|
629
|
+
seen.add(id);
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
return issues;
|
|
633
|
+
}
|
|
634
|
+
/** True when the tour matches the schema. */
|
|
635
|
+
function isValidTour(tour) {
|
|
636
|
+
return validateTour(tour).every((issue) => issue.level !== "error");
|
|
637
|
+
}
|
|
638
|
+
/** One line per issue, for a console warning or a CI log. */
|
|
639
|
+
function formatIssues(issues) {
|
|
640
|
+
return issues.map((issue) => {
|
|
641
|
+
const where = issue.path === "" ? "tour" : issue.path;
|
|
642
|
+
const hint = issue.suggestion ? ` Did you mean "${issue.suggestion}"?` : "";
|
|
643
|
+
return `${issue.level === "error" ? "✗" : "!"} ${where}: ${issue.message}${hint}`;
|
|
644
|
+
}).join("\n");
|
|
645
|
+
}
|
|
646
|
+
function check(value, spec, path, issues, unknownFields) {
|
|
647
|
+
switch (spec.kind) {
|
|
648
|
+
case "ref":
|
|
649
|
+
check(value, SPECS[spec.name], path, issues, unknownFields);
|
|
650
|
+
return;
|
|
651
|
+
case "any": return;
|
|
652
|
+
case "string":
|
|
653
|
+
if (typeof value !== "string") issues.push(wrongType(path, "a string", value));
|
|
654
|
+
return;
|
|
655
|
+
case "boolean":
|
|
656
|
+
if (typeof value !== "boolean") issues.push(wrongType(path, "true or false", value));
|
|
657
|
+
return;
|
|
658
|
+
case "number":
|
|
659
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
660
|
+
issues.push(wrongType(path, "a number", value));
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (spec.min !== void 0 && value < spec.min) issues.push({
|
|
664
|
+
level: "error",
|
|
665
|
+
path,
|
|
666
|
+
message: `${value} is below the minimum ${spec.min}.`
|
|
667
|
+
});
|
|
668
|
+
if (spec.max !== void 0 && value > spec.max) issues.push({
|
|
669
|
+
level: "error",
|
|
670
|
+
path,
|
|
671
|
+
message: `${value} is above the maximum ${spec.max}.`
|
|
672
|
+
});
|
|
673
|
+
return;
|
|
674
|
+
case "enum": {
|
|
675
|
+
if (typeof value === "string" && spec.values.includes(value)) return;
|
|
676
|
+
const near = typeof value === "string" ? nearest(value, spec.values) : void 0;
|
|
677
|
+
issues.push({
|
|
678
|
+
level: "error",
|
|
679
|
+
path,
|
|
680
|
+
message: `${show(value)} is not one of: ${spec.values.join(", ")}.`,
|
|
681
|
+
...near ? { suggestion: near } : {}
|
|
682
|
+
});
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
case "array":
|
|
686
|
+
if (!Array.isArray(value)) {
|
|
687
|
+
issues.push(wrongType(path, "a list", value));
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
value.forEach((item, i) => {
|
|
691
|
+
check(item, spec.items, `${path}[${i}]`, issues, unknownFields);
|
|
692
|
+
});
|
|
693
|
+
return;
|
|
694
|
+
case "record":
|
|
695
|
+
if (!isObject(value)) {
|
|
696
|
+
issues.push(wrongType(path, "an object", value));
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
for (const [key, item] of Object.entries(value)) check(item, spec.values, join(path, key), issues, unknownFields);
|
|
700
|
+
return;
|
|
701
|
+
case "object":
|
|
702
|
+
if (!isObject(value)) {
|
|
703
|
+
issues.push(wrongType(path, "an object", value));
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
checkFields(value, spec.fields, path, issues, unknownFields);
|
|
707
|
+
return;
|
|
708
|
+
case "union": {
|
|
709
|
+
const branch = pickBranch(value, spec, path, issues);
|
|
710
|
+
if (branch) check(value, branch, path, issues, unknownFields);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
function checkFields(value, fields, path, issues, unknownFields) {
|
|
716
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
717
|
+
const item = value[key];
|
|
718
|
+
if (item === void 0) {
|
|
719
|
+
if (field.required) issues.push({
|
|
720
|
+
level: "error",
|
|
721
|
+
path: join(path, key),
|
|
722
|
+
message: "Required field is missing."
|
|
723
|
+
});
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
if (field.deprecated) issues.push({
|
|
727
|
+
level: "warning",
|
|
728
|
+
path: join(path, key),
|
|
729
|
+
message: field.deprecated
|
|
730
|
+
});
|
|
731
|
+
check(item, field.spec, join(path, key), issues, unknownFields);
|
|
732
|
+
}
|
|
733
|
+
if (!unknownFields) return;
|
|
734
|
+
const known = Object.keys(fields);
|
|
735
|
+
for (const key of Object.keys(value)) {
|
|
736
|
+
if (known.includes(key)) continue;
|
|
737
|
+
const near = nearest(key, known);
|
|
738
|
+
issues.push({
|
|
739
|
+
level: "warning",
|
|
740
|
+
path: join(path, key),
|
|
741
|
+
message: "Unknown field, which Docent will ignore.",
|
|
742
|
+
...near ? { suggestion: near } : {}
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
/** Choose the branch of a union, using its discriminator when it has one. */
|
|
747
|
+
function pickBranch(value, spec, path, issues) {
|
|
748
|
+
const objects = spec.of.filter((s) => s.kind === "object");
|
|
749
|
+
const others = spec.of.filter((s) => s.kind !== "object");
|
|
750
|
+
if (!isObject(value)) {
|
|
751
|
+
if (others.some((s) => matches(value, s))) return void 0;
|
|
752
|
+
const names = spec.of.map(describe).join(", or ");
|
|
753
|
+
const near = typeof value === "string" ? nearest(value, others.flatMap((s) => s.kind === "enum" ? [...s.values] : [])) : void 0;
|
|
754
|
+
issues.push({
|
|
755
|
+
level: "error",
|
|
756
|
+
path,
|
|
757
|
+
message: `${show(value)} is not valid here. Expected ${names}.`,
|
|
758
|
+
...near ? { suggestion: near } : {}
|
|
759
|
+
});
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (objects.length === 1) return objects[0];
|
|
763
|
+
const by = spec.by;
|
|
764
|
+
if (!by) return objects.find((s) => matches(value, s)) ?? objects[0];
|
|
765
|
+
const tag = value[by];
|
|
766
|
+
const tags = objects.flatMap((s) => {
|
|
767
|
+
const f = s.kind === "object" ? s.fields[by]?.spec : void 0;
|
|
768
|
+
return f?.kind === "enum" ? [...f.values] : [];
|
|
769
|
+
});
|
|
770
|
+
const found = objects.find((s) => {
|
|
771
|
+
const f = s.kind === "object" ? s.fields[by]?.spec : void 0;
|
|
772
|
+
return f?.kind === "enum" && typeof tag === "string" && f.values.includes(tag);
|
|
773
|
+
});
|
|
774
|
+
if (found) return found;
|
|
775
|
+
const near = typeof tag === "string" ? nearest(tag, tags) : void 0;
|
|
776
|
+
issues.push({
|
|
777
|
+
level: "error",
|
|
778
|
+
path: join(path, by),
|
|
779
|
+
message: tag === void 0 ? `Required field is missing. Expected one of: ${tags.join(", ")}.` : `${show(tag)} is not one of: ${tags.join(", ")}.`,
|
|
780
|
+
...near ? { suggestion: near } : {}
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
/** A quick shape test, without collecting issues. */
|
|
784
|
+
function matches(value, spec) {
|
|
785
|
+
const issues = [];
|
|
786
|
+
check(value, spec, "", issues, false);
|
|
787
|
+
return issues.every((i) => i.level !== "error");
|
|
788
|
+
}
|
|
789
|
+
function describe(spec) {
|
|
790
|
+
switch (spec.kind) {
|
|
791
|
+
case "string": return "a string";
|
|
792
|
+
case "number": return "a number";
|
|
793
|
+
case "boolean": return "true or false";
|
|
794
|
+
case "enum": return `one of: ${spec.values.join(", ")}`;
|
|
795
|
+
case "array": return "a list";
|
|
796
|
+
case "object":
|
|
797
|
+
case "record": return "an object";
|
|
798
|
+
default: return "a value";
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
802
|
+
const join = (path, key) => path === "" ? key : `${path}.${key}`;
|
|
803
|
+
function wrongType(path, expected, value) {
|
|
804
|
+
return {
|
|
805
|
+
level: "error",
|
|
806
|
+
path,
|
|
807
|
+
message: `Expected ${expected}, found ${show(value)}.`
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
function show(value) {
|
|
811
|
+
if (typeof value === "string") return `"${value}"`;
|
|
812
|
+
if (value === void 0) return "nothing";
|
|
813
|
+
if (Array.isArray(value)) return "a list";
|
|
814
|
+
if (typeof value === "object" && value !== null) return "an object";
|
|
815
|
+
return String(value);
|
|
816
|
+
}
|
|
817
|
+
/** The closest option, when one is close enough to be worth suggesting. */
|
|
818
|
+
function nearest(value, options) {
|
|
819
|
+
let best;
|
|
820
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
821
|
+
for (const option of options) {
|
|
822
|
+
const d = distance(value.toLowerCase(), option.toLowerCase());
|
|
823
|
+
if (d < bestDistance) {
|
|
824
|
+
bestDistance = d;
|
|
825
|
+
best = option;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
const allowed = Math.max(1, Math.round(value.length / 3));
|
|
829
|
+
return best !== void 0 && bestDistance <= allowed ? best : void 0;
|
|
830
|
+
}
|
|
831
|
+
/** Levenshtein distance, two rows at a time. */
|
|
832
|
+
function distance(a, b) {
|
|
833
|
+
if (a === b) return 0;
|
|
834
|
+
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
835
|
+
for (let i = 1; i <= a.length; i++) {
|
|
836
|
+
const current = [i];
|
|
837
|
+
for (let j = 1; j <= b.length; j++) {
|
|
838
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
839
|
+
current[j] = Math.min((current[j - 1] ?? 0) + 1, (previous[j] ?? 0) + 1, (previous[j - 1] ?? 0) + cost);
|
|
840
|
+
}
|
|
841
|
+
previous = current;
|
|
842
|
+
}
|
|
843
|
+
return previous[b.length] ?? 0;
|
|
844
|
+
}
|
|
845
|
+
//#endregion
|
|
846
|
+
exports.ARROW_STYLES = ARROW_STYLES;
|
|
847
|
+
exports.OVERLAY_STYLES = OVERLAY_STYLES;
|
|
848
|
+
exports.PLACEMENTS = PLACEMENTS;
|
|
849
|
+
exports.SCHEMA_ID = SCHEMA_ID;
|
|
850
|
+
exports.SPOTLIGHT_RINGS = SPOTLIGHT_RINGS;
|
|
851
|
+
exports.SPOTLIGHT_SHAPES = SPOTLIGHT_SHAPES;
|
|
852
|
+
exports.THEME_NAMES = THEME_NAMES;
|
|
853
|
+
exports.TOUR_STATES = TOUR_STATES;
|
|
854
|
+
exports.TRAIT_OPERATORS = TRAIT_OPERATORS;
|
|
855
|
+
exports.formatIssues = formatIssues;
|
|
856
|
+
exports.isValidTour = isValidTour;
|
|
857
|
+
exports.nearest = nearest;
|
|
858
|
+
exports.tourJsonSchema = tourJsonSchema;
|
|
859
|
+
exports.validateTour = validateTour;
|
|
860
|
+
|
|
861
|
+
//# sourceMappingURL=validate.cjs.map
|