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.
@@ -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(DATA.labels.wait, { s: waitSeconds(id) }); }
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 DATA.questions.filter(function (q) { return q.id === qid; })[0]; }
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 DATA.questions.map(function (q) {
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 DATA.questions.filter(function (q) { return !valid(DATA.allowed.questions[q.id], a[q.id]); });
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 DATA.questions.every(function (q) { return a[q.id] === p.expected[q.id]; });
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", DATA.labels.practice_example + o.example));
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(DATA.labels.place, { id: item.id, n: shown.indexOf(item) + 1, total: shown.length })));
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
- DATA.questions.forEach(function (q) {
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 ? DATA.labels.hidden_one : DATA.labels.hidden, { from: from, to: to })).row, "gap", { from: from, to: 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(DATA.labels.body ? row(DATA.labels.body, body) : body);
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(DATA.labels.repeat, item.repeat)));
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(DATA.labels.line_part, { line: DATA.labels.line, part: item.unit.part, of: item.unit.of }) : DATA.labels.line,
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 = DATA.questions.filter(function (q) { return !valid(DATA.allowed.questions[q.id], a[q.id]); });
966
- var asked = DATA.questions.filter(function (q) { return q.id === editing; })[0] || open[0];
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
- DATA.questions.forEach(function (q) {
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 === DATA.questions[0] ? situated("p", "q", situation.question, situation.line) : rich("p", "q", asked.text),
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
- if (practiceOpen()) sec.appendChild(el("h2", "", "📖 How to answer"));
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", "", "📖 How to answer"));
1056
+ body.appendChild(el("summary", "", heading));
997
1057
  }
998
- DATA.questions.forEach(function (q) {
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", "For example: " + o.example));
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 = DATA.questions.map(function (q) { return labelOf(q, p.expected[q.id]); }).join(" · ");
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
- top.appendChild(el("h1", "", DATA.title));
1138
- if (DATA.intro) top.appendChild(el("p", "muted", DATA.intro));
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 " +