paperlint 14.4.0 → 14.6.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/skills/build-label-page/scripts/label-page-core.d.ts +435 -1
- package/package.json +1 -1
- package/skills/build-label-page/SKILL.md +21 -1
- package/skills/build-label-page/SKILL.md.spec.ts +20 -0
- package/skills/build-label-page/references/closed-choice-generator.md +104 -0
- package/skills/build-label-page/scripts/fixtures/v7-cards-ru.json +23 -0
- package/skills/build-label-page/scripts/label-page-browser.test.ts +344 -1
- package/skills/build-label-page/scripts/label-page-core.js +217 -17
- package/skills/build-label-page/scripts/label-page-core.test.ts +68 -0
- package/skills/build-label-page/scripts/label-page-core.ts +312 -17
- package/skills/build-label-page/scripts/label-page-sources.test.ts +231 -0
- package/skills/build-label-page/scripts/label-page-translations.test.ts +421 -0
- package/skills/build-label-page/scripts/page-template.html +91 -29
|
@@ -222,7 +222,12 @@ const CARD_FIELDS = {
|
|
|
222
222
|
}, true, false),
|
|
223
223
|
[READINGS_FIELD]: obj({ a: string(), b: string() }, true, false),
|
|
224
224
|
file: string(false),
|
|
225
|
-
situation: optional({
|
|
225
|
+
situation: optional({
|
|
226
|
+
summary: string(false),
|
|
227
|
+
question: string(),
|
|
228
|
+
// the source tag over the summary (#400); the question is the page's own and gets none
|
|
229
|
+
from: string(false),
|
|
230
|
+
}),
|
|
226
231
|
excerpt: optional({
|
|
227
232
|
file: string(false),
|
|
228
233
|
// the file's language, after its path: "path · Python"
|
|
@@ -242,8 +247,15 @@ const CARD_FIELDS = {
|
|
|
242
247
|
annotation: string(false),
|
|
243
248
|
caption: string(false),
|
|
244
249
|
mark_label: string(false),
|
|
250
|
+
// source tags (#400): over the code box, and over the caption under it
|
|
251
|
+
from: string(false),
|
|
252
|
+
caption_from: string(false),
|
|
253
|
+
}),
|
|
254
|
+
quote: optional({
|
|
255
|
+
text: string(),
|
|
256
|
+
source: string(false),
|
|
257
|
+
from: string(false),
|
|
245
258
|
}),
|
|
246
|
-
quote: optional({ text: string(), source: string(false) }),
|
|
247
259
|
terms: array(obj({
|
|
248
260
|
term: string(),
|
|
249
261
|
note: array(obj({
|
|
@@ -347,6 +359,36 @@ const QUESTION_SCHEMA = obj({
|
|
|
347
359
|
short: string(false),
|
|
348
360
|
options: array(OPTION_SCHEMA, true, 2),
|
|
349
361
|
});
|
|
362
|
+
// Page-level words the blocks use (references/closed-choice-generator.md §«Content blocks»).
|
|
363
|
+
const LABELS = optional({
|
|
364
|
+
place: string(false),
|
|
365
|
+
body: string(false),
|
|
366
|
+
line: string(false),
|
|
367
|
+
line_part: string(false),
|
|
368
|
+
repeat: string(false),
|
|
369
|
+
practice_example: string(false),
|
|
370
|
+
hidden: string(false),
|
|
371
|
+
hidden_one: string(false),
|
|
372
|
+
wait: string(false),
|
|
373
|
+
how_to: string(false),
|
|
374
|
+
for_example: string(false),
|
|
375
|
+
});
|
|
376
|
+
// The page's own words in another language (#394): never the quote, code, notes or any card text
|
|
377
|
+
// the labeller judges. Keyed like the page, so a translation names its questions and answers by id.
|
|
378
|
+
const TRANSLATION = obj({
|
|
379
|
+
title: string(false),
|
|
380
|
+
intro: string(false),
|
|
381
|
+
labels: LABELS,
|
|
382
|
+
// the first question as the cards with a `situation` ask it; `{n}` is the marked line's number
|
|
383
|
+
situation_question: string(false),
|
|
384
|
+
// the labels of the page's `sources`, by id; one left out reads as in the page's own language
|
|
385
|
+
sources: mapping(string(), NAME_PATTERN, 1, false),
|
|
386
|
+
questions: mapping(obj({
|
|
387
|
+
text: string(),
|
|
388
|
+
short: string(false),
|
|
389
|
+
options: mapping(obj({ label: string(), example: string(), ask_why: string(false) }), NAME_PATTERN, 1),
|
|
390
|
+
}), NAME_PATTERN, 1),
|
|
391
|
+
});
|
|
350
392
|
const configSchema = (kind) => obj({
|
|
351
393
|
study: string(true, NAME_PATTERN), // becomes a file name
|
|
352
394
|
kind: string(true, Object.keys(KINDS).join("|"), `one of ${KIND_NAMES}`),
|
|
@@ -366,18 +408,12 @@ const configSchema = (kind) => obj({
|
|
|
366
408
|
questions: array(QUESTION_SCHEMA, true, 1),
|
|
367
409
|
practice: array(practiceSchema(kind), true, MIN_PRACTICE),
|
|
368
410
|
practice_round: integer(false),
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
repeat: string(false),
|
|
376
|
-
practice_example: string(false),
|
|
377
|
-
hidden: string(false),
|
|
378
|
-
hidden_one: string(false),
|
|
379
|
-
wait: string(false),
|
|
380
|
-
}),
|
|
411
|
+
labels: LABELS,
|
|
412
|
+
// the source tags a card's blocks carry (#400): id → the tag's label, in colour order
|
|
413
|
+
sources: mapping(string(), NAME_PATTERN, 1, false),
|
|
414
|
+
// the language of the page's own words, and the same words in more languages, with a switch
|
|
415
|
+
lang: string(false, NAME_PATTERN),
|
|
416
|
+
translations: mapping(TRANSLATION, NAME_PATTERN, 1, false),
|
|
381
417
|
items: array(itemSchema(kind), true, 1),
|
|
382
418
|
});
|
|
383
419
|
/** The declared schema (`schema` prints it): every field, those a kind requires left optional. */
|
|
@@ -654,6 +690,57 @@ function checkBlocks(cfg) {
|
|
|
654
690
|
fail(`${at}.repeat.index: ${String(r.index)} is not between 1 and ${String(r.of)}`);
|
|
655
691
|
});
|
|
656
692
|
}
|
|
693
|
+
// ── source tags (#400) ────────────────────────────────────────────────────────────────────────
|
|
694
|
+
/** The colours the page has for tags (`.tag.s0` to `.tag.s4` in page-template.html), in `sources` order. */
|
|
695
|
+
export const SOURCE_COLOURS = 5;
|
|
696
|
+
/** Each tag a card asks for: the path of the field that names it, the id it names, the text it sits over. */
|
|
697
|
+
const tagsOf = (at, card) => [
|
|
698
|
+
...(card.quote?.from === undefined
|
|
699
|
+
? []
|
|
700
|
+
: [{ at: `${at}.quote.from`, id: card.quote.from, over: "text" }]),
|
|
701
|
+
...(card.situation?.from === undefined
|
|
702
|
+
? []
|
|
703
|
+
: [
|
|
704
|
+
{
|
|
705
|
+
at: `${at}.situation.from`,
|
|
706
|
+
id: card.situation.from,
|
|
707
|
+
over: card.situation.summary,
|
|
708
|
+
},
|
|
709
|
+
]),
|
|
710
|
+
...(card.excerpt?.from === undefined
|
|
711
|
+
? []
|
|
712
|
+
: [{ at: `${at}.excerpt.from`, id: card.excerpt.from, over: "box" }]),
|
|
713
|
+
...(card.excerpt?.caption_from === undefined
|
|
714
|
+
? []
|
|
715
|
+
: [
|
|
716
|
+
{
|
|
717
|
+
at: `${at}.excerpt.caption_from`,
|
|
718
|
+
id: card.excerpt.caption_from,
|
|
719
|
+
over: card.excerpt.caption,
|
|
720
|
+
},
|
|
721
|
+
]),
|
|
722
|
+
];
|
|
723
|
+
/**
|
|
724
|
+
* What the schema cannot say about source tags: a tag names an id of the page's `sources`, over a
|
|
725
|
+
* text the card has, the page has colours for its ids, and an id nothing uses is a dead entry.
|
|
726
|
+
*/
|
|
727
|
+
function checkSources(cfg) {
|
|
728
|
+
const tags = cardsOf(cfg).flatMap(([at, card]) => tagsOf(at, card));
|
|
729
|
+
const ids = Object.keys(cfg.sources ?? {});
|
|
730
|
+
if (ids.length > SOURCE_COLOURS)
|
|
731
|
+
fail(`sources: ${String(ids.length)} ids, and the page has ${String(SOURCE_COLOURS)} colours for tags — merge ids, or drop one`);
|
|
732
|
+
tags.forEach((t) => {
|
|
733
|
+
if (cfg.sources === undefined)
|
|
734
|
+
fail(`${t.at}: names the source ${repr(t.id)}, and the page has no \`sources\` — add them, or take the tag out`);
|
|
735
|
+
else if (!has(cfg.sources, t.id))
|
|
736
|
+
fail(`${t.at}: ${repr(t.id)} is not in \`sources\` (${ids.join(", ")})`);
|
|
737
|
+
if (t.over === undefined)
|
|
738
|
+
fail(`${t.at}: tags a text the card does not have — add it, or take the tag out`);
|
|
739
|
+
});
|
|
740
|
+
const dead = ids.find((id) => !tags.some((t) => t.id === id));
|
|
741
|
+
if (dead !== undefined)
|
|
742
|
+
fail(`sources.${dead}: no item or practice item names it — a tag nothing carries is a dead entry`);
|
|
743
|
+
}
|
|
657
744
|
/**
|
|
658
745
|
* The texts a term may open a note from, the words asked about first: the quote, its source, then
|
|
659
746
|
* the excerpt's annotation. The page looks a term up in the same texts, split the same way at
|
|
@@ -765,6 +852,104 @@ function checkNoSourceShown(cfg) {
|
|
|
765
852
|
}
|
|
766
853
|
}
|
|
767
854
|
}
|
|
855
|
+
// ── a second language for the page's own words (#394) ─────────────────────────────────────────
|
|
856
|
+
/** The language of the page's own words when items.json does not name it. */
|
|
857
|
+
const BASE_LANG = "en";
|
|
858
|
+
/** In a translated `situation_question`, the number of the card's marked line. */
|
|
859
|
+
const NUMBER_TOKEN = "{n}";
|
|
860
|
+
/** A translated text is given exactly when the page shows the text it translates. */
|
|
861
|
+
function checkPresence(where, shown, given) {
|
|
862
|
+
if (shown !== undefined && given === undefined)
|
|
863
|
+
fail(`${where}: is required and missing — ${shown}`);
|
|
864
|
+
if (shown === undefined && given !== undefined)
|
|
865
|
+
fail(`${where}: is never shown — the page has no text it translates`);
|
|
866
|
+
}
|
|
867
|
+
/** The cards' first question, translated: given exactly when a card has a `situation`. */
|
|
868
|
+
function checkSituationWords(cfg, at, t) {
|
|
869
|
+
const cards = cardsOf(cfg).filter(([, c]) => c.situation !== undefined);
|
|
870
|
+
const first = cards[0];
|
|
871
|
+
checkPresence(`${at}.situation_question`, first &&
|
|
872
|
+
`${first[0]}.situation asks the first question in the card's own words`, t.situation_question);
|
|
873
|
+
const q = t.situation_question ?? "";
|
|
874
|
+
const bare = cards.find(([, c]) => lineOf(c) === undefined);
|
|
875
|
+
if (bare !== undefined &&
|
|
876
|
+
[NUMBER_TOKEN, LINE_TOKEN].some((token) => q.includes(token)))
|
|
877
|
+
fail(`${at}.situation_question: names the marked line, and ${bare[0]} has no \`excerpt\` — add it, or write the question without \`${NUMBER_TOKEN}\` and \`${LINE_TOKEN}\``);
|
|
878
|
+
}
|
|
879
|
+
/** One answer in the translation's words; refused when the translation leaves it out. */
|
|
880
|
+
const optionIn = (o, words, where) => {
|
|
881
|
+
const w = words.options[o.value] ??
|
|
882
|
+
fail(`${where}.options.${o.value}: is required and missing — every answer is translated`);
|
|
883
|
+
checkPresence(`${where}.options.${o.value}.ask_why`, o.ask_why && "this answer asks why", w.ask_why);
|
|
884
|
+
return {
|
|
885
|
+
...R.omit(o, ["ask_why"]),
|
|
886
|
+
label: w.label,
|
|
887
|
+
example: w.example,
|
|
888
|
+
...R.pickBy({ ask_why: w.ask_why }, R.isDefined),
|
|
889
|
+
};
|
|
890
|
+
};
|
|
891
|
+
/** One question in the translation's words: every answer of it, no answer it does not have. */
|
|
892
|
+
function questionIn(q, t, at) {
|
|
893
|
+
const where = `${at}.questions.${q.id}`;
|
|
894
|
+
const words = t.questions[q.id] ??
|
|
895
|
+
fail(`${where}: is required and missing — every question of the page is translated`);
|
|
896
|
+
const values = q.options.map((o) => o.value);
|
|
897
|
+
const stray = Object.keys(words.options).find((v) => !values.includes(v));
|
|
898
|
+
if (stray !== undefined)
|
|
899
|
+
fail(`${where}.options.${stray}: not one of ${values.join(", ")}`);
|
|
900
|
+
const options = q.options.map((o) => optionIn(o, words, where));
|
|
901
|
+
const twice = firstRepeat(options.map((o) => o.label));
|
|
902
|
+
if (twice !== undefined)
|
|
903
|
+
fail(`${where}.options.${String(values[twice.at])}.label: ${repr(options[twice.at]?.label)} reads the same as options.${String(values[twice.earlier])}.label — each button needs its own label`);
|
|
904
|
+
return {
|
|
905
|
+
id: q.id,
|
|
906
|
+
text: words.text,
|
|
907
|
+
...R.pickBy({ short: words.short }, R.isDefined),
|
|
908
|
+
options,
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
/** A translated tag label names an id of the page's `sources`. */
|
|
912
|
+
function checkTranslatedSources(cfg, at, t) {
|
|
913
|
+
const known = Object.keys(cfg.sources ?? {});
|
|
914
|
+
const stray = Object.keys(t.sources ?? {}).find((id) => !known.includes(id));
|
|
915
|
+
if (stray !== undefined)
|
|
916
|
+
fail(`${at}.sources.${stray}: not one of the page's \`sources\` (${known.join(", ") || "none"})`);
|
|
917
|
+
}
|
|
918
|
+
/** The translated words a translation may leave out. */
|
|
919
|
+
const optionalWords = (t) => R.pickBy({ situation_question: t.situation_question, sources: t.sources }, R.isDefined);
|
|
920
|
+
/**
|
|
921
|
+
* Every language of the page with its words whole, the translations checked: each covers every
|
|
922
|
+
* question and answer of the page and names none it does not have, and gives an intro, a reason
|
|
923
|
+
* prompt and a situation question exactly where the page shows one. A label it leaves out reads as
|
|
924
|
+
* in the page's own language.
|
|
925
|
+
*/
|
|
926
|
+
function translationsOf(cfg) {
|
|
927
|
+
const base = cfg.lang ?? BASE_LANG;
|
|
928
|
+
return R.mapValues(cfg.translations ?? {}, (t, lang) => {
|
|
929
|
+
const at = `translations.${lang}`;
|
|
930
|
+
if (lang === base)
|
|
931
|
+
fail(`${at}: is the page's own language (\`lang\` is ${repr(base)}) — translate into another one`);
|
|
932
|
+
checkPresence(`${at}.intro`, cfg.intro && "the page has an `intro`", t.intro);
|
|
933
|
+
checkSituationWords(cfg, at, t);
|
|
934
|
+
checkTranslatedSources(cfg, at, t);
|
|
935
|
+
const ids = cfg.questions.map((q) => q.id);
|
|
936
|
+
const stray = Object.keys(t.questions).find((id) => !ids.includes(id));
|
|
937
|
+
if (stray !== undefined)
|
|
938
|
+
fail(`${at}.questions.${stray}: not a question of this page (${ids.join(", ")})`);
|
|
939
|
+
return {
|
|
940
|
+
title: t.title ?? cfg.title,
|
|
941
|
+
intro: t.intro ?? "",
|
|
942
|
+
labels: { ...DEFAULT_LABELS, ...cfg.labels, ...t.labels },
|
|
943
|
+
...optionalWords(t),
|
|
944
|
+
questions: cfg.questions.map((q) => questionIn(q, t, at)),
|
|
945
|
+
};
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
/** The languages: the page's own when items.json names it, and with translations always (English when unnamed).
|
|
949
|
+
* A page that names neither is as it was. */
|
|
950
|
+
const languagesOf = (cfg, translated) => cfg.translations === undefined
|
|
951
|
+
? R.pickBy({ lang: cfg.lang }, R.isDefined)
|
|
952
|
+
: { lang: cfg.lang ?? BASE_LANG, translations: translated };
|
|
768
953
|
/** key.json is this script's own output, written by `build`; its shape is trusted, not re-checked. */
|
|
769
954
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- an assertion signature, no body to run
|
|
770
955
|
function assertOwnKey(_value) { }
|
|
@@ -805,7 +990,7 @@ const situationShown = (item) => item.situation === undefined
|
|
|
805
990
|
? {}
|
|
806
991
|
: {
|
|
807
992
|
situation: {
|
|
808
|
-
...R.pickBy({ summary: item.situation.summary }, R.isDefined),
|
|
993
|
+
...R.pickBy({ summary: item.situation.summary, from: item.situation.from }, R.isDefined),
|
|
809
994
|
question: item.situation.question,
|
|
810
995
|
...(usesLine(item) ? { line: lineOf(item) } : {}),
|
|
811
996
|
},
|
|
@@ -882,16 +1067,28 @@ const DEFAULT_LABELS = {
|
|
|
882
1067
|
hidden: "lines {from}\u2013{to}",
|
|
883
1068
|
hidden_one: "line {from}",
|
|
884
1069
|
wait: "\u23f3 You can answer in {s} s",
|
|
1070
|
+
how_to: "📖 How to answer",
|
|
1071
|
+
for_example: "For example: ",
|
|
885
1072
|
};
|
|
1073
|
+
/** The tags' ids and labels in colour order, only when items.json has `sources`: a page without them is as it was. */
|
|
1074
|
+
const sourcesShown = (cfg) => cfg.sources === undefined
|
|
1075
|
+
? {}
|
|
1076
|
+
: {
|
|
1077
|
+
sources: Object.entries(cfg.sources).map(([id, label]) => ({
|
|
1078
|
+
id,
|
|
1079
|
+
label,
|
|
1080
|
+
})),
|
|
1081
|
+
};
|
|
886
1082
|
/** The answerers and the dwell, only when items.json sets them: a page without them is as it was. */
|
|
887
1083
|
const twoKeys = (cfg) => R.pickBy({ answerers: cfg.answerers, min_seconds: cfg.min_seconds }, R.isDefined);
|
|
888
1084
|
/** What a page shows: the questions, the practice and the items, blind, with their neutral ids. */
|
|
889
|
-
function payloadOf(cfg, page, described) {
|
|
1085
|
+
function payloadOf(cfg, page, described, translated) {
|
|
890
1086
|
const used = Object.keys(cfg.groups).filter((g) => page.items.some((i) => i.group === g));
|
|
891
1087
|
return {
|
|
892
1088
|
title: cfg.title,
|
|
893
1089
|
intro: cfg.intro ?? "",
|
|
894
1090
|
labels: { ...DEFAULT_LABELS, ...cfg.labels },
|
|
1091
|
+
...sourcesShown(cfg),
|
|
895
1092
|
groups: used.map((g) => ({ id: g, label: String(cfg.groups[g]) })),
|
|
896
1093
|
familiarity_question: asksFamiliarity(cfg)
|
|
897
1094
|
? (cfg.familiarity_question ?? "How well do you know {group}?")
|
|
@@ -899,6 +1096,7 @@ function payloadOf(cfg, page, described) {
|
|
|
899
1096
|
familiarity: asksFamiliarity(cfg) ? FAMILIARITY : [],
|
|
900
1097
|
allowed: allowedValues(cfg.questions, asksFamiliarity(cfg)),
|
|
901
1098
|
...twoKeys(cfg),
|
|
1099
|
+
...languagesOf(cfg, translated),
|
|
902
1100
|
questions: cfg.questions,
|
|
903
1101
|
practice: {
|
|
904
1102
|
round: practiceRound(cfg),
|
|
@@ -982,7 +1180,9 @@ export function prepareBuild(configPath, outDir) {
|
|
|
982
1180
|
checkUnits(cfg);
|
|
983
1181
|
checkBlocks(cfg);
|
|
984
1182
|
checkCardText(cfg);
|
|
1183
|
+
checkSources(cfg);
|
|
985
1184
|
checkNoSourceShown(cfg);
|
|
1185
|
+
const translated = translationsOf(cfg);
|
|
986
1186
|
const described = describeRules(cfg, dirname(configPath));
|
|
987
1187
|
mkdirSync(outDir, { recursive: true });
|
|
988
1188
|
const keyPath = join(outDir, "key.json");
|
|
@@ -990,7 +1190,7 @@ export function prepareBuild(configPath, outDir) {
|
|
|
990
1190
|
const page = itemsOnPage(cfg, keyPath);
|
|
991
1191
|
writeJson(keyPath, keyOf(cfg, page, earlier));
|
|
992
1192
|
return {
|
|
993
|
-
payload: payloadOf(cfg, page, described),
|
|
1193
|
+
payload: payloadOf(cfg, page, described, translated),
|
|
994
1194
|
dropped: cfg.items.length - page.items.length,
|
|
995
1195
|
};
|
|
996
1196
|
}
|
|
@@ -139,6 +139,42 @@ const CARD = {
|
|
|
139
139
|
},
|
|
140
140
|
repeat: { group: "g", index: 1, of: 1 },
|
|
141
141
|
};
|
|
142
|
+
/** The translation of FULL's page, with every optional field present. */
|
|
143
|
+
const RU_FULL = {
|
|
144
|
+
title: "Т",
|
|
145
|
+
intro: "Разметьте.",
|
|
146
|
+
labels: {
|
|
147
|
+
place: "К",
|
|
148
|
+
body: "Т",
|
|
149
|
+
line: "С",
|
|
150
|
+
line_part: "{line} {part}/{of}",
|
|
151
|
+
repeat: "П",
|
|
152
|
+
practice_example: "П",
|
|
153
|
+
hidden: "С {from}-{to}",
|
|
154
|
+
hidden_one: "С {from}",
|
|
155
|
+
wait: "Ж {s}",
|
|
156
|
+
how_to: "Как",
|
|
157
|
+
for_example: "Н: ",
|
|
158
|
+
},
|
|
159
|
+
situation_question: "В?",
|
|
160
|
+
questions: {
|
|
161
|
+
label: {
|
|
162
|
+
text: "Верно?",
|
|
163
|
+
short: "верно?",
|
|
164
|
+
options: {
|
|
165
|
+
yes: { label: "Да", example: "1, 2, 3" },
|
|
166
|
+
no: { label: "Нет", example: "3, 1, 2", ask_why: "Что не так?" },
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
boundary: {
|
|
170
|
+
text: "Граница?",
|
|
171
|
+
options: {
|
|
172
|
+
clear: { label: "Ясно", example: "[]" },
|
|
173
|
+
boundary: { label: "Граница", example: "равные ключи" },
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
};
|
|
142
178
|
/** A config with EVERY optional field present, so every schema path has a value to spoil. */
|
|
143
179
|
const FULL: DocObject = (() => {
|
|
144
180
|
const doc = Doc.parse(
|
|
@@ -169,7 +205,11 @@ const FULL: DocObject = (() => {
|
|
|
169
205
|
hidden: "H {from}-{to}",
|
|
170
206
|
hidden_one: "H {from}",
|
|
171
207
|
wait: "W {s}",
|
|
208
|
+
how_to: "How",
|
|
209
|
+
for_example: "E: ",
|
|
172
210
|
},
|
|
211
|
+
lang: "en",
|
|
212
|
+
translations: { ru: RU_FULL },
|
|
173
213
|
practice: PRACTICE.map((p, n) => (n === 0 ? { ...p, ...CARD } : p)),
|
|
174
214
|
items: ITEMS.map((i, n) =>
|
|
175
215
|
n === 0 ? { ...i, ...CARD, answerers: ["author"] } : i,
|
|
@@ -304,6 +344,15 @@ function badCases({ node, member }: Leaf): readonly BadCase[] {
|
|
|
304
344
|
return [...wrongType, ...extra];
|
|
305
345
|
}
|
|
306
346
|
|
|
347
|
+
/** `doc` without the value at `steps`. */
|
|
348
|
+
function without(
|
|
349
|
+
doc: DocObject,
|
|
350
|
+
steps: readonly (string | number)[],
|
|
351
|
+
): DocObject {
|
|
352
|
+
const out = put(doc, steps, undefined);
|
|
353
|
+
assert.ok(isObject(out));
|
|
354
|
+
return out;
|
|
355
|
+
}
|
|
307
356
|
/** `doc` with its first item's own answerers taken out. */
|
|
308
357
|
function withoutItemAnswerers(doc: DocObject): DocObject {
|
|
309
358
|
const out = put(doc, ["items", 0, "answerers"], undefined);
|
|
@@ -317,6 +366,25 @@ function withoutItemAnswerers(doc: DocObject): DocObject {
|
|
|
317
366
|
const RELAX: Partial<Record<string, (doc: DocObject) => DocObject>> = {
|
|
318
367
|
familiarity: (doc) => R.omit(doc, ["familiarity_question"]),
|
|
319
368
|
answerers: withoutItemAnswerers,
|
|
369
|
+
// A translation gives an intro, a reason prompt and a card question exactly where the page has one.
|
|
370
|
+
intro: (doc) => without(doc, ["translations", "ru", "intro"]),
|
|
371
|
+
"translations.ru.intro": (doc) => R.omit(doc, ["intro"]),
|
|
372
|
+
"questions[0].options[1].ask_why": (doc) =>
|
|
373
|
+
without(doc, [
|
|
374
|
+
"translations",
|
|
375
|
+
"ru",
|
|
376
|
+
"questions",
|
|
377
|
+
"label",
|
|
378
|
+
"options",
|
|
379
|
+
"no",
|
|
380
|
+
"ask_why",
|
|
381
|
+
]),
|
|
382
|
+
"translations.ru.situation_question": (doc) =>
|
|
383
|
+
without(without(doc, ["items", 0, "situation"]), [
|
|
384
|
+
"practice",
|
|
385
|
+
0,
|
|
386
|
+
"situation",
|
|
387
|
+
]),
|
|
320
388
|
};
|
|
321
389
|
|
|
322
390
|
/** Spoil `leaf` each way in `badCases`; every one must stop the build and name the path. */
|