proschi 0.9.0 → 0.10.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/cli.cjs CHANGED
@@ -20267,6 +20267,12 @@ function problemFromFiles(id, files) {
20267
20267
  };
20268
20268
  }
20269
20269
 
20270
+ // ../frontend/src/practice/lessonBlocks.ts
20271
+ var CALLOUT_TONES = ["tip", "pitfall", "interview", "takeaway"];
20272
+ var RICH_BLOCKS = ["tldr", "callout", "numbers", "deepdive", "quiz"];
20273
+ var FENCE = /^\s*(`{3,})\s*([\w-]*)(?:\s+(.*?))?\s*$/;
20274
+ var closesFence = (line, ticks) => new RegExp(`^\\s*\`{${ticks.length},}\\s*$`).test(line);
20275
+
20270
20276
  // ../frontend/src/practice/lesson.ts
20271
20277
  var LESSON_HEADINGS = [
20272
20278
  "What you'll learn",
@@ -20279,19 +20285,43 @@ var LESSON_HEADINGS = [
20279
20285
  "Further reading"
20280
20286
  ];
20281
20287
  var normalize = (heading) => heading.replace(/[‘’]/g, "'").trim();
20282
- function proseLines(source2) {
20283
- const out = [];
20284
- let fence = false;
20288
+ function scan(source2) {
20289
+ const prose = [];
20290
+ const blocks2 = [];
20291
+ const open = [];
20292
+ let code;
20285
20293
  source2.replace(/\r\n?/g, "\n").split("\n").forEach((text, i) => {
20286
- if (/^\s*```/.test(text)) fence = !fence;
20287
- else if (!fence) out.push({ text, line: i + 1 });
20294
+ const line = i + 1;
20295
+ if (code !== void 0) {
20296
+ if (closesFence(text, code)) code = void 0;
20297
+ return;
20298
+ }
20299
+ const top = open.at(-1);
20300
+ if (top && closesFence(text, top.ticks)) {
20301
+ open.pop();
20302
+ return;
20303
+ }
20304
+ const fence = FENCE.exec(text);
20305
+ if (fence) {
20306
+ const [, ticks, lang, info = ""] = fence;
20307
+ if (top) blocks2.find((b) => b.line === top.line).body.push({ text, line });
20308
+ if (RICH_BLOCKS.includes(lang)) {
20309
+ const block = { lang, info, line, body: [] };
20310
+ blocks2.push(block);
20311
+ open.push({ ...block, ticks });
20312
+ } else code = ticks;
20313
+ return;
20314
+ }
20315
+ if (top) blocks2.find((b) => b.line === top.line).body.push({ text, line });
20316
+ if (top?.lang !== "quiz") prose.push({ text, line, ...top ? { block: blocks2.find((b) => b.line === top.line) } : {} });
20288
20317
  });
20289
- return out;
20318
+ return { prose, blocks: blocks2 };
20290
20319
  }
20320
+ var proseLines = (source2) => scan(source2).prose;
20291
20321
  function h2Headings(source2) {
20292
- return proseLines(source2).map(({ text, line }) => ({ m: /^##\s+(.*?)\s*#*\s*$/.exec(text), line })).filter((h) => h.m !== null).map(({ m, line }) => ({ text: m[1], line }));
20322
+ return proseLines(source2).filter((l) => !l.block).map(({ text, line }) => ({ m: /^##\s+(.*?)\s*#*\s*$/.exec(text), line })).filter((h) => h.m !== null).map(({ m, line }) => ({ text: m[1], line }));
20293
20323
  }
20294
- function lessonIssues(source2) {
20324
+ function lessonIssues(source2, cardIds) {
20295
20325
  const issues = [];
20296
20326
  if (!source2.trim()) return [{ message: "The lesson is empty" }];
20297
20327
  const headings = h2Headings(source2);
@@ -20310,6 +20340,7 @@ function lessonIssues(source2) {
20310
20340
  lastName = required;
20311
20341
  }
20312
20342
  }
20343
+ issues.push(...blockIssues(source2, cardIds));
20313
20344
  for (const { text, line } of proseLines(source2)) {
20314
20345
  const prose = text.replace(/`[^`]*`/g, "");
20315
20346
  for (const m of prose.matchAll(/\]\(([^)\s]*)[^)]*\)/g)) {
@@ -20318,6 +20349,26 @@ function lessonIssues(source2) {
20318
20349
  }
20319
20350
  return issues;
20320
20351
  }
20352
+ function blockIssues(source2, cardIds) {
20353
+ const issues = [];
20354
+ for (const block of scan(source2).blocks) {
20355
+ const { lang, info, line } = block;
20356
+ const tone = info.split(/\s+/)[0];
20357
+ if (lang === "callout" && !CALLOUT_TONES.includes(tone))
20358
+ issues.push({ message: `A callout needs a tone after "callout": ${CALLOUT_TONES.join(", ")}${tone ? `, not "${tone}"` : ""}`, line });
20359
+ if (lang === "deepdive" && !info.trim()) issues.push({ message: 'A deepdive needs a title after "deepdive"', line });
20360
+ if (lang !== "callout" && lang !== "deepdive" && info.trim()) issues.push({ message: `A ${lang} block takes nothing after its name`, line });
20361
+ if (!block.body.some((l) => l.text.trim())) issues.push({ message: `The ${lang} block is empty`, line });
20362
+ for (const { text, line: at } of block.body) {
20363
+ if (lang === "quiz") {
20364
+ for (const id of text.split(/\s+/).filter(Boolean)) if (cardIds && !cardIds.has(id)) issues.push({ message: `The quiz names "${id}", which is not a review card`, line: at });
20365
+ } else if (FENCE.test(text)) continue;
20366
+ else if (/^#{1,2}\s/.test(text)) issues.push({ message: `Headings inside a ${lang} block must be ### or smaller, so the lesson's sections stay its own`, line: at });
20367
+ else if (lang === "numbers" && text.trim() && !/^[^|]+\|\s*\S/.test(text)) issues.push({ message: 'Each line of a numbers block is "value | label"', line: at });
20368
+ }
20369
+ }
20370
+ return issues;
20371
+ }
20321
20372
 
20322
20373
  // ../frontend/src/practice/markdown.ts
20323
20374
  function safeHref(href) {
@@ -20395,7 +20446,6 @@ function delimiterRow(line) {
20395
20446
  return cells.map((c) => c.startsWith(":") && c.endsWith(":") ? "center" : c.endsWith(":") ? "right" : c.startsWith(":") ? "left" : void 0);
20396
20447
  }
20397
20448
  var LIST_ITEM = /^\s*(?:([-*+])|(\d+)[.)])\s+(.*)$/;
20398
- var FENCE = /^\s*```\s*([\w-]*)(?:\s+[\w-]+)*\s*$/;
20399
20449
  var QUOTE = /^\s{0,3}>\s?(.*)$/;
20400
20450
  function parseMarkdown(source2, slug2 = slugger()) {
20401
20451
  const lines = source2.replace(/\r\n?/g, "\n").split("\n");
@@ -20419,9 +20469,10 @@ function parseMarkdown(source2, slug2 = slugger()) {
20419
20469
  const fence = FENCE.exec(line);
20420
20470
  if (fence) {
20421
20471
  flush();
20472
+ const [, ticks, lang, info = ""] = fence;
20422
20473
  const body = [];
20423
- while (++i < lines.length && !/^\s*```\s*$/.test(lines[i])) body.push(lines[i]);
20424
- blocks2.push({ kind: "code", ...fence[1] ? { lang: fence[1] } : {}, text: body.join("\n") });
20474
+ while (++i < lines.length && !closesFence(lines[i], ticks)) body.push(lines[i]);
20475
+ blocks2.push(isRich(lang) ? richBlock(lang, info, body.join("\n"), slug2) : { kind: "code", ...lang ? { lang } : {}, text: body.join("\n") });
20425
20476
  continue;
20426
20477
  }
20427
20478
  if (!line.trim()) {
@@ -20477,6 +20528,30 @@ function parseMarkdown(source2, slug2 = slugger()) {
20477
20528
  flush();
20478
20529
  return blocks2;
20479
20530
  }
20531
+ var isRich = (lang) => RICH_BLOCKS.includes(lang);
20532
+ function richBlock(lang, info, body, slug2) {
20533
+ switch (lang) {
20534
+ case "tldr":
20535
+ return { kind: "tldr", children: parseMarkdown(body, slug2) };
20536
+ case "callout": {
20537
+ const [, tone = "", title = ""] = /^(\S*)\s*(.*)$/.exec(info) ?? [];
20538
+ const known = CALLOUT_TONES.includes(tone) ? tone : "tip";
20539
+ return { kind: "callout", tone: known, ...title ? { title: parseInline(title) } : {}, children: parseMarkdown(body, slug2) };
20540
+ }
20541
+ case "numbers":
20542
+ return {
20543
+ kind: "numbers",
20544
+ items: body.split("\n").filter((l) => l.trim()).map((l) => {
20545
+ const bar = l.indexOf("|");
20546
+ return bar < 0 ? { value: l.trim(), label: [] } : { value: l.slice(0, bar).trim(), label: parseInline(l.slice(bar + 1).trim()) };
20547
+ })
20548
+ };
20549
+ case "deepdive":
20550
+ return { kind: "deepdive", title: parseInline(info || "Deep dive"), children: parseMarkdown(body, slug2) };
20551
+ case "quiz":
20552
+ return { kind: "quiz", ids: body.split(/\s+/).filter(Boolean) };
20553
+ }
20554
+ }
20480
20555
 
20481
20556
  // ../frontend/src/practice/modes/mdItems.ts
20482
20557
  function splitAt(lines, level) {
@@ -21094,7 +21169,7 @@ function validateProblem(problem, engine = defaultEngine, options = {}) {
21094
21169
  if (!problem.statement.includes(`**${name}**`)) add(PROBLEM_MD, `The statement must name the use case **${name}** in bold`);
21095
21170
  if (!problem.solution.includes(`usecase "${name}"`)) add(SOLUTION, `The solution must define usecase "${name}" from the traffic`);
21096
21171
  }
21097
- if (problem.lesson !== void 0) for (const issue of lessonIssues(problem.lesson)) add(LESSON_MD, issue.message, issue.line);
21172
+ if (problem.lesson !== void 0) for (const issue of lessonIssues(problem.lesson, options.cardIds)) add(LESSON_MD, issue.message, issue.line);
21098
21173
  const importLine = problem.given.split("\n").findIndex((l) => /^\s*import /.test(l));
21099
21174
  if (importLine >= 0) add(GIVEN, "The given must not import anything", importLine + 1);
21100
21175
  report(GIVEN, parse(problem.given).diagnostics.filter((d) => d.severity === "error"));
@@ -21808,7 +21883,7 @@ var FACTOR = 19 / 81;
21808
21883
 
21809
21884
  // ../frontend/src/learn/achievements.ts
21810
21885
  var TIERS = ["bronze", "silver", "gold"];
21811
- var ICONS = ["layers", "brain", "flame", "trophy", "check", "zap", "coins", "calculator", "target", "map", "database", "radio", "shield", "star"];
21886
+ var ICONS = ["layers", "brain", "flame", "trophy", "check", "zap", "coins", "calculator", "target", "map", "database", "radio", "shield", "star", "book"];
21812
21887
  var RULE_FIELDS = {
21813
21888
  reviews: { required: ["min"], optional: [] },
21814
21889
  mastered: { required: ["min"], optional: [] },
@@ -21825,7 +21900,10 @@ var RULE_FIELDS = {
21825
21900
  "challenge-streak": { required: ["min"], optional: [] },
21826
21901
  "game-waves": { required: ["min"], optional: [] },
21827
21902
  "game-clears": { required: ["min"], optional: [] },
21828
- "game-ascension": { required: ["min"], optional: [] }
21903
+ "game-ascension": { required: ["min"], optional: [] },
21904
+ lessons: { required: ["min"], optional: ["stage"] },
21905
+ "stage-lessons": { required: [], optional: [] },
21906
+ "all-lessons": { required: [], optional: [] }
21829
21907
  };
21830
21908
  var RULE_KINDS = Object.keys(RULE_FIELDS);
21831
21909
  var ACHIEVEMENTS_LOCK_FILE = "achievements.lock";
@@ -21913,6 +21991,10 @@ function checkRule(raw, context, retired = false) {
21913
21991
  if (rule.stage !== void 0 && (typeof rule.stage !== "string" || !context.stages.includes(rule.stage))) return `"stage" names "${String(rule.stage)}", which is not a roadmap stage`;
21914
21992
  if (kind === "game-waves" && rule.min > 24) return '"min" of a game-waves rule is at most 24, the last wave of Endless';
21915
21993
  if (kind === "game-ascension" && rule.min > 10) return '"min" of a game-ascension rule is at most 10';
21994
+ if (kind === "lessons") {
21995
+ const reachable = rule.stage ? context.stageProblems?.[rule.stage]?.length ?? Infinity : context.problems.length;
21996
+ if (rule.min > reachable) return `"min" is ${String(rule.min)}, but only ${reachable} lesson(s) can count towards it`;
21997
+ }
21916
21998
  if (kind === "solved" || kind === "first-run" || kind === "under-reference") {
21917
21999
  const reachable = context.problems.filter((p) => (!rule.difficulty || p.difficulty === rule.difficulty) && (!rule.tag || p.tags.includes(rule.tag))).length;
21918
22000
  if (rule.min > reachable) return `"min" is ${String(rule.min)}, but only ${reachable} problem(s) can count towards it`;
@@ -22094,7 +22176,7 @@ ${ACHIEVEMENTS_USAGE}`);
22094
22176
  err(`${displayPath3(file)}: not valid JSON: ${e.message}`);
22095
22177
  return 1;
22096
22178
  }
22097
- const violations = checkAchievements(raw, { problems: problemsIn(problemsDir), topics, stages: ROADMAP.map((s) => s.id), lock: lock ?? "" }).violations;
22179
+ const violations = checkAchievements(raw, { problems: problemsIn(problemsDir), topics, stages: ROADMAP.map((s) => s.id), stageProblems: Object.fromEntries(ROADMAP.map((s) => [s.id, s.problems])), lock: lock ?? "" }).violations;
22098
22180
  if (lock === void 0) {
22099
22181
  const fresh = violations.filter((v) => !v.message.startsWith("New achievement"));
22100
22182
  violations.length = 0;
@@ -26196,7 +26278,7 @@ ${IMPORT_USAGE}`);
26196
26278
  }
26197
26279
 
26198
26280
  // src/cli.ts
26199
- var VERSION2 = true ? "0.9.0" : "dev";
26281
+ var VERSION2 = true ? "0.10.0" : "dev";
26200
26282
  var USAGE = `Usage:
26201
26283
  proschi check [--strict] [--format text|github|json] [--openapi <node>=<spec>]... <file|dir>...
26202
26284
  proschi parse <file>
package/dist/server.cjs CHANGED
@@ -29432,7 +29432,7 @@ function positionAt(text, offset) {
29432
29432
 
29433
29433
  // src/server.ts
29434
29434
  if (process.argv.includes("--version")) {
29435
- console.log(true ? "0.9.0" : "dev");
29435
+ console.log(true ? "0.10.0" : "dev");
29436
29436
  process.exit(0);
29437
29437
  }
29438
29438
  var connection = (0, import_node.createConnection)(import_node.ProposedFeatures.all);
@@ -29473,7 +29473,7 @@ connection.onInitialize(() => ({
29473
29473
  documentFormattingProvider: true,
29474
29474
  codeActionProvider: { codeActionKinds: [import_node.CodeActionKind.QuickFix] }
29475
29475
  },
29476
- serverInfo: { name: "proschi-language-server", version: true ? "0.9.0" : "dev" }
29476
+ serverInfo: { name: "proschi-language-server", version: true ? "0.10.0" : "dev" }
29477
29477
  }));
29478
29478
  function validate(document) {
29479
29479
  const analysis = analysisOf(document);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "proschi",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Command-line checker and language server for Proschi architecture diagrams",
5
5
  "license": "MIT",
6
6
  "repository": {