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
|
@@ -133,6 +133,14 @@ button.term code { background: transparent; border: 0; padding: 0; }
|
|
|
133
133
|
#tip .tip-arrow { position: absolute; width: 10px; height: 10px; background: var(--surface); transform: rotate(45deg); }
|
|
134
134
|
#tip.below .tip-arrow { top: -6px; border-left: 1px solid var(--accent); border-top: 1px solid var(--accent); }
|
|
135
135
|
#tip.above .tip-arrow { bottom: -6px; border-right: 1px solid var(--accent); border-bottom: 1px solid var(--accent); }
|
|
136
|
+
/* A source tag (#400): a small pill right above the block it names. The colour is the id's place in the
|
|
137
|
+
page's `sources`, one of five tokens of :root; text and edge are the colour, the fill a faint wash of it. */
|
|
138
|
+
.tagged { display: flex; flex-direction: column; gap: 6px; }
|
|
139
|
+
.tag { align-self: flex-start; font-size: 0.7rem; line-height: 1.2; letter-spacing: 0.08em; text-transform: uppercase;
|
|
140
|
+
font-weight: 500; padding: 3px 9px; border-radius: 999px; color: var(--tag); border: 1px solid var(--tag);
|
|
141
|
+
background: color-mix(in srgb, var(--tag) 14%, var(--bg)); }
|
|
142
|
+
.tag.s0 { --tag: var(--accent); } .tag.s1 { --tag: var(--good); } .tag.s2 { --tag: var(--mark); }
|
|
143
|
+
.tag.s3 { --tag: var(--bad); } .tag.s4 { --tag: var(--text); }
|
|
136
144
|
.rulebox { display: flex; flex-direction: column; gap: 8px; }
|
|
137
145
|
.clamped { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
|
138
146
|
/* The clamp cuts at the padding box, so a code block's bottom padding is a border: no fourth line peeks in. */
|
|
@@ -143,6 +151,9 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
|
|
|
143
151
|
.match.no { color: var(--bad); }
|
|
144
152
|
.who { display: flex; align-items: center; justify-content: space-between; gap: 12px; color: var(--muted); }
|
|
145
153
|
.who button { width: auto; min-height: 44px; border: 0; color: var(--accent); padding: 0 4px; }
|
|
154
|
+
/* The language switch: small buttons side by side under the title, the picked one pressed. */
|
|
155
|
+
.langs { display: flex; gap: 8px; }
|
|
156
|
+
.langs button { width: auto; min-width: 56px; min-height: 44px; text-align: center; }
|
|
146
157
|
</style>
|
|
147
158
|
<main id="app"><p class="muted">Loading…</p></main>
|
|
148
159
|
<script type="application/json" id="data">__DATA__</script>
|
|
@@ -182,6 +193,34 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
|
|
|
182
193
|
var WHY_PAUSE_MS = 800;
|
|
183
194
|
|
|
184
195
|
try { if (localStorage.getItem("label-filter") === "next") state.filter = "next"; } catch (e) {}
|
|
196
|
+
var BASE = { title: DATA.title, intro: DATA.intro, labels: DATA.labels, questions: DATA.questions };
|
|
197
|
+
// ── the page's own words, in the language picked (DATA.translations, #394) ─────────────────
|
|
198
|
+
// Only the page's words change: the quote, the code and every note on a card stay as built.
|
|
199
|
+
var LANGS = DATA.translations ? [DATA.lang].concat(Object.keys(DATA.translations)) : [];
|
|
200
|
+
state.lang = DATA.lang || "en";
|
|
201
|
+
try {
|
|
202
|
+
var said = localStorage.getItem("label-lang");
|
|
203
|
+
if (LANGS.indexOf(said) !== -1) state.lang = said;
|
|
204
|
+
} catch (e) {}
|
|
205
|
+
function words() { return state.lang === DATA.lang || !DATA.translations ? BASE : DATA.translations[state.lang]; }
|
|
206
|
+
function Qs() { return words().questions; }
|
|
207
|
+
function pickLang(lang) {
|
|
208
|
+
state.lang = lang;
|
|
209
|
+
try { localStorage.setItem("label-lang", lang); } catch (e) {}
|
|
210
|
+
render();
|
|
211
|
+
}
|
|
212
|
+
// One button per language, its tag on it ("EN", "RU"); the one picked is pressed.
|
|
213
|
+
function langSwitch() {
|
|
214
|
+
var row = mark(el("div", "langs"), "lang-switch", { lang: state.lang, langs: LANGS });
|
|
215
|
+
LANGS.forEach(function (lang) {
|
|
216
|
+
var b = mark(el("button", "", lang.toUpperCase()), "lang-option", { lang: lang });
|
|
217
|
+
b.type = "button";
|
|
218
|
+
b.setAttribute("aria-pressed", String(state.lang === lang));
|
|
219
|
+
b.addEventListener("click", function () { pickLang(lang); });
|
|
220
|
+
row.appendChild(b);
|
|
221
|
+
});
|
|
222
|
+
return row;
|
|
223
|
+
}
|
|
185
224
|
// ── who is answering (DATA.answerers): each answerer's documents are "<answerer>~<id>" ─────
|
|
186
225
|
// "~" is outside the build's name pattern, so no answerer, item, group or practice id holds it.
|
|
187
226
|
var SEP = "~";
|
|
@@ -240,7 +279,7 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
|
|
|
240
279
|
function unsaved(id) { return !state.answers[id] && state.firstSeconds[id] === undefined; }
|
|
241
280
|
function locked(id) { return MIN_SECONDS > 0 && unsaved(id) && dwellMs(id) < MIN_SECONDS * 1000; }
|
|
242
281
|
function waitSeconds(id) { return Math.ceil((MIN_SECONDS * 1000 - dwellMs(id)) / 1000); }
|
|
243
|
-
function waitText(id) { return fill(
|
|
282
|
+
function waitText(id) { return fill(words().labels.wait, { s: waitSeconds(id) }); }
|
|
244
283
|
// The seconds stored with an item: those of its first save, kept on every later write.
|
|
245
284
|
function secondsOf(id) {
|
|
246
285
|
var d = state.answers[id];
|
|
@@ -341,11 +380,11 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
|
|
|
341
380
|
return d[qid];
|
|
342
381
|
}
|
|
343
382
|
function needsWhy(q, v) { return (DATA.allowed.needs_why[q.id] || []).indexOf(v) !== -1; }
|
|
344
|
-
function questionOf(qid) { return
|
|
383
|
+
function questionOf(qid) { return Qs().filter(function (q) { return q.id === qid; })[0]; }
|
|
345
384
|
// What an item still needs: an answer, or the reason its answer asks for (named "<short> (why)").
|
|
346
385
|
function missingFor(id) {
|
|
347
386
|
var a = answersOf(id), r = reasonsOf(id);
|
|
348
|
-
return
|
|
387
|
+
return Qs().map(function (q) {
|
|
349
388
|
if (!valid(DATA.allowed.questions[q.id], a[q.id])) return q;
|
|
350
389
|
if (needsWhy(q, a[q.id]) && !(r[q.id] || "").trim()) {
|
|
351
390
|
return { id: q.id, text: q.text + " (why)", short: (q.short || q.text) + " (why)" };
|
|
@@ -373,12 +412,12 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
|
|
|
373
412
|
}
|
|
374
413
|
function practiceMissing(pid) {
|
|
375
414
|
var a = practiceAnswers(pid);
|
|
376
|
-
return
|
|
415
|
+
return Qs().filter(function (q) { return !valid(DATA.allowed.questions[q.id], a[q.id]); });
|
|
377
416
|
}
|
|
378
417
|
function practiceAnswered(p) { return practiceMissing(p.id).length === 0; }
|
|
379
418
|
function practiceMatched(p) {
|
|
380
419
|
var a = practiceAnswers(p.id);
|
|
381
|
-
return
|
|
420
|
+
return Qs().every(function (q) { return a[q.id] === p.expected[q.id]; });
|
|
382
421
|
}
|
|
383
422
|
function practiceOpen() { return !DATA.practice.items.every(practiceAnswered); }
|
|
384
423
|
function groupDone(g) { return familiarity(g) !== undefined && groupItems(g).every(function (i) { return itemDone(i.id); }); }
|
|
@@ -506,6 +545,18 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
|
|
|
506
545
|
n.setAttribute("data-test-meta", JSON.stringify(meta || {}));
|
|
507
546
|
return n;
|
|
508
547
|
}
|
|
548
|
+
// A block under its source tag (#400): the block itself when the page has no `sources` or the block names none.
|
|
549
|
+
// The tag shows the label in the language picked, else the page's own; its colour is the id's place in `sources`.
|
|
550
|
+
function tagged(block, id, kind) {
|
|
551
|
+
if (!id || !DATA.sources) return block;
|
|
552
|
+
var at = DATA.sources.map(function (s) { return s.id; }).indexOf(id);
|
|
553
|
+
var own = state.lang !== DATA.lang && DATA.translations ? DATA.translations[state.lang].sources : null;
|
|
554
|
+
var tag = mark(el("span", "tag s" + at, (own && own[id]) || DATA.sources[at].label), "source-tag", { source: id, block: kind });
|
|
555
|
+
var wrap = el("div", "tagged");
|
|
556
|
+
wrap.appendChild(tag);
|
|
557
|
+
wrap.appendChild(block);
|
|
558
|
+
return wrap;
|
|
559
|
+
}
|
|
509
560
|
// Text with `backticks` shown as code spans; everything stays textContent, so nothing is parsed as HTML.
|
|
510
561
|
function rich(tag, cls, text) {
|
|
511
562
|
var n = el(tag, cls);
|
|
@@ -540,7 +591,7 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
|
|
|
540
591
|
if (o.icon) b.appendChild(el("span", "icon", o.icon));
|
|
541
592
|
b.appendChild(el("span", "label", o.label));
|
|
542
593
|
if (notes && notes[o.value]) b.appendChild(mark(rich("span", "sub", notes[o.value]), "answer-sub", { question: qid, value: o.value }));
|
|
543
|
-
if (withExamples && o.example) b.appendChild(rich("span", "ex",
|
|
594
|
+
if (withExamples && o.example) b.appendChild(rich("span", "ex", words().labels.practice_example + o.example));
|
|
544
595
|
b.type = "button";
|
|
545
596
|
b.setAttribute("aria-pressed", String(current === o.value));
|
|
546
597
|
b.disabled = disabled;
|
|
@@ -589,7 +640,7 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
|
|
|
589
640
|
var shown = itemsShown();
|
|
590
641
|
if (MIN_SECONDS && unsaved(item.id)) card.setAttribute("data-id", item.id);
|
|
591
642
|
mark(card, "item", { id: item.id, index: shown.indexOf(item) + 1, total: shown.length });
|
|
592
|
-
card.appendChild(el("div", "id", fill(
|
|
643
|
+
card.appendChild(el("div", "id", fill(words().labels.place, { id: item.id, n: shown.indexOf(item) + 1, total: shown.length })));
|
|
593
644
|
var ctx = termsOf(item);
|
|
594
645
|
evidence(card, item, ctx);
|
|
595
646
|
var a = answersOf(item.id);
|
|
@@ -602,7 +653,7 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
|
|
|
602
653
|
var note = ask.appendChild(mark(el("p", "gate", waitText(item.id)), "wait", { id: item.id, seconds: waitSeconds(item.id) }));
|
|
603
654
|
state.gates[item.id] = { note: note, buttons: Array.prototype.slice.call(ask.querySelectorAll(".opts button")) };
|
|
604
655
|
}
|
|
605
|
-
|
|
656
|
+
Qs().forEach(function (q) {
|
|
606
657
|
var o = q.options.filter(function (x) { return x.value === a[q.id] && x.ask_why; })[0];
|
|
607
658
|
if (!o) return;
|
|
608
659
|
card.appendChild(el("p", "q", o.ask_why));
|
|
@@ -716,7 +767,7 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
|
|
|
716
767
|
if (prev && l.n > prev.n + 1) {
|
|
717
768
|
var from = prev.n + 1, to = l.n - 1;
|
|
718
769
|
pre.appendChild(mark(codeRow(" gap", "\u22ee", "", withGutter,
|
|
719
|
-
fill(from === to ?
|
|
770
|
+
fill(from === to ? words().labels.hidden_one : words().labels.hidden, { from: from, to: to })).row, "gap", { from: from, to: to }));
|
|
720
771
|
}
|
|
721
772
|
var r = codeRow((l.mark ? " hit" : "") + (l.context ? " ctx" : ""), (l.mark ? "\u25b6" : "") + l.n,
|
|
722
773
|
l.gutter, withGutter, l.text);
|
|
@@ -732,7 +783,7 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
|
|
|
732
783
|
} else if (!ex.annotation) r.code.appendChild(el("span", "this", ex.mark_label || "\u25c0 this line"));
|
|
733
784
|
});
|
|
734
785
|
box.appendChild(pre);
|
|
735
|
-
if (ex.caption) box.appendChild(rich("p", "said", ex.caption));
|
|
786
|
+
if (ex.caption) box.appendChild(tagged(rich("p", "said", ex.caption), ex.caption_from, "caption"));
|
|
736
787
|
return box;
|
|
737
788
|
}
|
|
738
789
|
// ── terms with a note (#392): a dotted term opens ONE floating note, anchored to it ─────────────
|
|
@@ -933,14 +984,14 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
|
|
|
933
984
|
// A card's quote comes first, under its title: the words asked about, then the code they are about.
|
|
934
985
|
function evidence(card, item, ctx) {
|
|
935
986
|
if (item.title) card.appendChild(rich("h3", "", item.title));
|
|
936
|
-
if (item.quote) card.appendChild(quoteBlock(item, ctx));
|
|
987
|
+
if (item.quote) card.appendChild(tagged(quoteBlock(item, ctx), item.quote.from, "quote"));
|
|
937
988
|
if (item.body !== undefined) {
|
|
938
989
|
var body = rich("p", "body", item.body);
|
|
939
|
-
card.appendChild(
|
|
990
|
+
card.appendChild(words().labels.body ? row(words().labels.body, body) : body);
|
|
940
991
|
}
|
|
941
|
-
if (item.repeat && item.repeat.of > 1) card.appendChild(el("p", "muted", fill(
|
|
992
|
+
if (item.repeat && item.repeat.of > 1) card.appendChild(el("p", "muted", fill(words().labels.repeat, item.repeat)));
|
|
942
993
|
if (item.code) card.appendChild(codeBlock(item.code));
|
|
943
|
-
if (item.unit) card.appendChild(row(item.unit.of > 1 ? fill(
|
|
994
|
+
if (item.unit) card.appendChild(row(item.unit.of > 1 ? fill(words().labels.line_part, { line: words().labels.line, part: item.unit.part, of: item.unit.of }) : words().labels.line,
|
|
944
995
|
markedLine(item.unit)));
|
|
945
996
|
// A rule the item names, with what it does in the linter's own words (looked up at build time).
|
|
946
997
|
(item.rules || []).forEach(function (r) {
|
|
@@ -952,20 +1003,28 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
|
|
|
952
1003
|
}
|
|
953
1004
|
if (item.file) card.appendChild(row("File", rich("p", "", item.file)));
|
|
954
1005
|
if (item.context) card.appendChild(rich("p", "muted", item.context));
|
|
955
|
-
if (item.situation && item.situation.summary) card.appendChild(situated("p", "situation", item.situation.summary, item.situation.line));
|
|
956
|
-
if (item.excerpt) card.appendChild(excerptBlock(item.excerpt, ctx));
|
|
1006
|
+
if (item.situation && item.situation.summary) card.appendChild(tagged(situated("p", "situation", item.situation.summary, item.situation.line), item.situation.from, "situation"));
|
|
1007
|
+
if (item.excerpt) card.appendChild(tagged(excerptBlock(item.excerpt, ctx), item.excerpt.from, "excerpt"));
|
|
957
1008
|
}
|
|
958
1009
|
function labelOf(q, v) {
|
|
959
1010
|
var o = q.options.filter(function (x) { return x.value === v; })[0];
|
|
960
1011
|
return o ? o.label : v;
|
|
961
1012
|
}
|
|
1013
|
+
// A card's own wording of the first question, or the translation's, with `{n}` its marked line's number.
|
|
1014
|
+
function situationQuestion(item) {
|
|
1015
|
+
var own = words().situation_question;
|
|
1016
|
+
if (own === undefined) return situated("p", "q", item.situation.question, item.situation.line);
|
|
1017
|
+
var hit = item.excerpt && item.excerpt.lines.filter(function (l) { return l.mark; })[0];
|
|
1018
|
+
var line = hit ? { n: hit.n, text: hit.text.trim() } : null;
|
|
1019
|
+
return situated("p", "q", line ? fill(own, { n: line.n }) : own, line);
|
|
1020
|
+
}
|
|
962
1021
|
// One question per screen: the answered ones stay as a line (with "Change"), and only one question
|
|
963
1022
|
// is asked: the one being changed, else the first unanswered.
|
|
964
1023
|
function askOne(card, a, editing, onPick, onChange, off, withExamples, item) {
|
|
965
|
-
var open =
|
|
966
|
-
var asked =
|
|
1024
|
+
var open = Qs().filter(function (q) { return !valid(DATA.allowed.questions[q.id], a[q.id]); });
|
|
1025
|
+
var asked = Qs().filter(function (q) { return q.id === editing; })[0] || open[0];
|
|
967
1026
|
var situation = item.situation, notes = item.answer_notes || {};
|
|
968
|
-
|
|
1027
|
+
Qs().forEach(function (q) {
|
|
969
1028
|
if (q === asked || !valid(DATA.allowed.questions[q.id], a[q.id])) return;
|
|
970
1029
|
var line = el("p", "answered", (q.short || q.text) + ": " + labelOf(q, a[q.id]) + " ");
|
|
971
1030
|
if (onChange) {
|
|
@@ -980,7 +1039,7 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
|
|
|
980
1039
|
if (!asked) return null;
|
|
981
1040
|
var ask = el("div", "ask");
|
|
982
1041
|
// A card's situation asks the page's first question in its own words; the others keep theirs.
|
|
983
|
-
ask.appendChild(mark(situation && asked ===
|
|
1042
|
+
ask.appendChild(mark(situation && asked === Qs()[0] ? situationQuestion(item) : rich("p", "q", asked.text),
|
|
984
1043
|
"question", { question: asked.id }));
|
|
985
1044
|
ask.appendChild(options(asked.options, a[asked.id], function (v) { onPick(asked, v); }, off, withExamples, notes[asked.id], asked.id));
|
|
986
1045
|
card.appendChild(ask);
|
|
@@ -990,18 +1049,19 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
|
|
|
990
1049
|
function howTo() {
|
|
991
1050
|
var sec = el("section");
|
|
992
1051
|
var body = sec;
|
|
993
|
-
|
|
1052
|
+
var heading = words().labels.how_to;
|
|
1053
|
+
if (practiceOpen()) sec.appendChild(el("h2", "", heading));
|
|
994
1054
|
else {
|
|
995
1055
|
body = sec.appendChild(el("details"));
|
|
996
|
-
body.appendChild(el("summary", "",
|
|
1056
|
+
body.appendChild(el("summary", "", heading));
|
|
997
1057
|
}
|
|
998
|
-
|
|
999
|
-
body.appendChild(el("p", "q", q.text));
|
|
1058
|
+
Qs().forEach(function (q) {
|
|
1059
|
+
body.appendChild(mark(el("p", "q", q.text), "how-to-question", { question: q.id }));
|
|
1000
1060
|
var list = el("div", "examples");
|
|
1001
1061
|
q.options.forEach(function (o) {
|
|
1002
|
-
var row = el("div", "example");
|
|
1062
|
+
var row = mark(el("div", "example"), "how-to-answer", { question: q.id, value: o.value });
|
|
1003
1063
|
row.appendChild(el("b", "", (o.icon ? o.icon + " " : "") + o.label));
|
|
1004
|
-
row.appendChild(rich("span", "muted",
|
|
1064
|
+
row.appendChild(rich("span", "muted", words().labels.for_example + o.example));
|
|
1005
1065
|
list.appendChild(row);
|
|
1006
1066
|
});
|
|
1007
1067
|
body.appendChild(list);
|
|
@@ -1016,7 +1076,7 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
|
|
|
1016
1076
|
var a = practiceAnswers(p.id);
|
|
1017
1077
|
askOne(card, a, undefined, function (q, v) { answerPractice(p.id, q.id, v); }, null, off, true, p);
|
|
1018
1078
|
if (practiceAnswered(p)) {
|
|
1019
|
-
var expected =
|
|
1079
|
+
var expected = Qs().map(function (q) { return labelOf(q, p.expected[q.id]); }).join(" · ");
|
|
1020
1080
|
card.appendChild(rich("p", practiceMatched(p) ? "status saved" : "quote",
|
|
1021
1081
|
(practiceMatched(p) ? "Matched. " : "Not matched. Expected: " + expected + ". ") + p.why));
|
|
1022
1082
|
}
|
|
@@ -1134,8 +1194,10 @@ button.more { width: auto; min-height: 44px; border: 0; color: var(--accent); pa
|
|
|
1134
1194
|
app.textContent = "";
|
|
1135
1195
|
var top = el("header", "top");
|
|
1136
1196
|
app.appendChild(top);
|
|
1137
|
-
|
|
1138
|
-
|
|
1197
|
+
try { document.documentElement.lang = state.lang; } catch (e) {}
|
|
1198
|
+
top.appendChild(el("h1", "", words().title));
|
|
1199
|
+
if (LANGS.length) top.appendChild(langSwitch());
|
|
1200
|
+
if (words().intro) top.appendChild(el("p", "muted", words().intro));
|
|
1139
1201
|
if (state.store === "none") {
|
|
1140
1202
|
top.appendChild(el("div", "banner",
|
|
1141
1203
|
"Answers cannot be saved in this view, so it is read-only. Open the page from the claude.ai " +
|