create-eziwiki 0.2.0 → 0.3.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.
@@ -12,6 +12,7 @@
12
12
  "@shikijs/rehype": "^4.3.1",
13
13
  "ajv": "^8.12.0",
14
14
  "ajv-formats": "^3.0.1",
15
+ "beautiful-mermaid": "^1.1.3",
15
16
  "github-slugger": "^2.0.0",
16
17
  "gray-matter": "^4.0.3",
17
18
  "hast-util-to-string": "^3.0.1",
@@ -2655,6 +2656,28 @@
2655
2656
  "baseline-browser-mapping": "dist/cli.js"
2656
2657
  }
2657
2658
  },
2659
+ "node_modules/beautiful-mermaid": {
2660
+ "version": "1.1.3",
2661
+ "resolved": "https://registry.npmjs.org/beautiful-mermaid/-/beautiful-mermaid-1.1.3.tgz",
2662
+ "integrity": "sha512-TItrtrAyHp1vwFfFVYauWGrquouk/6SS21Aq3RsxindSYZODcN4xYrPZD6BiZRU+o5mKJzDPz9MUSMvELdylyg==",
2663
+ "license": "MIT",
2664
+ "dependencies": {
2665
+ "elkjs": "^0.11.0",
2666
+ "entities": "^7.0.1"
2667
+ }
2668
+ },
2669
+ "node_modules/beautiful-mermaid/node_modules/entities": {
2670
+ "version": "7.0.1",
2671
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
2672
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
2673
+ "license": "BSD-2-Clause",
2674
+ "engines": {
2675
+ "node": ">=0.12"
2676
+ },
2677
+ "funding": {
2678
+ "url": "https://github.com/fb55/entities?sponsor=1"
2679
+ }
2680
+ },
2658
2681
  "node_modules/binary-extensions": {
2659
2682
  "version": "2.3.0",
2660
2683
  "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -3244,6 +3267,12 @@
3244
3267
  "dev": true,
3245
3268
  "license": "ISC"
3246
3269
  },
3270
+ "node_modules/elkjs": {
3271
+ "version": "0.11.1",
3272
+ "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.11.1.tgz",
3273
+ "integrity": "sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==",
3274
+ "license": "EPL-2.0"
3275
+ },
3247
3276
  "node_modules/emoji-regex": {
3248
3277
  "version": "9.2.2",
3249
3278
  "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
@@ -24,6 +24,7 @@
24
24
  "@shikijs/rehype": "^4.3.1",
25
25
  "ajv": "^8.12.0",
26
26
  "ajv-formats": "^3.0.1",
27
+ "beautiful-mermaid": "^1.1.3",
27
28
  "github-slugger": "^2.0.0",
28
29
  "gray-matter": "^4.0.3",
29
30
  "hast-util-to-string": "^3.0.1",
@@ -1,40 +1,82 @@
1
1
  #!/usr/bin/env tsx
2
2
 
3
3
  /**
4
- * Reports links that point at no page.
4
+ * Reports on the shape of the link graph.
5
5
  *
6
- * Runs as part of the build and reports rather than fails: a dangling link in
7
- * one page is not a reason to block a deploy of the other twenty, and content
8
- * is often written before the page it references exists. Pass `--strict` in CI
9
- * to make it an error.
6
+ * Two kinds of finding, treated differently. An unresolved link is an error in
7
+ * the content and `--strict` makes it fail the build, which is what CI passes.
8
+ * A page nothing links to, or nothing links on from, is not an error — a
9
+ * correct wiki can have either — so those are reported and never fail. They are
10
+ * the shapes a set of documents falls into as it stops being a wiki, and
11
+ * neither is visible from inside a single page.
12
+ *
13
+ * Reporting rather than failing on unresolved links by default is deliberate
14
+ * too: a dangling link in one page is not a reason to block a deploy of the
15
+ * other twenty, and content is often written before the page it references.
10
16
  */
11
17
 
12
18
  import { getLinkGraph } from '../lib/graph/build';
19
+ import { getWikiHealth } from '../lib/graph/health';
20
+ import type { GraphNode } from '../lib/graph/build';
13
21
 
14
22
  const strict = process.argv.includes('--strict');
15
23
  const { broken, nodes, edges } = getLinkGraph();
16
24
 
17
- if (broken.length === 0) {
18
- console.log(`🔗 Links OK — ${edges.length} links across ${nodes.length} pages\n`);
19
- process.exit(0);
25
+ /**
26
+ * Prints a list of pages under a heading, or nothing when there are none.
27
+ *
28
+ * @param label - What the pages have in common, singular and plural
29
+ * @param explanation - Why it is worth knowing
30
+ * @param pages - The pages found
31
+ */
32
+ function report(
33
+ label: { one: string; many: string },
34
+ explanation: string,
35
+ pages: GraphNode[],
36
+ ): void {
37
+ if (pages.length === 0) return;
38
+
39
+ const noun = pages.length === 1 ? label.one : label.many;
40
+
41
+ console.log(`⚠️ ${pages.length} ${noun} — ${explanation}`);
42
+ for (const page of pages) console.log(` content/${page.path}.md`);
43
+ console.log();
20
44
  }
21
45
 
22
- console.log(`\n🔗 ${broken.length} unresolved link${broken.length === 1 ? '' : 's'}:\n`);
46
+ if (broken.length > 0) {
47
+ console.log(`\n🔗 ${broken.length} unresolved link${broken.length === 1 ? '' : 's'}:\n`);
23
48
 
24
- for (const link of broken) {
25
- console.log(` content/${link.from}.md`);
49
+ for (const link of broken) {
50
+ console.log(` content/${link.from}.md`);
26
51
 
27
- if (link.reason === 'ambiguous') {
28
- console.log(` [[${link.target}]] is ambiguous — matches ${link.candidates?.join(', ')}`);
29
- console.log(' Use the full path to disambiguate.');
30
- } else {
31
- console.log(` [[${link.target}]] matches no page`);
52
+ if (link.reason === 'ambiguous') {
53
+ console.log(` [[${link.target}]] is ambiguous — matches ${link.candidates?.join(', ')}`);
54
+ console.log(' Use the full path to disambiguate.');
55
+ } else {
56
+ console.log(` [[${link.target}]] matches no page`);
57
+ }
32
58
  }
59
+
60
+ console.log();
61
+ } else {
62
+ console.log(`\n🔗 Links OK — ${edges.length} links across ${nodes.length} pages\n`);
33
63
  }
34
64
 
35
- console.log();
65
+ const { orphans, deadEnds } = getWikiHealth();
66
+
67
+ report(
68
+ { one: 'orphaned page', many: 'orphaned pages' },
69
+ 'nothing links here, so a reader can only arrive from the sidebar',
70
+ orphans,
71
+ );
72
+
73
+ report(
74
+ { one: 'dead end', many: 'dead ends' },
75
+ 'no links out, so a reader arrives with nowhere to go',
76
+ deadEnds,
77
+ );
36
78
 
37
- if (strict) {
79
+ if (broken.length > 0 && strict) {
38
80
  console.error('❌ Failing because --strict was passed.\n');
39
81
  process.exit(1);
40
82
  }
@@ -239,3 +239,160 @@ html.dark .ezw-broken-link {
239
239
  }
240
240
  }
241
241
  }
242
+
243
+ /* Callouts from `> [!NOTE]`, GitHub's and Obsidian's shared syntax.
244
+ Distinguished by a coloured edge and a heading rather than an icon: no glyph
245
+ to load, and the meaning survives when colour alone is not perceived. */
246
+ .ezw-callout {
247
+ --ezw-callout-accent: var(--color-text-muted);
248
+ margin: 1.5rem 0;
249
+ padding: 0.875rem 1.125rem;
250
+ border: 1px solid var(--color-border);
251
+ border-left: 3px solid var(--ezw-callout-accent);
252
+ border-radius: 0.375rem;
253
+ background: var(--color-sidebar-bg);
254
+ }
255
+
256
+ .ezw-callout__title {
257
+ margin: 0 0 0.5rem;
258
+ font-size: 0.8125rem;
259
+ font-weight: 600;
260
+ letter-spacing: 0.01em;
261
+ color: var(--ezw-callout-accent);
262
+ }
263
+
264
+ /* The body's first block sits against the title, and the last against the
265
+ floor, so a one-paragraph callout is not padded twice over. */
266
+ .ezw-callout > :nth-child(2) {
267
+ margin-top: 0;
268
+ }
269
+
270
+ .ezw-callout > :last-child {
271
+ margin-bottom: 0;
272
+ }
273
+
274
+ /* A foldable callout is a <details>; the marker is dropped in favour of the
275
+ title carrying the affordance, and the cursor says it is interactive. */
276
+ summary.ezw-callout__title {
277
+ cursor: pointer;
278
+ list-style: none;
279
+ }
280
+
281
+ summary.ezw-callout__title::-webkit-details-marker {
282
+ display: none;
283
+ }
284
+
285
+ summary.ezw-callout__title::before {
286
+ content: '▸';
287
+ display: inline-block;
288
+ /* A trailing space in `content` collapses, leaving the marker against the
289
+ title, so the gap is a margin. */
290
+ margin-right: 0.4em;
291
+ transition: transform 120ms ease;
292
+ }
293
+
294
+ details[open] > summary.ezw-callout__title::before {
295
+ transform: rotate(90deg);
296
+ }
297
+
298
+ details.ezw-callout:not([open]) > summary.ezw-callout__title {
299
+ margin-bottom: 0;
300
+ }
301
+
302
+ .ezw-callout--note {
303
+ --ezw-callout-accent: #1d4ed8;
304
+ }
305
+
306
+ .ezw-callout--tip {
307
+ --ezw-callout-accent: #047857;
308
+ }
309
+
310
+ .ezw-callout--important {
311
+ --ezw-callout-accent: #6d28d9;
312
+ }
313
+
314
+ .ezw-callout--warning {
315
+ --ezw-callout-accent: #b45309;
316
+ }
317
+
318
+ .ezw-callout--caution {
319
+ --ezw-callout-accent: #b91c1c;
320
+ }
321
+
322
+ .dark .ezw-callout--note {
323
+ --ezw-callout-accent: #93c5fd;
324
+ }
325
+
326
+ .dark .ezw-callout--tip {
327
+ --ezw-callout-accent: #6ee7b7;
328
+ }
329
+
330
+ .dark .ezw-callout--important {
331
+ --ezw-callout-accent: #c4b5fd;
332
+ }
333
+
334
+ .dark .ezw-callout--warning {
335
+ --ezw-callout-accent: #fcd34d;
336
+ }
337
+
338
+ .dark .ezw-callout--caution {
339
+ --ezw-callout-accent: #fca5a5;
340
+ }
341
+
342
+ /* A link on each heading pointing at itself, so a section can be shared
343
+ without reading the id out of the address bar. */
344
+ .ezw-heading {
345
+ position: relative;
346
+ }
347
+
348
+ .ezw-heading__anchor {
349
+ margin-left: 0.4em;
350
+ font-weight: 400;
351
+ color: var(--color-text-muted);
352
+ text-decoration: none;
353
+ /* Hidden until wanted, but only visually: it stays in the accessible tree
354
+ and in the tab order, so a keyboard or screen reader user can reach it. */
355
+ opacity: 0;
356
+ transition: opacity 120ms ease;
357
+ }
358
+
359
+ .ezw-heading:hover .ezw-heading__anchor,
360
+ .ezw-heading__anchor:focus-visible {
361
+ opacity: 1;
362
+ }
363
+
364
+ .ezw-heading__anchor:hover {
365
+ color: var(--color-text);
366
+ }
367
+
368
+ /* A pointer is not always available. On touch the anchor is simply shown,
369
+ since there is no hover to reveal it with. */
370
+ @media (hover: none) {
371
+ .ezw-heading__anchor {
372
+ opacity: 0.5;
373
+ }
374
+ }
375
+
376
+ /* Diagrams from ```mermaid fences, drawn during the build.
377
+ The SVG refers to --bg and --fg but no longer fixes them inline, so the
378
+ colours come from here and follow the theme — the same arrangement the
379
+ syntax highlighter uses for code. */
380
+ .ezw-mermaid {
381
+ --bg: transparent;
382
+ --fg: var(--color-text);
383
+ --line: var(--color-text-muted);
384
+ --accent: var(--color-primary, #2563eb);
385
+ --muted: var(--color-text-muted);
386
+ --surface: var(--color-code-bg);
387
+ --border: var(--color-border);
388
+ margin: 1.5rem 0;
389
+ /* A wide diagram scrolls inside its own box rather than widening the page. */
390
+ overflow-x: auto;
391
+ }
392
+
393
+ .ezw-mermaid svg {
394
+ display: block;
395
+ max-width: 100%;
396
+ height: auto;
397
+ margin-inline: auto;
398
+ }