@hawkeyexl/manni 4.4.1 → 4.5.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.
Files changed (163) hide show
  1. package/README.md +1 -1
  2. package/dist/{batch-GFNLTGOH.js → batch-GOE3UXOD.js} +11 -11
  3. package/dist/{build-COYIQ25L.js → build-M7BMEHL2.js} +13 -11
  4. package/dist/{calibrate-MDBLJKEE.js → calibrate-6RRAILUY.js} +12 -12
  5. package/dist/{capture-LRML2PKV.js → capture-RJPQ6B4N.js} +12 -12
  6. package/dist/{check-EDLBYL5N.js → check-BQD64X3N.js} +11 -11
  7. package/dist/{check-HYNGXYJV.js → check-ESNS7ZBC.js} +4 -4
  8. package/dist/{check-ASPIHA6B.js → check-RQIIIURI.js} +12 -12
  9. package/dist/{chunk-NXV7WE2D.js → chunk-3R5Y3XOL.js} +3 -3
  10. package/dist/{chunk-A5ULMVHQ.js → chunk-3YHAYXBC.js} +2 -2
  11. package/dist/{chunk-57NEQRVO.js → chunk-527HHQPS.js} +4 -4
  12. package/dist/{chunk-57NEQRVO.js.map → chunk-527HHQPS.js.map} +1 -1
  13. package/dist/{chunk-6OYEI2VV.js → chunk-52PDIZKV.js} +3 -3
  14. package/dist/{chunk-UJCVAWP7.js → chunk-6XTQDFGP.js} +2 -2
  15. package/dist/{chunk-UJCVAWP7.js.map → chunk-6XTQDFGP.js.map} +1 -1
  16. package/dist/{chunk-3VK37OR2.js → chunk-7OQAU4EA.js} +5 -5
  17. package/dist/{chunk-4IUC2MNL.js → chunk-7ZUCEMCY.js} +8 -8
  18. package/dist/{chunk-M2JCK5F6.js → chunk-B7XUEQF3.js} +13 -2
  19. package/dist/chunk-B7XUEQF3.js.map +1 -0
  20. package/dist/{chunk-MYMBCTDE.js → chunk-BOIF2XGG.js} +2 -2
  21. package/dist/{chunk-YJOGUXGP.js → chunk-C3KI45RF.js} +8 -6
  22. package/dist/{chunk-YJOGUXGP.js.map → chunk-C3KI45RF.js.map} +1 -1
  23. package/dist/{chunk-MMTSSTG7.js → chunk-C6HZ6C5T.js} +2 -2
  24. package/dist/{chunk-QQUKGCAJ.js → chunk-CP4ZM43V.js} +2 -2
  25. package/dist/chunk-DE7JSWXV.js +2462 -0
  26. package/dist/chunk-DE7JSWXV.js.map +1 -0
  27. package/dist/{chunk-YB4IBW3M.js → chunk-DX32P2DT.js} +2 -2
  28. package/dist/{chunk-RL5XWQOL.js → chunk-EL2BZTDL.js} +137 -50
  29. package/dist/chunk-EL2BZTDL.js.map +1 -0
  30. package/dist/{chunk-WB3QM6PP.js → chunk-ELZ2IIOF.js} +332 -2522
  31. package/dist/chunk-ELZ2IIOF.js.map +1 -0
  32. package/dist/{chunk-MWJLZMBL.js → chunk-F2OVHJNC.js} +10 -10
  33. package/dist/{chunk-I2AH3CJJ.js → chunk-FLF6AECQ.js} +7 -7
  34. package/dist/{chunk-GCQRPUSB.js → chunk-GAZFKZHM.js} +4 -4
  35. package/dist/{chunk-QZ6LRSNV.js → chunk-HQFVJDEN.js} +7 -7
  36. package/dist/{chunk-M3W7J6MU.js → chunk-HVFR66GQ.js} +2 -2
  37. package/dist/{chunk-2B7ZNTPV.js → chunk-IA7BMIPE.js} +2 -2
  38. package/dist/{chunk-PIKOVN6W.js → chunk-JQU7FHYH.js} +6 -6
  39. package/dist/{chunk-NLXTXE5Y.js → chunk-K3U5F2QT.js} +183 -31
  40. package/dist/chunk-K3U5F2QT.js.map +1 -0
  41. package/dist/{chunk-JCDXE76J.js → chunk-MZC62HPZ.js} +2 -2
  42. package/dist/{chunk-TK3FC4RF.js → chunk-NAZRLJAK.js} +3 -3
  43. package/dist/{chunk-JXLTXMJ7.js → chunk-NGK3N6F2.js} +3 -3
  44. package/dist/{chunk-PU6DFJMY.js → chunk-NN4ASVTY.js} +3 -3
  45. package/dist/{chunk-C4SQM22E.js → chunk-OIZ3XMKB.js} +13 -11
  46. package/dist/chunk-OIZ3XMKB.js.map +1 -0
  47. package/dist/{chunk-6JSSEWBG.js → chunk-QCBICNKC.js} +6 -6
  48. package/dist/chunk-QCBICNKC.js.map +1 -0
  49. package/dist/{chunk-DXNZS72W.js → chunk-QLUMKWQQ.js} +3 -3
  50. package/dist/{chunk-EPH54CAY.js → chunk-QY3DOZMS.js} +4 -4
  51. package/dist/{chunk-FBEQCK47.js → chunk-SJO2FTPG.js} +6 -6
  52. package/dist/{chunk-PNPQTHBT.js → chunk-SNEHX4VE.js} +2 -2
  53. package/dist/{chunk-SKSDRUUK.js → chunk-TA42STP7.js} +4 -4
  54. package/dist/{chunk-CK7SKVHR.js → chunk-TBAMBLF3.js} +3 -12
  55. package/dist/chunk-TBAMBLF3.js.map +1 -0
  56. package/dist/{chunk-IDV6HJCB.js → chunk-TCC737IS.js} +3 -3
  57. package/dist/{chunk-QRWGPQKO.js → chunk-TVIX653Q.js} +3 -3
  58. package/dist/{chunk-YTURO7SI.js → chunk-UK2I3ACY.js} +3 -3
  59. package/dist/{chunk-7UKO3ASZ.js → chunk-XWFNO5PO.js} +3 -3
  60. package/dist/{chunk-37LP5HMZ.js → chunk-YA63PVWI.js} +5 -5
  61. package/dist/chunk-YGFIQ6Y5.js +17 -0
  62. package/dist/chunk-YGFIQ6Y5.js.map +1 -0
  63. package/dist/{chunk-UQISLDAV.js → chunk-ZX43SRZR.js} +3 -3
  64. package/dist/{cli-MN7PYMBI.js → cli-6PGVHP7Y.js} +15 -15
  65. package/dist/{cli-6UB5N6CU.js → cli-AAG3N4NB.js} +16 -16
  66. package/dist/{cli-6HIDIFVT.js → cli-E3H6ZP4I.js} +2 -2
  67. package/dist/{cli-RPM5PELX.js → cli-HUIMNJT6.js} +20 -15
  68. package/dist/cli-HUIMNJT6.js.map +1 -0
  69. package/dist/{cli-VI7VHWNZ.js → cli-MHTYPVOD.js} +16 -16
  70. package/dist/{cli-LFP5ZGYS.js → cli-PRZHQ3QB.js} +15 -13
  71. package/dist/{cli-LFP5ZGYS.js.map → cli-PRZHQ3QB.js.map} +1 -1
  72. package/dist/{cli-3BIBYHFK.js → cli-QKIJOXGW.js} +13 -13
  73. package/dist/{cli-PYPX6O6E.js → cli-TYGOHJOJ.js} +25 -25
  74. package/dist/{cli-673OTRGE.js → cli-YW6VWBHX.js} +9 -9
  75. package/dist/cli.js +11 -11
  76. package/dist/docmeta.js +8 -8
  77. package/dist/{github-SXPGP5DB.js → github-3WMSTNTK.js} +13 -13
  78. package/dist/{github-6QMYOHC3.js → github-OXSJQ4PS.js} +11 -11
  79. package/dist/index.d.ts +30 -8
  80. package/dist/index.js +42 -38
  81. package/dist/index.js.map +1 -1
  82. package/dist/{json-SY7KLNC7.js → json-4DCKDGUV.js} +11 -11
  83. package/dist/lint-YDHVVZHT.js +30 -0
  84. package/dist/{list-KZNVTNVC.js → list-3ZO2NIYO.js} +11 -11
  85. package/dist/{list-Q57YTFUQ.js → list-EJH6VAOW.js} +11 -11
  86. package/dist/{page-KSGV5RD4.js → page-QEDB7EVU.js} +10 -10
  87. package/dist/{pretty-A5EGTN35.js → pretty-5DGEBZSV.js} +11 -11
  88. package/dist/{pretty-CXSDXNAS.js → pretty-SQX33UM2.js} +12 -12
  89. package/dist/{reporters-HUY7UXFS.js → reporters-OHSI3RMV.js} +12 -11
  90. package/dist/{reporters-K2VUKSAK.js → reporters-YELDDYJD.js} +4 -4
  91. package/dist/{run-3DYJNRIG.js → run-IP6NZHAG.js} +11 -11
  92. package/dist/{term-BIQUPMOR.js → term-2WMDZ4PY.js} +15 -15
  93. package/dist/{validate-53RKM2XG.js → validate-G6HH3YXL.js} +5 -5
  94. package/package.json +1 -1
  95. package/plugin/manni/.claude-plugin/plugin.json +1 -1
  96. package/dist/chunk-6JSSEWBG.js.map +0 -1
  97. package/dist/chunk-C4SQM22E.js.map +0 -1
  98. package/dist/chunk-CK7SKVHR.js.map +0 -1
  99. package/dist/chunk-M2JCK5F6.js.map +0 -1
  100. package/dist/chunk-NLXTXE5Y.js.map +0 -1
  101. package/dist/chunk-RL5XWQOL.js.map +0 -1
  102. package/dist/chunk-WB3QM6PP.js.map +0 -1
  103. package/dist/cli-RPM5PELX.js.map +0 -1
  104. package/dist/lint-ASMJLLPA.js +0 -28
  105. /package/dist/{batch-GFNLTGOH.js.map → batch-GOE3UXOD.js.map} +0 -0
  106. /package/dist/{build-COYIQ25L.js.map → build-M7BMEHL2.js.map} +0 -0
  107. /package/dist/{calibrate-MDBLJKEE.js.map → calibrate-6RRAILUY.js.map} +0 -0
  108. /package/dist/{capture-LRML2PKV.js.map → capture-RJPQ6B4N.js.map} +0 -0
  109. /package/dist/{check-ASPIHA6B.js.map → check-BQD64X3N.js.map} +0 -0
  110. /package/dist/{check-EDLBYL5N.js.map → check-ESNS7ZBC.js.map} +0 -0
  111. /package/dist/{check-HYNGXYJV.js.map → check-RQIIIURI.js.map} +0 -0
  112. /package/dist/{chunk-NXV7WE2D.js.map → chunk-3R5Y3XOL.js.map} +0 -0
  113. /package/dist/{chunk-A5ULMVHQ.js.map → chunk-3YHAYXBC.js.map} +0 -0
  114. /package/dist/{chunk-6OYEI2VV.js.map → chunk-52PDIZKV.js.map} +0 -0
  115. /package/dist/{chunk-3VK37OR2.js.map → chunk-7OQAU4EA.js.map} +0 -0
  116. /package/dist/{chunk-4IUC2MNL.js.map → chunk-7ZUCEMCY.js.map} +0 -0
  117. /package/dist/{chunk-MYMBCTDE.js.map → chunk-BOIF2XGG.js.map} +0 -0
  118. /package/dist/{chunk-MMTSSTG7.js.map → chunk-C6HZ6C5T.js.map} +0 -0
  119. /package/dist/{chunk-QQUKGCAJ.js.map → chunk-CP4ZM43V.js.map} +0 -0
  120. /package/dist/{chunk-YB4IBW3M.js.map → chunk-DX32P2DT.js.map} +0 -0
  121. /package/dist/{chunk-MWJLZMBL.js.map → chunk-F2OVHJNC.js.map} +0 -0
  122. /package/dist/{chunk-I2AH3CJJ.js.map → chunk-FLF6AECQ.js.map} +0 -0
  123. /package/dist/{chunk-GCQRPUSB.js.map → chunk-GAZFKZHM.js.map} +0 -0
  124. /package/dist/{chunk-QZ6LRSNV.js.map → chunk-HQFVJDEN.js.map} +0 -0
  125. /package/dist/{chunk-M3W7J6MU.js.map → chunk-HVFR66GQ.js.map} +0 -0
  126. /package/dist/{chunk-2B7ZNTPV.js.map → chunk-IA7BMIPE.js.map} +0 -0
  127. /package/dist/{chunk-PIKOVN6W.js.map → chunk-JQU7FHYH.js.map} +0 -0
  128. /package/dist/{chunk-JCDXE76J.js.map → chunk-MZC62HPZ.js.map} +0 -0
  129. /package/dist/{chunk-TK3FC4RF.js.map → chunk-NAZRLJAK.js.map} +0 -0
  130. /package/dist/{chunk-JXLTXMJ7.js.map → chunk-NGK3N6F2.js.map} +0 -0
  131. /package/dist/{chunk-PU6DFJMY.js.map → chunk-NN4ASVTY.js.map} +0 -0
  132. /package/dist/{chunk-DXNZS72W.js.map → chunk-QLUMKWQQ.js.map} +0 -0
  133. /package/dist/{chunk-EPH54CAY.js.map → chunk-QY3DOZMS.js.map} +0 -0
  134. /package/dist/{chunk-FBEQCK47.js.map → chunk-SJO2FTPG.js.map} +0 -0
  135. /package/dist/{chunk-PNPQTHBT.js.map → chunk-SNEHX4VE.js.map} +0 -0
  136. /package/dist/{chunk-SKSDRUUK.js.map → chunk-TA42STP7.js.map} +0 -0
  137. /package/dist/{chunk-IDV6HJCB.js.map → chunk-TCC737IS.js.map} +0 -0
  138. /package/dist/{chunk-QRWGPQKO.js.map → chunk-TVIX653Q.js.map} +0 -0
  139. /package/dist/{chunk-YTURO7SI.js.map → chunk-UK2I3ACY.js.map} +0 -0
  140. /package/dist/{chunk-7UKO3ASZ.js.map → chunk-XWFNO5PO.js.map} +0 -0
  141. /package/dist/{chunk-37LP5HMZ.js.map → chunk-YA63PVWI.js.map} +0 -0
  142. /package/dist/{chunk-UQISLDAV.js.map → chunk-ZX43SRZR.js.map} +0 -0
  143. /package/dist/{cli-MN7PYMBI.js.map → cli-6PGVHP7Y.js.map} +0 -0
  144. /package/dist/{cli-6UB5N6CU.js.map → cli-AAG3N4NB.js.map} +0 -0
  145. /package/dist/{cli-6HIDIFVT.js.map → cli-E3H6ZP4I.js.map} +0 -0
  146. /package/dist/{cli-VI7VHWNZ.js.map → cli-MHTYPVOD.js.map} +0 -0
  147. /package/dist/{cli-3BIBYHFK.js.map → cli-QKIJOXGW.js.map} +0 -0
  148. /package/dist/{cli-PYPX6O6E.js.map → cli-TYGOHJOJ.js.map} +0 -0
  149. /package/dist/{cli-673OTRGE.js.map → cli-YW6VWBHX.js.map} +0 -0
  150. /package/dist/{github-6QMYOHC3.js.map → github-3WMSTNTK.js.map} +0 -0
  151. /package/dist/{github-SXPGP5DB.js.map → github-OXSJQ4PS.js.map} +0 -0
  152. /package/dist/{json-SY7KLNC7.js.map → json-4DCKDGUV.js.map} +0 -0
  153. /package/dist/{lint-ASMJLLPA.js.map → lint-YDHVVZHT.js.map} +0 -0
  154. /package/dist/{list-KZNVTNVC.js.map → list-3ZO2NIYO.js.map} +0 -0
  155. /package/dist/{list-Q57YTFUQ.js.map → list-EJH6VAOW.js.map} +0 -0
  156. /package/dist/{page-KSGV5RD4.js.map → page-QEDB7EVU.js.map} +0 -0
  157. /package/dist/{pretty-A5EGTN35.js.map → pretty-5DGEBZSV.js.map} +0 -0
  158. /package/dist/{pretty-CXSDXNAS.js.map → pretty-SQX33UM2.js.map} +0 -0
  159. /package/dist/{reporters-HUY7UXFS.js.map → reporters-OHSI3RMV.js.map} +0 -0
  160. /package/dist/{reporters-K2VUKSAK.js.map → reporters-YELDDYJD.js.map} +0 -0
  161. /package/dist/{run-3DYJNRIG.js.map → run-IP6NZHAG.js.map} +0 -0
  162. /package/dist/{term-BIQUPMOR.js.map → term-2WMDZ4PY.js.map} +0 -0
  163. /package/dist/{validate-53RKM2XG.js.map → validate-G6HH3YXL.js.map} +0 -0
@@ -1,49 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- packageRoot
4
- } from "./chunk-TIUTIOFB.js";
3
+ LintError
4
+ } from "./chunk-YGFIQ6Y5.js";
5
5
  import {
6
- BLOCK_KINDS,
7
- BLOCK_KIND_NODE,
8
- LintError,
9
- checkContains,
10
- checkContainsIn,
11
- checkHeading,
12
- checkSequence,
13
- checkSequenceIn,
14
- isWildcard,
15
- matchSections,
16
- occurrenceRange
17
- } from "./chunk-CK7SKVHR.js";
18
- import {
19
- STDIN_TOKEN,
20
- assertNonEmpty,
21
6
  extractFrontmatter,
22
7
  extractorForExtension,
23
- gitignoreOptions,
24
- isErrorSeverity,
25
- locateFrontmatter,
26
- resolveTargetSet
27
- } from "./chunk-57NEQRVO.js";
28
- import {
29
- ditaOtHome,
30
- findConfigFile,
31
- readConfigFile,
32
- selectCollections,
33
- warn
34
- } from "./chunk-Q35YIDBV.js";
8
+ locateFrontmatter
9
+ } from "./chunk-527HHQPS.js";
35
10
  import {
36
- package_default
37
- } from "./chunk-UJCVAWP7.js";
38
- import {
39
- ToolError,
40
11
  errorMessage
41
12
  } from "./chunk-G3T47CFV.js";
42
13
 
43
- // src/lint/commands/lint.ts
44
- import { readFile as readFile3 } from "node:fs/promises";
45
- import { extname, relative, resolve as resolve2 } from "node:path";
46
-
47
14
  // src/lint/parsers/markdown.ts
48
15
  import { unified } from "unified";
49
16
  import remarkParse from "remark-parse";
@@ -309,6 +276,8 @@ function sectionize(fragments, docEnd) {
309
276
  order: 0,
310
277
  parentSlug: null,
311
278
  titlePosition: { ...fragment.position },
279
+ ...fragment.id !== void 0 ? { id: fragment.id } : {},
280
+ ...fragment.synthetic ? { synthetic: true } : {},
312
281
  position: { start: { ...fragment.position.start }, end: { ...fragment.position.end } },
313
282
  children: [],
314
283
  sections: []
@@ -354,7 +323,7 @@ function withMetadataTitle(fragments, metadata2, position) {
354
323
  if (title === null || title.length === 0) return fragments;
355
324
  if (position === null) return fragments;
356
325
  if (fragments.some((b) => b.type === "heading" && b.level === 1)) return fragments;
357
- return [{ type: "heading", level: 1, title, position }, ...fragments];
326
+ return [{ type: "heading", level: 1, title, position, synthetic: true }, ...fragments];
358
327
  }
359
328
  function withoutFence(content) {
360
329
  const loc = locateFrontmatter(content);
@@ -385,7 +354,9 @@ function parseWith(processor2, format, content, filePath) {
385
354
  sections: sectionize(
386
355
  withMetadataTitle(toFragments(tree), frontmatter, metaPosition),
387
356
  documentEnd(tree)
388
- )
357
+ ),
358
+ // Markdown's links are read from mdast by `manni graph` itself.
359
+ links: []
389
360
  };
390
361
  }
391
362
  var SHARED_KINDS = [
@@ -473,16 +444,18 @@ function positionOf2(node) {
473
444
  end: { line: loc.endLine, column: loc.endCol, offset: loc.endOffset }
474
445
  };
475
446
  }
476
- function rawText(node) {
447
+ function rawText(node, skipHref) {
477
448
  if (defaultTreeAdapter.isTextNode(node)) return node.value;
478
449
  if (!defaultTreeAdapter.isElementNode(node)) return "";
479
- if (NON_TEXT_TAGS.has(tagOf(node))) return "";
450
+ const tag = tagOf(node);
451
+ if (NON_TEXT_TAGS.has(tag)) return "";
452
+ if (skipHref !== void 0 && tag === "a" && attrOf(node, "href") === skipHref) return "";
480
453
  let out = "";
481
- for (const child of node.childNodes) out += rawText(child);
454
+ for (const child of node.childNodes) out += rawText(child, skipHref);
482
455
  return out;
483
456
  }
484
- function flatText(node) {
485
- return rawText(node).replace(/\s+/g, " ").trim();
457
+ function flatText(node, skipHref) {
458
+ return rawText(node, skipHref).replace(/\s+/g, " ").trim();
486
459
  }
487
460
  function langOf(el) {
488
461
  const className = attrOf(el, "class");
@@ -672,16 +645,16 @@ function listItems(list) {
672
645
  function childContent2(parent) {
673
646
  const out = [];
674
647
  let text = "";
675
- let span2 = null;
648
+ let span = null;
676
649
  const extend = (node) => {
677
650
  const at = positionOf2(node);
678
- span2 = span2 === null ? at : { start: span2.start, end: at.end };
651
+ span = span === null ? at : { start: span.start, end: at.end };
679
652
  };
680
653
  const flush = () => {
681
654
  const value = text.trim();
682
- const at = span2;
655
+ const at = span;
683
656
  text = "";
684
- span2 = null;
657
+ span = null;
685
658
  if (value.length === 0) return;
686
659
  out.push({ kind: "paragraph", position: at ?? positionOf2(parent), text: value });
687
660
  };
@@ -707,9 +680,9 @@ function childContent2(parent) {
707
680
  out.push(...nested);
708
681
  continue;
709
682
  }
710
- const inline = flatText(child);
711
- text += inline;
712
- if (inline.trim().length > 0) extend(child);
683
+ const inline2 = flatText(child);
684
+ text += inline2;
685
+ if (inline2.trim().length > 0) extend(child);
713
686
  }
714
687
  flush();
715
688
  return out;
@@ -760,17 +733,21 @@ function contentIn(parent) {
760
733
  }
761
734
  return out;
762
735
  }
763
- function walk(parent, out) {
736
+ function walk(parent, out, ids) {
764
737
  for (const child of parent.childNodes) {
765
738
  if (!defaultTreeAdapter.isElementNode(child)) continue;
766
739
  const tag = tagOf(child);
767
740
  const role = roleOf(child, tag);
768
741
  if (role === "heading") {
742
+ const id = ids.claim(child);
769
743
  out.push({
770
744
  type: "heading",
771
745
  level: HEADING_LEVELS.get(tag) ?? 1,
772
- title: flatText(child),
773
- position: positionOf2(child)
746
+ // The self-permalink is dropped from the title as it is from the links:
747
+ // Sphinx's `¶` is a widget, and "Install the SDK¶" is no reader's title.
748
+ title: flatText(child, id !== void 0 ? `#${id}` : void 0),
749
+ position: positionOf2(child),
750
+ ...id !== void 0 ? { id } : {}
774
751
  });
775
752
  continue;
776
753
  }
@@ -780,7 +757,44 @@ function walk(parent, out) {
780
757
  continue;
781
758
  }
782
759
  if (role === "opaque") continue;
783
- walk(child, out);
760
+ if (ID_CARRIERS.has(tag)) {
761
+ ids.enter(child);
762
+ walk(child, out, ids);
763
+ ids.leave();
764
+ continue;
765
+ }
766
+ walk(child, out, ids);
767
+ }
768
+ }
769
+ var ID_CARRIERS = /* @__PURE__ */ new Set(["section", "article"]);
770
+ var HeadingIds = class {
771
+ open = [];
772
+ byHeading = /* @__PURE__ */ new Map();
773
+ enter(el) {
774
+ this.open.push({ id: attrOf(el, "id"), spent: false });
775
+ }
776
+ leave() {
777
+ this.open.pop();
778
+ }
779
+ claim(heading) {
780
+ const nearest = this.open.at(-1);
781
+ const inherited = nearest && !nearest.spent ? nearest.id : void 0;
782
+ for (const carrier of this.open) carrier.spent = true;
783
+ const id = attrOf(heading, "id") ?? inherited;
784
+ if (id !== void 0) this.byHeading.set(heading, id);
785
+ return id;
786
+ }
787
+ };
788
+ function collectLinks(parent, ids, heading, out) {
789
+ for (const child of parent.childNodes) {
790
+ if (!defaultTreeAdapter.isElementNode(child)) continue;
791
+ const tag = tagOf(child);
792
+ if (tag === "a" || tag === "area") {
793
+ const href = attrOf(child, "href");
794
+ const permalink = heading !== void 0 && href === `#${heading}`;
795
+ if (href !== void 0 && !permalink) out.push({ target: href, position: positionOf2(child) });
796
+ }
797
+ collectLinks(child, ids, HEADING_LEVELS.has(tag) ? ids.get(child) : heading, out);
784
798
  }
785
799
  }
786
800
  function documentEnd2(content) {
@@ -817,7 +831,10 @@ function findHead(doc) {
817
831
  function parseHtml(content, filePath) {
818
832
  const doc = parse(content, { sourceCodeLocationInfo: true });
819
833
  const fragments = [];
820
- walk(doc, fragments);
834
+ const ids = new HeadingIds();
835
+ walk(doc, fragments, ids);
836
+ const links = [];
837
+ collectLinks(doc, ids.byHeading, void 0, links);
821
838
  const { frontmatter, position } = metadataOf(content, filePath, doc);
822
839
  return {
823
840
  format: "html",
@@ -827,7 +844,8 @@ function parseHtml(content, filePath) {
827
844
  sections: sectionize(
828
845
  withMetadataTitle(fragments, frontmatter, position),
829
846
  documentEnd2(content)
830
- )
847
+ ),
848
+ links
831
849
  };
832
850
  }
833
851
  var htmlParser = {
@@ -1235,11 +1253,13 @@ function scanRange(src, levels, from, to, base, headings) {
1235
1253
  if (headings) {
1236
1254
  const hit = titleAt(src, i, to);
1237
1255
  if (hit) {
1256
+ const id = labelAbove(src, hit.first);
1238
1257
  fragments.push({
1239
1258
  type: "heading",
1240
1259
  level: levelFor(levels, hit.style),
1241
1260
  title: hit.title,
1242
- position: spanOf(src, hit.first, hit.last)
1261
+ position: spanOf(src, hit.first, hit.last),
1262
+ ...id !== void 0 ? { id } : {}
1243
1263
  });
1244
1264
  i = hit.last + 1;
1245
1265
  continue;
@@ -1568,6 +1588,106 @@ function readParagraph(src, levels, fragments, from, to, base, headings) {
1568
1588
  });
1569
1589
  return Math.max(blockEnd, end);
1570
1590
  }
1591
+ var TARGET = /^[ \t]*\.\.[ \t]+_(?:`([^`]+)`|([^:`]+)):(?:[ \t]+(.*?))?[ \t]*$/;
1592
+ function targetAt(src, line) {
1593
+ const match = TARGET.exec(src.text[line] ?? "");
1594
+ const name = (match?.[1] ?? match?.[2] ?? "").trim();
1595
+ if (!match || name === "" || name === "_") return null;
1596
+ let uri = match[3] ?? "";
1597
+ const base = indentOf(src.text[line] ?? "");
1598
+ for (let next = line + 1; next < src.text.length; next++) {
1599
+ const text = src.text[next] ?? "";
1600
+ if (isBlank(text) || indentOf(text) <= base) break;
1601
+ uri += text;
1602
+ }
1603
+ return { name, uri: uri.replace(/\s+/g, "") };
1604
+ }
1605
+ var refName = (name) => name.replace(/\s+/g, " ").trim().toLowerCase();
1606
+ function labelAbove(src, first) {
1607
+ let line = first - 1;
1608
+ while (line >= 0 && isBlank(src.text[line])) line--;
1609
+ const target = line >= 0 ? targetAt(src, line) : null;
1610
+ return target && target.uri === "" ? target.name : void 0;
1611
+ }
1612
+ function codeLines(nodes, out) {
1613
+ for (const node of nodes) {
1614
+ if (node.kind === "codeBlock") {
1615
+ for (let l = node.position.start.line; l <= node.position.end.line; l++) out.add(l);
1616
+ } else if (node.kind === "list") {
1617
+ codeLines(node.items, out);
1618
+ } else if (node.kind === "definitionItem") {
1619
+ codeLines(node.definition, out);
1620
+ } else if ("children" in node) {
1621
+ codeLines(node.children, out);
1622
+ }
1623
+ }
1624
+ return out;
1625
+ }
1626
+ var blank = (text, index, length) => text.slice(0, index) + " ".repeat(length) + text.slice(index + length);
1627
+ var INLINE_LITERAL = /``.+?``/g;
1628
+ var DOC_ROLE = /:doc:`([^`]*)`/g;
1629
+ var ROLE = /:[\w.+:-]+:`[^`]*`/g;
1630
+ var EMBEDDED = /`([^`<]*?)\s*<([^<>`]+)>`(__?)/g;
1631
+ var PHRASE_REF = /`([^`]+)`_(?!_)/g;
1632
+ var SIMPLE_REF = /(?<![\w\\])([A-Za-z0-9]+(?:[-_.:+][A-Za-z0-9]+)*)_(?!\w)/g;
1633
+ function collectLinks2(src, from, code) {
1634
+ const targets = /* @__PURE__ */ new Map();
1635
+ for (let line = from; line < src.text.length; line++) {
1636
+ const target = targetAt(src, line);
1637
+ if (target && target.uri !== "") targets.set(refName(target.name), target.uri);
1638
+ }
1639
+ for (let line = from; line < src.text.length; line++) {
1640
+ for (const m of (src.text[line] ?? "").matchAll(EMBEDDED)) {
1641
+ const name = refName(m[1] ?? "");
1642
+ if (m[3] === "_" && name !== "" && !targets.has(name)) {
1643
+ targets.set(name, (m[2] ?? "").replace(/\s+/g, ""));
1644
+ }
1645
+ }
1646
+ }
1647
+ const resolve = (name) => {
1648
+ let uri = targets.get(refName(name));
1649
+ for (let hops = 0; uri?.endsWith("_") && hops < targets.size; hops++) {
1650
+ uri = targets.get(refName(uri.slice(0, -1)));
1651
+ }
1652
+ return uri?.endsWith("_") ? void 0 : uri;
1653
+ };
1654
+ const links = [];
1655
+ for (let line = from; line < src.text.length; line++) {
1656
+ let text = src.text[line] ?? "";
1657
+ if (code.has(line + 1) || TARGET.test(text)) continue;
1658
+ const hits = [];
1659
+ const take = (pattern, toTarget) => {
1660
+ for (const m of text.matchAll(pattern)) {
1661
+ const target = toTarget(m);
1662
+ if (target) hits.push({ column: m.index + 1, length: m[0].length, target });
1663
+ text = blank(text, m.index, m[0].length);
1664
+ }
1665
+ };
1666
+ take(INLINE_LITERAL, () => void 0);
1667
+ take(DOC_ROLE, (m) => {
1668
+ const inner = (m[1] ?? "").trim();
1669
+ return /<([^<>]+)>$/.exec(inner)?.[1]?.trim() ?? inner;
1670
+ });
1671
+ take(ROLE, () => void 0);
1672
+ take(EMBEDDED, (m) => {
1673
+ const uri = (m[2] ?? "").replace(/\s+/g, "");
1674
+ return uri.endsWith("_") ? resolve(uri.slice(0, -1)) : uri;
1675
+ });
1676
+ take(PHRASE_REF, (m) => resolve(m[1] ?? ""));
1677
+ take(SIMPLE_REF, (m) => resolve(m[1] ?? ""));
1678
+ hits.sort((a, b) => a.column - b.column);
1679
+ for (const hit of hits) {
1680
+ links.push({
1681
+ target: hit.target,
1682
+ position: {
1683
+ start: pointAt(src, line, hit.column),
1684
+ end: pointAt(src, line, hit.column + hit.length)
1685
+ }
1686
+ });
1687
+ }
1688
+ }
1689
+ return links;
1690
+ }
1571
1691
  function docinfoPosition(src) {
1572
1692
  const limit = src.text.length;
1573
1693
  let i = 0;
@@ -1647,6 +1767,14 @@ var rstParser = {
1647
1767
  sections: sectionize(
1648
1768
  withMetadataTitle(fragments, frontmatter, position),
1649
1769
  src.end
1770
+ ),
1771
+ links: collectLinks2(
1772
+ src,
1773
+ Math.max(bodyStart, 0),
1774
+ codeLines(
1775
+ fragments.flatMap((f) => f.type === "content" ? [f.node] : []),
1776
+ /* @__PURE__ */ new Set()
1777
+ )
1650
1778
  )
1651
1779
  };
1652
1780
  }
@@ -1719,7 +1847,12 @@ var DITA = {
1719
1847
  definitionLists: ["dl"],
1720
1848
  definitionItems: ["dlentry"],
1721
1849
  definitionTerms: ["dt"],
1722
- definitionBodies: ["dd"]
1850
+ definitionBodies: ["dd"],
1851
+ // `keyref`, `conkeyref` and `conref` are not here: a key resolves only
1852
+ // against a map at build time, and a conref pulls content in rather than
1853
+ // pointing a reader anywhere.
1854
+ links: [{ elements: ["xref", "link"], attribute: "href" }],
1855
+ ditaHrefs: true
1723
1856
  };
1724
1857
  var DOCBOOK = {
1725
1858
  name: "docbook",
@@ -1784,34 +1917,40 @@ var DOCBOOK = {
1784
1917
  definitionTerms: ["term"],
1785
1918
  // DocBook reuses `<listitem>` for a variable list's own body, distinct from
1786
1919
  // the `listItems` bucket above which reads it inside `<itemizedlist>`.
1787
- definitionBodies: ["listitem"]
1920
+ definitionBodies: ["listitem"],
1921
+ links: [
1922
+ { elements: ["link"], attribute: "href" },
1923
+ { elements: ["ulink"], attribute: "url" },
1924
+ { elements: ["xref", "link"], attribute: "linkend", idref: true }
1925
+ ]
1788
1926
  };
1927
+ var MAP_ENTRIES = [
1928
+ "map",
1929
+ "bookmap",
1930
+ "topicref",
1931
+ "topichead",
1932
+ "mapref",
1933
+ "glossref",
1934
+ "anchorref",
1935
+ "navref",
1936
+ "topicset",
1937
+ "topicsetref",
1938
+ "chapter",
1939
+ "part",
1940
+ "appendix",
1941
+ "appendices",
1942
+ "preface",
1943
+ "notices",
1944
+ "dedication",
1945
+ "colophon",
1946
+ "amendments"
1947
+ ];
1789
1948
  var DITAMAP = {
1790
1949
  name: "ditamap",
1791
1950
  label: "DITA map",
1792
1951
  namespaces: [],
1793
1952
  roots: ["map", "bookmap"],
1794
- sections: [
1795
- "map",
1796
- "bookmap",
1797
- "topicref",
1798
- "topichead",
1799
- "mapref",
1800
- "glossref",
1801
- "anchorref",
1802
- "navref",
1803
- "topicset",
1804
- "topicsetref",
1805
- "chapter",
1806
- "part",
1807
- "appendix",
1808
- "appendices",
1809
- "preface",
1810
- "notices",
1811
- "dedication",
1812
- "colophon",
1813
- "amendments"
1814
- ],
1953
+ sections: MAP_ENTRIES,
1815
1954
  // `navtitle` sits in both buckets on purpose, because DITA spells it either
1816
1955
  // way. The element form wins: `headingOf` asks `titleOf` first, so
1817
1956
  // `@navtitle` is read only when the entry has no `<topicmeta><navtitle>`.
@@ -1831,7 +1970,12 @@ var DITAMAP = {
1831
1970
  codeLangAttributes: [],
1832
1971
  unorderedLists: [],
1833
1972
  orderedLists: [],
1834
- listItems: []
1973
+ listItems: [],
1974
+ // Every navigation entry's `href`. `<keydef>` is not an entry - it binds a
1975
+ // key to a resource and never appears in a table of contents - so its
1976
+ // `href` is not a link, for the reason it is not a section.
1977
+ links: [{ elements: MAP_ENTRIES, attribute: "href" }],
1978
+ ditaHrefs: true
1835
1979
  };
1836
1980
  var XML_VOCABULARIES = [
1837
1981
  DITA,
@@ -1865,6 +2009,11 @@ function compile(vocab) {
1865
2009
  definitionItems: lower(vocab.definitionItems),
1866
2010
  definitionTerms: lower(vocab.definitionTerms),
1867
2011
  definitionBodies: lower(vocab.definitionBodies),
2012
+ links: (vocab.links ?? []).map((l) => ({
2013
+ elements: lower(l.elements),
2014
+ attribute: l.attribute.toLowerCase(),
2015
+ idref: l.idref ?? false
2016
+ })),
1868
2017
  known: /* @__PURE__ */ new Set()
1869
2018
  };
1870
2019
  for (const set of [
@@ -1907,6 +2056,19 @@ function* elementChildren2(el) {
1907
2056
  if (child.nodeType === ELEMENT_NODE) yield child;
1908
2057
  }
1909
2058
  }
2059
+ function attributeByLocalName(el, name) {
2060
+ for (let i = 0; i < el.attributes.length; i++) {
2061
+ const attr = el.attributes.item(i);
2062
+ if (attr && localName(attr) === name) return attr.value;
2063
+ }
2064
+ return null;
2065
+ }
2066
+ function ditaTarget(href) {
2067
+ if (/^[a-z][a-z0-9+.-]*:/i.test(href)) return href;
2068
+ const hash = href.indexOf("#");
2069
+ const slash = href.lastIndexOf("/");
2070
+ return hash === -1 || slash < hash ? href : href.slice(0, hash + 1) + href.slice(slash + 1);
2071
+ }
1910
2072
  function findDirectChild(el, names) {
1911
2073
  for (const child of elementChildren2(el)) {
1912
2074
  if (names.has(localName(child))) return child;
@@ -1942,7 +2104,7 @@ var NAMESPACE_MATCH = 1e3;
1942
2104
  var EXCLUSIVE_ELEMENT = 10;
1943
2105
  var ROOT_NAME = 5;
1944
2106
  var SHARED_ELEMENT = 1;
1945
- function chooseVocabulary(root, compiled3) {
2107
+ function chooseVocabulary(root, compiled) {
1946
2108
  const counts = /* @__PURE__ */ new Map();
1947
2109
  const tally = (node) => {
1948
2110
  for (const child of elementChildren2(node)) {
@@ -1954,14 +2116,14 @@ function chooseVocabulary(root, compiled3) {
1954
2116
  counts.set(localName(root), 1);
1955
2117
  tally(root);
1956
2118
  const claims = /* @__PURE__ */ new Map();
1957
- for (const c of compiled3) {
2119
+ for (const c of compiled) {
1958
2120
  for (const name of c.known) claims.set(name, (claims.get(name) ?? 0) + 1);
1959
2121
  }
1960
2122
  const ns = (root.namespaceURI ?? "").toLowerCase();
1961
2123
  const rootName = localName(root);
1962
2124
  let best;
1963
2125
  let bestScore = -1;
1964
- for (const c of compiled3) {
2126
+ for (const c of compiled) {
1965
2127
  let score = 0;
1966
2128
  if (ns && c.vocab.namespaces.some((u) => u.toLowerCase() === ns)) {
1967
2129
  score += NAMESPACE_MATCH;
@@ -2104,10 +2266,12 @@ var Flattener = class {
2104
2266
  return;
2105
2267
  }
2106
2268
  const next = Math.min(level + 1, 6);
2269
+ const id = attributeByLocalName(el, "id");
2107
2270
  this.fragments.push({
2108
2271
  type: "heading",
2109
2272
  level: next,
2110
2273
  title: heading.title,
2274
+ ...id ? { id } : {},
2111
2275
  // From the container's start tag through the end of its title: the
2112
2276
  // container is the section, so a section that began at its `<title>`
2113
2277
  // would leave its own opening tag outside itself and stop adjacent
@@ -2124,6 +2288,23 @@ var Flattener = class {
2124
2288
  const node = this.content(el);
2125
2289
  if (node) this.fragments.push({ type: "content", node });
2126
2290
  }
2291
+ /**
2292
+ * Every link target under `el`, in document order. A walk of its own rather
2293
+ * than part of `visit`, because `visit` skips unmapped subtrees and
2294
+ * `<related-links>` is one of them.
2295
+ */
2296
+ links(el, out = []) {
2297
+ const name = localName(el);
2298
+ for (const source of this.c.links) {
2299
+ if (!source.elements.has(name)) continue;
2300
+ const value = attributeByLocalName(el, source.attribute);
2301
+ if (!value) continue;
2302
+ const target = source.idref ? `#${value}` : this.c.vocab.ditaHrefs ? ditaTarget(value) : value;
2303
+ out.push({ target, position: this.span(el) });
2304
+ }
2305
+ for (const child of elementChildren2(el)) this.links(child, out);
2306
+ return out;
2307
+ }
2127
2308
  walkChildren(el, level) {
2128
2309
  for (const child of elementChildren2(el)) this.visit(child, level);
2129
2310
  }
@@ -2423,12 +2604,12 @@ function readMetadata2(content, filePath) {
2423
2604
  function parseXml(content, filePath, vocabularies = XML_VOCABULARIES) {
2424
2605
  const map = new SourceMap(content);
2425
2606
  const root = parseDocument(map.text, filePath);
2426
- const compiled3 = vocabularies.map(compile);
2427
- if (compiled3.length === 0) {
2607
+ const compiled = vocabularies.map(compile);
2608
+ if (compiled.length === 0) {
2428
2609
  throw new LintError(`${filePath}: no XML vocabularies are configured.`);
2429
2610
  }
2430
- const { best, score } = chooseVocabulary(root, compiled3);
2431
- const names = compiled3.map((c) => c.vocab.label).join(", ");
2611
+ const { best, score } = chooseVocabulary(root, compiled);
2612
+ const names = compiled.map((c) => c.vocab.label).join(", ");
2432
2613
  if (score <= 0) {
2433
2614
  throw new LintError(
2434
2615
  `${filePath}: no known XML vocabulary matched <${localName(root)}>. manni lint understands ${names}; add an entry to XML_VOCABULARIES in src/parsers/xml.ts to describe another schema.`
@@ -2453,7 +2634,8 @@ function parseXml(content, filePath, vocabularies = XML_VOCABULARIES) {
2453
2634
  start: map.point(root) ?? { ...map.docEnd },
2454
2635
  end: map.point(root.firstChild) ?? map.endOfLine(map.point(root)?.line ?? 1)
2455
2636
  } : null,
2456
- sections: sectionize(fragments, map.docEnd)
2637
+ sections: sectionize(fragments, map.docEnd),
2638
+ links: flattener.links(root)
2457
2639
  };
2458
2640
  }
2459
2641
  var xmlParser = {
@@ -2560,7 +2742,10 @@ var NAMED_ENTITIES = {
2560
2742
  };
2561
2743
  function flattenInline2(html) {
2562
2744
  if (!html) return "";
2563
- return html.replace(/<[^>]*>/g, "").replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(Number(dec))).replace(
2745
+ return decodeEntities(html.replace(/<[^>]*>/g, ""));
2746
+ }
2747
+ function decodeEntities(text) {
2748
+ return text.replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(Number(dec))).replace(
2564
2749
  /&#x([0-9a-f]+);/gi,
2565
2750
  (_, hex) => String.fromCodePoint(Number.parseInt(hex, 16))
2566
2751
  ).replace(
@@ -2568,6 +2753,21 @@ function flattenInline2(html) {
2568
2753
  (whole, name) => NAMED_ENTITIES[name.toLowerCase()] ?? whole
2569
2754
  );
2570
2755
  }
2756
+ var harvested = [];
2757
+ var ANCHOR = /<a\s([^>]*)>/gi;
2758
+ var HREF = /(?:^|\s)href="([^"]*)"/;
2759
+ var CLASS = /(?:^|\s)class="([^"]*)"/;
2760
+ function inline(html, line) {
2761
+ for (const match of (html ?? "").matchAll(ANCHOR)) {
2762
+ const attrs = match[1] ?? "";
2763
+ const href = HREF.exec(attrs)?.[1];
2764
+ if (href === void 0) continue;
2765
+ const classes = (CLASS.exec(attrs)?.[1] ?? "").split(/\s+/);
2766
+ if (classes.includes("include") || classes.includes("footnote")) continue;
2767
+ harvested.push({ target: decodeEntities(href), line });
2768
+ }
2769
+ return flattenInline2(html);
2770
+ }
2571
2771
  function stringAttribute(node, name) {
2572
2772
  const value = node.getAttribute?.(name);
2573
2773
  return typeof value === "string" && value.length > 0 ? value : void 0;
@@ -2578,7 +2778,7 @@ function draftList(node, ordered) {
2578
2778
  const item = asItem(raw);
2579
2779
  if (!item) continue;
2580
2780
  const line = lineOf(item);
2581
- const text = flattenInline2(item.getText());
2781
+ const text = inline(item.getText(), line);
2582
2782
  const children = [];
2583
2783
  if (text.length > 0) children.push({ kind: "paragraph", line, text });
2584
2784
  collect(
@@ -2608,7 +2808,7 @@ function admonitionVariant(node) {
2608
2808
  function draftAdmonition(node, variant) {
2609
2809
  const children = draftChildren(node);
2610
2810
  if (children.length === 0) {
2611
- const text = flattenInline2(node.getContent?.());
2811
+ const text = inline(node.getContent?.(), lineOf(node));
2612
2812
  if (text.length > 0) children.push({ kind: "paragraph", line: lineOf(node), text });
2613
2813
  }
2614
2814
  return { kind: "admonition", line: lineOf(node), variant, children };
@@ -2616,7 +2816,7 @@ function draftAdmonition(node, variant) {
2616
2816
  function draftBlockquote(node) {
2617
2817
  const children = draftChildren(node);
2618
2818
  if (children.length === 0) {
2619
- const text = flattenInline2(node.getContent?.());
2819
+ const text = inline(node.getContent?.(), lineOf(node));
2620
2820
  if (text.length > 0) children.push({ kind: "paragraph", line: lineOf(node), text });
2621
2821
  }
2622
2822
  return { kind: "blockquote", line: lineOf(node), children };
@@ -2626,10 +2826,10 @@ function draftTable(node) {
2626
2826
  const toRow = (header) => (cells) => ({
2627
2827
  line: cells[0]?.getLineNumber?.() ?? lineOf(node),
2628
2828
  header,
2629
- cells: cells.map((cell) => ({
2630
- line: cell.getLineNumber?.() ?? lineOf(node),
2631
- text: flattenInline2(cell.getText())
2632
- }))
2829
+ cells: cells.map((cell) => {
2830
+ const line = cell.getLineNumber?.() ?? lineOf(node);
2831
+ return { line, text: inline(cell.getText(), line) };
2832
+ })
2633
2833
  });
2634
2834
  return {
2635
2835
  kind: "table",
@@ -2652,7 +2852,7 @@ function draftDefinitionList(node) {
2652
2852
  const children = [];
2653
2853
  const desc = asItem(rawDesc);
2654
2854
  if (desc) {
2655
- const text = flattenInline2(desc.getText());
2855
+ const text = inline(desc.getText(), lineOf(desc));
2656
2856
  if (text.length > 0) children.push({ kind: "paragraph", line: lineOf(desc), text });
2657
2857
  collect(
2658
2858
  desc.getBlocks().map(asNode).filter((n) => n !== null),
@@ -2661,7 +2861,7 @@ function draftDefinitionList(node) {
2661
2861
  }
2662
2862
  items.push({
2663
2863
  line: lineOf(first),
2664
- term: terms.map((t) => flattenInline2(t.getText())).join(", "),
2864
+ term: terms.map((t) => inline(t.getText(), lineOf(t))).join(", "),
2665
2865
  children
2666
2866
  });
2667
2867
  }
@@ -2673,7 +2873,7 @@ function draftOf(node) {
2673
2873
  return {
2674
2874
  kind: "paragraph",
2675
2875
  line: lineOf(node),
2676
- text: flattenInline2(node.getContent?.())
2876
+ text: inline(node.getContent?.(), lineOf(node))
2677
2877
  };
2678
2878
  case "listing":
2679
2879
  case "literal": {
@@ -2700,7 +2900,7 @@ function draftOf(node) {
2700
2900
  case "image": {
2701
2901
  const url = stringAttribute(node, "target");
2702
2902
  if (!url) return null;
2703
- const title = flattenInline2(node.getTitle?.());
2903
+ const title = inline(node.getTitle?.(), lineOf(node));
2704
2904
  return {
2705
2905
  kind: "image",
2706
2906
  line: lineOf(node),
@@ -2721,13 +2921,15 @@ function collect(nodes, out) {
2721
2921
  for (const node of nodes) {
2722
2922
  const context = node.getContext();
2723
2923
  if (context === "section") {
2924
+ const id = node.getId?.();
2724
2925
  out.push({
2725
2926
  kind: "heading",
2726
2927
  line: lineOf(node),
2727
2928
  // Asciidoctor's `=` title is level 0, so every level is one shallower
2728
2929
  // than the heading level the rest of the tool speaks in.
2729
2930
  level: (node.getLevel?.() ?? 0) + 1,
2730
- title: flattenInline2(node.getTitle?.())
2931
+ title: inline(node.getTitle?.(), lineOf(node)),
2932
+ ...id ? { id } : {}
2731
2933
  });
2732
2934
  collect(childrenOf(node), out);
2733
2935
  continue;
@@ -2749,6 +2951,7 @@ function place(drafts, end, index) {
2749
2951
  type: "heading",
2750
2952
  level: draft.level,
2751
2953
  title: draft.title,
2954
+ ...draft.id !== void 0 ? { id: draft.id } : {},
2752
2955
  position: {
2753
2956
  start: index.start(draft.line),
2754
2957
  end: index.endOfLine(draft.line)
@@ -2922,9 +3125,18 @@ function load(content, filePath) {
2922
3125
  // Without this every block's `getLineNumber()` is undefined and there are
2923
3126
  // no positions to derive at all.
2924
3127
  sourcemap: true,
2925
- // A leading `---` fence is a thematic break in AsciiDoc; this consumes it
2926
- // as metadata instead, without disturbing line numbering.
2927
- attributes: { "skip-front-matter": true }
3128
+ // Asciidoctor.js's default already, pinned because it is what keeps
3129
+ // `include::` from reading the disk: a refused include renders as a link
3130
+ // to its target, which `inline` then drops.
3131
+ safe: "secure",
3132
+ attributes: {
3133
+ // A leading `---` fence is a thematic break in AsciiDoc; this consumes
3134
+ // it as metadata instead, without disturbing line numbering.
3135
+ "skip-front-matter": true,
3136
+ // `xref:other.adoc[]` keeps its source suffix rather than becoming
3137
+ // `other.html`, so a harvested link names the file that exists.
3138
+ relfilesuffix: ".adoc"
3139
+ }
2928
3140
  });
2929
3141
  } catch (err) {
2930
3142
  throw new LintError(
@@ -2936,7 +3148,7 @@ function headerDraft(doc) {
2936
3148
  if (!doc.hasHeader()) return null;
2937
3149
  const header = asNode(doc.getHeader());
2938
3150
  if (!header) return null;
2939
- const title = flattenInline2(header.getTitle?.());
3151
+ const title = inline(header.getTitle?.(), lineOf(header));
2940
3152
  if (title.length === 0) return null;
2941
3153
  return {
2942
3154
  kind: "heading",
@@ -2948,10 +3160,15 @@ function headerDraft(doc) {
2948
3160
  function parse2(content, filePath) {
2949
3161
  const doc = load(content, filePath);
2950
3162
  const index = lineIndex(content);
3163
+ harvested = [];
2951
3164
  const drafts = [];
2952
3165
  const header = headerDraft(doc);
2953
3166
  if (header) drafts.push(header);
2954
3167
  collect(childrenOf(doc), drafts);
3168
+ const links = harvested.sort((a, b) => a.line - b.line).map(({ target, line }) => ({
3169
+ target,
3170
+ position: { start: index.start(line), end: index.endOfLine(line) }
3171
+ }));
2955
3172
  const meta = metadata(content, filePath, index);
2956
3173
  const end = index.documentEnd();
2957
3174
  return {
@@ -2962,7 +3179,8 @@ function parse2(content, filePath) {
2962
3179
  sections: sectionize(
2963
3180
  withMetadataTitle(place(drafts, end, index), meta.data, meta.position),
2964
3181
  end
2965
- )
3182
+ ),
3183
+ links
2966
3184
  };
2967
3185
  }
2968
3186
  var asciidocParser = {
@@ -3010,6 +3228,9 @@ function parserByName(name) {
3010
3228
  function supportedExtensions() {
3011
3229
  return PARSERS.flatMap((p) => p.walkExtensions ?? p.extensions);
3012
3230
  }
3231
+ function unsupportedFormatMessage(name) {
3232
+ return `no parser is registered for "${name}". Supported extensions: ${supportedExtensions().join(", ")}. Use --as to override.`;
3233
+ }
3013
3234
  function listFormats() {
3014
3235
  return PARSERS.map((p) => ({
3015
3236
  name: p.name,
@@ -3019,2422 +3240,11 @@ function listFormats() {
3019
3240
  }));
3020
3241
  }
3021
3242
 
3022
- // src/lint/core/template-registry.ts
3023
- import { readFile } from "node:fs/promises";
3024
- import { dirname, isAbsolute, join, resolve as resolvePath } from "node:path";
3025
- import { parse as parseYaml } from "yaml";
3026
- import { dereference } from "@apidevtools/json-schema-ref-parser";
3027
- import * as AjvNs from "ajv";
3028
-
3029
- // schemas/lint/template.json
3030
- var template_default = {
3031
- $schema: "http://json-schema.org/draft-07/schema#",
3032
- $id: "manni-lint:template:2",
3033
- title: "manni lint template file",
3034
- description: "A template file: named doctype templates, plus the reusable fragments they point at with `$ref`. Validated after dereferencing, so no `$ref` survives to be seen here. A template is itself a rule: it describes the page, and it takes every rule key plus `title`, `types` and `extends`.",
3035
- type: "object",
3036
- additionalProperties: false,
3037
- required: ["templates"],
3038
- properties: {
3039
- $schema: {
3040
- description: "URL of this schema, so an editor can offer completion. Read by editors, never by the linter.",
3041
- type: "string"
3042
- },
3043
- info: {
3044
- description: "Free-form metadata about the file (title, version, author). Never read by the linter; declared so a file that carries it is not rejected.",
3045
- type: "object"
3046
- },
3047
- templates: {
3048
- description: "The doctype templates this file defines, keyed by name.",
3049
- type: "object",
3050
- additionalProperties: false,
3051
- patternProperties: {
3052
- "^[A-Za-z][A-Za-z0-9-_]*$": { $ref: "#/definitions/template" }
3053
- }
3054
- },
3055
- components: {
3056
- description: "Reusable fragments, targeted by `$ref` from elsewhere in the file. Left unconstrained: a fragment is validated where it is inlined, not where it is declared.",
3057
- type: "object"
3058
- }
3059
- },
3060
- definitions: {
3061
- min: {
3062
- description: "Fewest occurrences. Absent means one. Deliberately carries no JSON Schema `default`, and neither does any other key in this file: Ajv's `useDefaults` would write one in at load time, and a written-in value is indistinguishable from a value the author typed, so it wins the `extends` merge against the parent's real value. The code reads `undefined` instead.",
3063
- type: "integer",
3064
- minimum: 0
3065
- },
3066
- max: {
3067
- description: "Most occurrences. Absent is unbounded, so write `max: 1` for exactly once and `max: 0` to forbid. Must be at least `min` where both are written; the loader checks that, because JSON Schema cannot compare two siblings.",
3068
- type: "integer",
3069
- minimum: 0
3070
- },
3071
- id: {
3072
- description: "Handle for this rule, unique among its siblings. It appears in messages, and `extends` targets it to replace one inherited rule in place.",
3073
- type: "string",
3074
- pattern: "^[A-Za-z][A-Za-z0-9-_]*$"
3075
- },
3076
- description: {
3077
- description: "Prose description of what the rule is for. Never checked.",
3078
- type: "string"
3079
- },
3080
- heading: {
3081
- description: "The heading of the section this rule describes. Absent means any heading, or none. A string is exact text, a list is one-of, `{ pattern }` is an unanchored regular expression, and `false` means the section has no heading of its own.",
3082
- oneOf: [
3083
- { type: "string", minLength: 1 },
3084
- {
3085
- type: "array",
3086
- minItems: 1,
3087
- items: { type: "string", minLength: 1 }
3088
- },
3089
- {
3090
- type: "object",
3091
- additionalProperties: false,
3092
- required: ["pattern"],
3093
- properties: {
3094
- pattern: { type: "string", minLength: 1 }
3095
- }
3096
- },
3097
- { const: false }
3098
- ]
3099
- },
3100
- attributes: {
3101
- description: "Literal attributes the node must carry. A string is the required value; `true` requires the attribute to be present, and `false` requires it to be absent.",
3102
- type: "object",
3103
- additionalProperties: {
3104
- oneOf: [{ type: "string" }, { type: "boolean" }]
3105
- }
3106
- },
3107
- oneOfSequenceOrContains: {
3108
- description: "A rule says what it holds in order (`sequence`) or in any order (`contains`), never both.",
3109
- type: "object",
3110
- not: { required: ["sequence", "contains"] }
3111
- },
3112
- noHeadingOnRepeat: {
3113
- description: "`repeat` groups sibling rules into one repeated unit, so the group has no heading of its own. Put the heading on the first rule inside it.",
3114
- type: "object",
3115
- not: { required: ["heading", "repeat"] }
3116
- },
3117
- noInstructions: {
3118
- description: "`instructions` was the language-model hook of doc-structure-lint. manni lint is deterministic and has no model, so a template carrying it is rejected outright rather than silently ignored; the loader turns this into a migration message pointing at manni docevals.",
3119
- type: "object",
3120
- not: { required: ["instructions"] }
3121
- },
3122
- template: {
3123
- description: "One doctype template. A template is a rule describing the page itself, so it takes every rule key, plus `title`, `types` and `extends`.",
3124
- type: "object",
3125
- additionalProperties: false,
3126
- allOf: [
3127
- { $ref: "#/definitions/noInstructions" },
3128
- { $ref: "#/definitions/oneOfSequenceOrContains" },
3129
- { $ref: "#/definitions/noHeadingOnRepeat" }
3130
- ],
3131
- properties: {
3132
- title: {
3133
- description: "Human-readable name for the template, shown by `manni lint templates`.",
3134
- type: "string"
3135
- },
3136
- types: {
3137
- description: "Doctypes this template serves, matched against a page's `type` frontmatter.",
3138
- type: "array",
3139
- items: { type: "string", minLength: 1 }
3140
- },
3141
- extends: {
3142
- description: "Template reference (built-in id, path, or URL, optionally with a `#name` fragment) whose rules this template inherits.",
3143
- type: "string",
3144
- minLength: 1
3145
- },
3146
- id: { $ref: "#/definitions/id" },
3147
- description: { $ref: "#/definitions/description" },
3148
- heading: { $ref: "#/definitions/heading" },
3149
- repeat: { $ref: "#/definitions/repeat" },
3150
- min: {
3151
- description: "Refused on a template: a page is one page. Declared here only so the loader can say that in a sentence, rather than leaving Ajv to report an unexpected key.",
3152
- type: "integer",
3153
- minimum: 0
3154
- },
3155
- max: {
3156
- description: "Refused on a template, for the same reason as `min`.",
3157
- type: "integer",
3158
- minimum: 0
3159
- },
3160
- sequence: { $ref: "#/definitions/sequence" },
3161
- contains: { $ref: "#/definitions/blockRules" },
3162
- sections: { $ref: "#/definitions/rules" }
3163
- }
3164
- },
3165
- rule: {
3166
- description: "One rule. It describes a section of the page, or, with `repeat`, a run of sections matched as one unit.",
3167
- type: "object",
3168
- additionalProperties: false,
3169
- allOf: [
3170
- { $ref: "#/definitions/noInstructions" },
3171
- { $ref: "#/definitions/oneOfSequenceOrContains" },
3172
- { $ref: "#/definitions/noHeadingOnRepeat" }
3173
- ],
3174
- properties: {
3175
- id: { $ref: "#/definitions/id" },
3176
- description: { $ref: "#/definitions/description" },
3177
- heading: { $ref: "#/definitions/heading" },
3178
- repeat: { $ref: "#/definitions/repeat" },
3179
- min: { $ref: "#/definitions/min" },
3180
- max: { $ref: "#/definitions/max" },
3181
- sequence: { $ref: "#/definitions/sequence" },
3182
- contains: { $ref: "#/definitions/blockRules" },
3183
- sections: { $ref: "#/definitions/rules" }
3184
- }
3185
- },
3186
- rules: {
3187
- description: "Rules for the subsections, in document order.",
3188
- type: "array",
3189
- items: { $ref: "#/definitions/rule" }
3190
- },
3191
- repeat: {
3192
- description: "Rules matched in order as one unit, repeated by this rule's `min` and `max`.",
3193
- type: "array",
3194
- minItems: 1,
3195
- items: { $ref: "#/definitions/rule" }
3196
- },
3197
- sequence: {
3198
- description: "What the section holds, in order. Each entry is one run of one content kind.",
3199
- type: "array",
3200
- minItems: 1,
3201
- items: {
3202
- allOf: [
3203
- { $ref: "#/definitions/blockRules" },
3204
- { type: "object", maxProperties: 1 }
3205
- ]
3206
- }
3207
- },
3208
- blockRules: {
3209
- description: "Block rules, keyed by content kind. The key is plural because it carries a count: the node model says `codeBlock`, a template says `codeBlocks: { min: 1 }`.",
3210
- type: "object",
3211
- additionalProperties: false,
3212
- minProperties: 1,
3213
- properties: {
3214
- paragraphs: { $ref: "#/definitions/paragraphs" },
3215
- codeBlocks: { $ref: "#/definitions/codeBlocks" },
3216
- lists: { $ref: "#/definitions/lists" },
3217
- tables: { $ref: "#/definitions/tables" },
3218
- admonitions: { $ref: "#/definitions/admonitions" },
3219
- images: { $ref: "#/definitions/images" },
3220
- blockquotes: { $ref: "#/definitions/counted" },
3221
- definitionLists: { $ref: "#/definitions/counted" },
3222
- elements: { $ref: "#/definitions/elements" }
3223
- }
3224
- },
3225
- counted: {
3226
- description: "A block rule that only counts.",
3227
- type: "object",
3228
- additionalProperties: false,
3229
- properties: {
3230
- min: { $ref: "#/definitions/min" },
3231
- max: { $ref: "#/definitions/max" }
3232
- }
3233
- },
3234
- paragraphs: {
3235
- description: "Paragraph requirements.",
3236
- type: "object",
3237
- additionalProperties: false,
3238
- properties: {
3239
- min: { $ref: "#/definitions/min" },
3240
- max: { $ref: "#/definitions/max" },
3241
- pattern: {
3242
- description: "Unanchored regular expression every paragraph in the run must match.",
3243
- type: "string",
3244
- minLength: 1
3245
- }
3246
- }
3247
- },
3248
- codeBlocks: {
3249
- description: "Code block requirements.",
3250
- type: "object",
3251
- additionalProperties: false,
3252
- properties: {
3253
- min: { $ref: "#/definitions/min" },
3254
- max: { $ref: "#/definitions/max" },
3255
- language: {
3256
- description: "Language every code block must be tagged with, as the node model spells it. A list is one-of: the block must declare one of them.",
3257
- oneOf: [
3258
- { type: "string", minLength: 1 },
3259
- {
3260
- type: "array",
3261
- minItems: 1,
3262
- items: { type: "string", minLength: 1 }
3263
- }
3264
- ]
3265
- },
3266
- fenceInfo: {
3267
- description: "Info-string tail after the language, where the format carries one.",
3268
- type: "string"
3269
- }
3270
- }
3271
- },
3272
- lists: {
3273
- description: "List requirements.",
3274
- type: "object",
3275
- additionalProperties: false,
3276
- properties: {
3277
- min: { $ref: "#/definitions/min" },
3278
- max: { $ref: "#/definitions/max" },
3279
- ordered: {
3280
- description: "Whether the list must be ordered. Absent accepts either.",
3281
- type: "boolean"
3282
- },
3283
- items: { $ref: "#/definitions/listItems" }
3284
- }
3285
- },
3286
- listItems: {
3287
- description: "Requirements applied to every item of every list in the run. Recurses arbitrarily deep.",
3288
- type: "object",
3289
- additionalProperties: false,
3290
- allOf: [{ $ref: "#/definitions/oneOfSequenceOrContains" }],
3291
- properties: {
3292
- min: { $ref: "#/definitions/min" },
3293
- max: { $ref: "#/definitions/max" },
3294
- sequence: { $ref: "#/definitions/sequence" },
3295
- contains: { $ref: "#/definitions/blockRules" }
3296
- }
3297
- },
3298
- tables: {
3299
- description: "Table requirements.",
3300
- type: "object",
3301
- additionalProperties: false,
3302
- properties: {
3303
- min: { $ref: "#/definitions/min" },
3304
- max: { $ref: "#/definitions/max" },
3305
- columns: {
3306
- description: "The header cells, in order.",
3307
- type: "array",
3308
- items: { type: "string" }
3309
- }
3310
- }
3311
- },
3312
- admonitions: {
3313
- description: "Admonition requirements.",
3314
- type: "object",
3315
- additionalProperties: false,
3316
- properties: {
3317
- min: { $ref: "#/definitions/min" },
3318
- max: { $ref: "#/definitions/max" },
3319
- variant: {
3320
- description: "Which flavor of admonition. The node model's `variant`, not any one format's spelling.",
3321
- enum: [
3322
- "note",
3323
- "tip",
3324
- "important",
3325
- "caution",
3326
- "warning",
3327
- "danger"
3328
- ]
3329
- }
3330
- }
3331
- },
3332
- images: {
3333
- description: "Image requirements.",
3334
- type: "object",
3335
- additionalProperties: false,
3336
- properties: {
3337
- min: { $ref: "#/definitions/min" },
3338
- max: { $ref: "#/definitions/max" },
3339
- url: {
3340
- description: "Where the image must point.",
3341
- type: "string"
3342
- },
3343
- alt: {
3344
- description: "Alternative text the image must carry.",
3345
- type: "string"
3346
- },
3347
- attributes: { $ref: "#/definitions/attributes" }
3348
- }
3349
- },
3350
- elements: {
3351
- description: "Named-wrapper requirements. `sequence` and `contains` here describe what the element holds, which is never the enclosing section's content.",
3352
- type: "object",
3353
- additionalProperties: false,
3354
- allOf: [{ $ref: "#/definitions/oneOfSequenceOrContains" }],
3355
- properties: {
3356
- min: { $ref: "#/definitions/min" },
3357
- max: { $ref: "#/definitions/max" },
3358
- tag: {
3359
- description: "The element's name, as an author of the format would write it. A list is one-of: an element counts when it carries any of them.",
3360
- oneOf: [
3361
- { type: "string", minLength: 1 },
3362
- {
3363
- type: "array",
3364
- minItems: 1,
3365
- items: { type: "string", minLength: 1 }
3366
- }
3367
- ]
3368
- },
3369
- attributes: { $ref: "#/definitions/attributes" },
3370
- sequence: { $ref: "#/definitions/sequence" },
3371
- contains: { $ref: "#/definitions/blockRules" }
3372
- }
3373
- }
3374
- }
3375
- };
3376
-
3377
- // templates/lint/tgdp/manifest.json
3378
- var manifest_default = {
3379
- $comment: 'Built-in doctype templates derived from The Good Docs Project templates, pinned to release v1.6.0 ("Iron"): https://gitlab.com/tgdp/templates/-/tree/v1.6.0. Each entry\'s `file` is a YAML template beside this manifest; `source` is the upstream Markdown it was derived from, vendored under test/lint/fixtures/tgdp/ and linted against the derived template so the two cannot drift. Run `npm run check:tgdp-pin` to see whether upstream has moved past the pin.',
3380
- vendor: "tgdp",
3381
- upstream: "https://gitlab.com/tgdp/templates",
3382
- pin: "v1.6.0",
3383
- templates: [
3384
- {
3385
- id: "tgdp:how-to:1.6",
3386
- file: "tgdp/how-to.yaml",
3387
- title: "How-to guide (The Good Docs Project)",
3388
- types: ["how-to"],
3389
- source: "how-to/template_how-to.md"
3390
- },
3391
- {
3392
- id: "tgdp:tutorial:1.6",
3393
- file: "tgdp/tutorial.yaml",
3394
- title: "Tutorial (The Good Docs Project)",
3395
- types: ["tutorial"],
3396
- source: "tutorial/template_tutorial.md"
3397
- },
3398
- {
3399
- id: "tgdp:reference:1.6",
3400
- file: "tgdp/reference.yaml",
3401
- title: "Reference (The Good Docs Project)",
3402
- types: ["reference"],
3403
- source: "reference/template_reference.md"
3404
- },
3405
- {
3406
- id: "tgdp:concept:1.6",
3407
- file: "tgdp/concept.yaml",
3408
- title: "Concept (The Good Docs Project)",
3409
- types: ["concept", "explanation"],
3410
- source: "concept/template_concept.md"
3411
- },
3412
- {
3413
- id: "tgdp:troubleshooting:1.6",
3414
- file: "tgdp/troubleshooting.yaml",
3415
- title: "Troubleshooting (The Good Docs Project)",
3416
- types: ["troubleshooting"],
3417
- source: "troubleshooting/template_troubleshooting.md"
3418
- },
3419
- {
3420
- id: "tgdp:release-notes:1.6",
3421
- file: "tgdp/release-notes.yaml",
3422
- title: "Release notes (The Good Docs Project)",
3423
- types: ["release-notes"],
3424
- source: "release-notes/template_release-notes.md"
3425
- },
3426
- {
3427
- id: "tgdp:readme:1.6",
3428
- file: "tgdp/readme.yaml",
3429
- title: "README (The Good Docs Project)",
3430
- types: ["readme"],
3431
- source: "readme/template_readme.md"
3432
- }
3433
- ]
3434
- };
3435
-
3436
- // src/lint/core/template-registry.ts
3437
- var Ajv = AjvNs.default;
3438
- var DEREFERENCE_OPTIONS = { resolve: { external: false } };
3439
- var MANIFESTS = [manifest_default];
3440
- var BUILTINS = /* @__PURE__ */ new Map();
3441
- for (const manifest of MANIFESTS) {
3442
- for (const entry of manifest.templates) BUILTINS.set(entry.id, entry);
3443
- }
3444
- var builtinCache = /* @__PURE__ */ new Map();
3445
- function listBuiltins() {
3446
- return [...BUILTINS.values()].map((entry) => ({
3447
- id: entry.id,
3448
- title: entry.title,
3449
- types: entry.types
3450
- }));
3451
- }
3452
- async function readBuiltinFile(id, file) {
3453
- const path = join(packageRoot(import.meta.url), "templates", "lint", file);
3454
- try {
3455
- return await readFile(path, "utf8");
3456
- } catch (err) {
3457
- throw new LintError(
3458
- `Built-in template "${id}" is registered but its file could not be read (${path}): ${errorMessage(err)}`
3459
- );
3460
- }
3461
- }
3462
- async function loadBuiltin(id) {
3463
- const cached = builtinCache.get(id);
3464
- if (cached) return cached;
3465
- const entry = BUILTINS.get(id);
3466
- if (!entry) {
3467
- throw new LintError(`No built-in template is registered under "${id}".`);
3468
- }
3469
- const raw = await readBuiltinFile(id, entry.file);
3470
- const parsed = parseYaml(raw);
3471
- if (!isRecord(parsed)) {
3472
- throw new LintError(
3473
- `Built-in template "${id}" (${entry.file}) is not a template file: expected an object at the top level, got ${parsed === null ? "null" : Array.isArray(parsed) ? "an array" : typeof parsed}.`
3474
- );
3475
- }
3476
- const file = validateTemplateFile(
3477
- await dereference(parsed, DEREFERENCE_OPTIONS),
3478
- entry.file
3479
- );
3480
- const names = Object.keys(file.templates ?? {});
3481
- const template = file.templates?.[names[0] ?? ""];
3482
- if (!template || names.length !== 1) {
3483
- throw new LintError(
3484
- `Built-in template "${id}" must define exactly one template, found ${names.length}.`
3485
- );
3486
- }
3487
- const resolved = { types: entry.types, ...template };
3488
- builtinCache.set(id, resolved);
3489
- return resolved;
3490
- }
3491
- var BUILTIN_ID = /^[a-z0-9][a-z0-9._-]*(?::[a-z0-9][a-z0-9._-]*)+$/i;
3492
- var FILE_EXTENSIONS = [".yaml", ".yml", ".json"];
3493
- function classifyRef(ref) {
3494
- if (/^https?:\/\//i.test(ref)) return { kind: "url", ref };
3495
- const lower2 = ref.toLowerCase();
3496
- if (!ref.includes("/") && !ref.includes("\\") && !FILE_EXTENSIONS.some((ext) => lower2.endsWith(ext)) && BUILTIN_ID.test(ref)) {
3497
- return { kind: "builtin", ref };
3498
- }
3499
- return { kind: "file", ref };
3500
- }
3501
- var DEFAULT_TIMEOUT_MS = 1e4;
3502
- var ajv = new Ajv();
3503
- var compiled = null;
3504
- function fileValidator() {
3505
- compiled ??= ajv.compile(template_default);
3506
- return compiled;
3507
- }
3508
- function isRecord(value) {
3509
- return typeof value === "object" && value !== null && !Array.isArray(value);
3510
- }
3511
- function stringsOf(value) {
3512
- if (typeof value === "string") return [value];
3513
- if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
3514
- return [];
3515
- }
3516
- function suggestEvalName(segments) {
3517
- const name = segments.filter((segment) => segment !== "templates" && segment !== "sections").join("-").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3518
- return name || "section-instructions";
3519
- }
3520
- function findInstructions(node, segments = [], seen = /* @__PURE__ */ new WeakSet()) {
3521
- if (typeof node !== "object" || node === null) return null;
3522
- if (seen.has(node)) return null;
3523
- seen.add(node);
3524
- if (Array.isArray(node)) {
3525
- for (const [index, item] of node.entries()) {
3526
- const hit = findInstructions(item, [...segments, String(index)], seen);
3527
- if (hit) return hit;
3528
- }
3529
- return null;
3530
- }
3531
- const record = node;
3532
- const insideSectionsMap = segments.at(-1) === "sections";
3533
- if (!insideSectionsMap && "instructions" in record) {
3534
- return {
3535
- path: segments.join("."),
3536
- name: suggestEvalName(segments),
3537
- values: stringsOf(record.instructions)
3538
- };
3539
- }
3540
- for (const [key, value] of Object.entries(record)) {
3541
- const hit = findInstructions(value, [...segments, key], seen);
3542
- if (hit) return hit;
3543
- }
3544
- return null;
3545
- }
3546
- function instructionsMessage(source, hit) {
3547
- const assertion = hit.values[0] ?? "";
3548
- return [
3549
- `${source}: "${hit.path}" uses \`instructions\`, which manni lint no longer evaluates \u2014 structure checking is deterministic. Move it to a manni docevals assertion eval in manni.config.yaml:`,
3550
- "",
3551
- " docevals:",
3552
- " evals:",
3553
- ` ${hit.name}:`,
3554
- ` assertion: ${assertion}`,
3555
- " grader: ai"
3556
- ].join("\n");
3557
- }
3558
- var V1_KEYS = {
3559
- required: '"required" is not a template key. A rule is optional with "min: 0".',
3560
- additionalSections: '"additionalSections" is not a template key. v2 has no equivalent, so drop it.',
3561
- code_blocks: '"code_blocks" is not a template key. The v2 spelling is "codeBlocks".'
3562
- };
3563
- var V1_SECTIONS_MAP = '"sections" is a list of rules, not a map. Name each rule with "id" and list them in order.';
3564
- var V1_REPEAT_BOOLEAN = '"repeat" is a list of rules, not a boolean. A rule repeats through "min" and "max".';
3565
- var V1_PARAGRAPH_PATTERNS = '"patterns" is not a paragraphs key. The v2 spelling is "pattern", one regular expression.';
3566
- function findV1Key(node, seen = /* @__PURE__ */ new WeakSet()) {
3567
- if (typeof node !== "object" || node === null) return null;
3568
- if (seen.has(node)) return null;
3569
- seen.add(node);
3570
- if (Array.isArray(node)) {
3571
- for (const item of node) {
3572
- const hit = findV1Key(item, seen);
3573
- if (hit) return hit;
3574
- }
3575
- return null;
3576
- }
3577
- const record = node;
3578
- for (const [key, message] of Object.entries(V1_KEYS)) {
3579
- if (key in record) return message;
3580
- }
3581
- if (isRecord(record.sections)) return V1_SECTIONS_MAP;
3582
- if (typeof record.repeat === "boolean") return V1_REPEAT_BOOLEAN;
3583
- if (isRecord(record.paragraphs) && "patterns" in record.paragraphs) {
3584
- return V1_PARAGRAPH_PATTERNS;
3585
- }
3586
- for (const value of Object.values(record)) {
3587
- const hit = findV1Key(value, seen);
3588
- if (hit) return hit;
3589
- }
3590
- return null;
3591
- }
3592
- function findV1File(data) {
3593
- if (!isRecord(data)) return null;
3594
- return findV1Key(data.templates) ?? findV1Key(data.components);
3595
- }
3596
- function ruleLabel(rule, path) {
3597
- return rule.id === void 0 ? path : `"${rule.id}"`;
3598
- }
3599
- function checkRange(range, source, label) {
3600
- const { min, max } = range;
3601
- if (min !== void 0 && max !== void 0 && max < min) {
3602
- throw new LintError(`${source}: rule ${label} sets max ${max} below min ${min}.`);
3603
- }
3604
- }
3605
- function sameOccurrenceRange(a, b) {
3606
- const rangeA = occurrenceRange(a);
3607
- const rangeB = occurrenceRange(b);
3608
- return rangeA.min === rangeB.min && rangeA.max === rangeB.max;
3609
- }
3610
- function checkPattern(pattern, source) {
3611
- if (pattern === void 0) return;
3612
- try {
3613
- new RegExp(pattern);
3614
- } catch (err) {
3615
- throw new LintError(
3616
- `Invalid pattern "${pattern}" in ${source}: ${errorMessage(err)}.`
3617
- );
3618
- }
3619
- }
3620
- function checkListItems(items, source, path) {
3621
- checkRange(items, source, path);
3622
- if (items.contains) checkBlockRule(items.contains, source, `${path}.contains`);
3623
- items.sequence?.forEach((entry, index) => {
3624
- checkBlockRule(entry, source, `${path}.sequence[${index}]`);
3625
- });
3626
- }
3627
- function checkBlockRule(block, source, path) {
3628
- for (const kind of BLOCK_KINDS) {
3629
- const counted = block[kind];
3630
- if (counted) checkRange(counted, source, `${path}.${kind}`);
3631
- }
3632
- checkPattern(block.paragraphs?.pattern, source);
3633
- if (block.lists?.items) {
3634
- checkListItems(block.lists.items, source, `${path}.lists.items`);
3635
- }
3636
- const element = block.elements;
3637
- if (element?.contains) {
3638
- checkBlockRule(element.contains, source, `${path}.elements.contains`);
3639
- }
3640
- element?.sequence?.forEach((entry, index) => {
3641
- checkBlockRule(entry, source, `${path}.elements.sequence[${index}]`);
3642
- });
3643
- }
3644
- function checkRuleBody(rule, source, path) {
3645
- const heading = rule.heading;
3646
- if (typeof heading === "object" && !Array.isArray(heading)) {
3647
- checkPattern(heading.pattern, source);
3648
- }
3649
- if (rule.contains) checkBlockRule(rule.contains, source, `${path}.contains`);
3650
- rule.sequence?.forEach((entry, index) => {
3651
- checkBlockRule(entry, source, `${path}.sequence[${index}]`);
3652
- });
3653
- checkRuleList(rule.repeat, source, `${path}.repeat`);
3654
- checkRuleList(rule.sections, source, `${path}.sections`);
3655
- }
3656
- function checkRuleList(rules, source, path) {
3657
- if (!rules) return;
3658
- const ids = /* @__PURE__ */ new Set();
3659
- for (const [index, rule] of rules.entries()) {
3660
- const here = `${path}[${index}]`;
3661
- if (rule.id !== void 0) {
3662
- if (ids.has(rule.id)) {
3663
- throw new LintError(`${source}: two sibling rules share the id "${rule.id}".`);
3664
- }
3665
- ids.add(rule.id);
3666
- }
3667
- checkRange(rule, source, ruleLabel(rule, here));
3668
- checkRuleBody(rule, source, here);
3669
- }
3670
- for (const [index, rule] of rules.entries()) {
3671
- const next = rules[index + 1];
3672
- if (!next) break;
3673
- if (isWildcard(rule) && rule.repeat === void 0 && isWildcard(next) && next.repeat === void 0 && sameOccurrenceRange(rule, next)) {
3674
- warn(
3675
- `${source}: two adjacent rules have no heading and no repeat; only rule order tells them apart.`
3676
- );
3677
- break;
3678
- }
3679
- }
3680
- }
3681
- function checkTemplateFile(file, source) {
3682
- for (const [name, template] of Object.entries(file.templates ?? {})) {
3683
- if (template.min !== void 0 || template.max !== void 0) {
3684
- throw new LintError(
3685
- `${source}: a template may not set min or max; a page is one page.`
3686
- );
3687
- }
3688
- checkRuleBody(template, source, `templates.${name}`);
3689
- }
3690
- }
3691
- var NOT_CONSTRAINTS = {
3692
- oneOfSequenceOrContains: "sets both sequence and contains; a rule says what it holds in order, or in any order, never both",
3693
- noHeadingOnRepeat: "sets both heading and repeat; a repeat group has no heading of its own, so put the heading on the first rule inside it",
3694
- noInstructions: "carries an instructions key, which this tool does not read"
3695
- };
3696
- function describeError(error) {
3697
- const where = error.instancePath || "/";
3698
- if (error.keyword === "not") {
3699
- for (const [name, why] of Object.entries(NOT_CONSTRAINTS)) {
3700
- if (error.schemaPath.includes(name)) return `${where} ${why}`;
3701
- }
3702
- }
3703
- const params = error.params;
3704
- const extra = typeof params.additionalProperty === "string" ? ` ("${params.additionalProperty}")` : "";
3705
- return `${where} ${error.message ?? "is invalid"}${extra}`;
3706
- }
3707
- function validateTemplateFile(data, source) {
3708
- const instructions = findInstructions(data);
3709
- if (instructions) throw new LintError(instructionsMessage(source, instructions));
3710
- const v1 = findV1File(data);
3711
- if (v1) throw new LintError(`${source}: ${v1}`);
3712
- const validate = fileValidator();
3713
- if (!validate(data)) {
3714
- const errors = validate.errors ?? [];
3715
- const detail = errors.map(describeError).join("; ") || "does not match the template schema";
3716
- throw new LintError(`${source} is not a valid template file: ${detail}.`);
3717
- }
3718
- const file = data;
3719
- checkTemplateFile(file, source);
3720
- return file;
3721
- }
3722
- function splitFragment(ref) {
3723
- const hash = ref.lastIndexOf("#");
3724
- if (hash === -1) return { base: ref, fragment: null };
3725
- return { base: ref.slice(0, hash), fragment: ref.slice(hash + 1) || null };
3726
- }
3727
- function extensionSource(ref, kind) {
3728
- if (kind !== "url") return ref;
3729
- try {
3730
- return new URL(ref).pathname;
3731
- } catch {
3732
- return ref;
3733
- }
3734
- }
3735
- function parseTemplateFile(raw, source, kind) {
3736
- const lower2 = extensionSource(source, kind).toLowerCase();
3737
- if (lower2.endsWith(".json")) {
3738
- try {
3739
- return JSON.parse(raw);
3740
- } catch (err) {
3741
- throw new LintError(`${source} is not valid JSON: ${errorMessage(err)}`);
3742
- }
3743
- }
3744
- try {
3745
- return parseYaml(raw);
3746
- } catch (err) {
3747
- throw new LintError(`${source} is not valid YAML: ${errorMessage(err)}`);
3748
- }
3749
- }
3750
- async function readText(ref) {
3751
- try {
3752
- return await readFile(ref, "utf8");
3753
- } catch (err) {
3754
- const code = err?.code;
3755
- if (code === "ENOENT") {
3756
- throw new LintError(`Template file not found: "${ref}".`);
3757
- }
3758
- throw new LintError(
3759
- `Template file "${ref}" could not be read: ${errorMessage(err)}`
3760
- );
3761
- }
3762
- }
3763
- var urlCache = /* @__PURE__ */ new Map();
3764
- function clearTemplateCaches() {
3765
- builtinCache.clear();
3766
- urlCache.clear();
3767
- }
3768
- async function fetchText(ref, timeoutMs) {
3769
- let res;
3770
- try {
3771
- res = await fetch(ref, { signal: AbortSignal.timeout(timeoutMs) });
3772
- } catch (err) {
3773
- const e = err;
3774
- if (e.name === "TimeoutError" || e.name === "AbortError") {
3775
- throw new LintError(
3776
- `Failed to fetch template file "${ref}": timed out after ${timeoutMs}ms.`
3777
- );
3778
- }
3779
- throw new LintError(`Failed to fetch template file "${ref}": ${e.message}`);
3780
- }
3781
- if (!res.ok) {
3782
- throw new LintError(`Failed to fetch template file "${ref}": HTTP ${res.status}.`);
3783
- }
3784
- return await res.text();
3785
- }
3786
- async function dereferenceTemplates(data, source) {
3787
- try {
3788
- return await dereference(data, DEREFERENCE_OPTIONS);
3789
- } catch (err) {
3790
- throw new LintError(`${source}: could not resolve a "$ref": ${errorMessage(err)}`);
3791
- }
3792
- }
3793
- async function loadTemplateFile(ref, options = {}) {
3794
- const { kind } = classifyRef(ref);
3795
- if (kind === "builtin") {
3796
- throw new LintError(
3797
- `"${ref}" is a built-in template id, not a template file. Load it with loadTemplate().`
3798
- );
3799
- }
3800
- const cached = kind === "url" ? urlCache.get(ref) : void 0;
3801
- if (cached) return cached;
3802
- const raw = kind === "url" ? await fetchText(ref, options.timeoutMs ?? DEFAULT_TIMEOUT_MS) : await readText(ref);
3803
- const parsed = parseTemplateFile(raw, ref, kind);
3804
- if (!isRecord(parsed)) {
3805
- throw new LintError(
3806
- `${ref} is not a template file: expected an object at the top level, got ${parsed === null ? "null" : Array.isArray(parsed) ? "an array" : typeof parsed}.`
3807
- );
3808
- }
3809
- const file = validateTemplateFile(await dereferenceTemplates(parsed, ref), ref);
3810
- if (kind === "url") urlCache.set(ref, file);
3811
- return file;
3812
- }
3813
- async function loadTemplate(ref, options = {}) {
3814
- const { base, fragment } = splitFragment(ref);
3815
- const { kind } = classifyRef(base);
3816
- if (kind === "builtin") {
3817
- if (fragment !== null) {
3818
- throw new LintError(
3819
- `"${ref}" adds a "#${fragment}" fragment to a built-in template id. A built-in id names one template on its own - drop the fragment and use "${base}". Fragments name a template inside a file, as in "./templates.yaml#how-to".`
3820
- );
3821
- }
3822
- if (!BUILTINS.has(base)) {
3823
- const available = [...BUILTINS.keys()].join(", ");
3824
- throw new LintError(
3825
- `Unknown built-in template "${base}". Available: ${available || "(none)"}.`
3826
- );
3827
- }
3828
- return loadBuiltin(base);
3829
- }
3830
- const file = await loadTemplateFile(base, options);
3831
- const templates = file.templates ?? {};
3832
- const names = Object.keys(templates);
3833
- if (fragment !== null) {
3834
- const named = Object.hasOwn(templates, fragment) ? templates[fragment] : void 0;
3835
- if (named === void 0) {
3836
- throw new LintError(
3837
- `${base} has no template named "${fragment}". Available: ${names.join(", ") || "(none)"}.`
3838
- );
3839
- }
3840
- return named;
3841
- }
3842
- const [only] = Object.values(templates);
3843
- if (names.length === 1 && only !== void 0) return only;
3844
- if (names.length === 0) throw new LintError(`${base} defines no templates.`);
3845
- throw new LintError(
3846
- `${base} defines ${names.length} templates; name one with a "#" fragment (e.g. "${base}#${names[0] ?? ""}"). Available: ${names.join(", ")}.`
3847
- );
3848
- }
3849
- function mergeRuleLists(parent, child) {
3850
- if (!parent) return child;
3851
- if (!child) return parent;
3852
- const merged = [...parent];
3853
- for (const childRule of child) {
3854
- const id = childRule.id;
3855
- const index = id === void 0 ? -1 : merged.findIndex((rule) => rule.id === id);
3856
- const target = index === -1 ? void 0 : merged[index];
3857
- if (target === void 0) merged.push(childRule);
3858
- else merged[index] = mergeRules(target, childRule);
3859
- }
3860
- return merged;
3861
- }
3862
- function mergeRules(parent, child) {
3863
- const sections = mergeRuleLists(parent.sections, child.sections);
3864
- const { sections: _parentSections, ...parentRest } = parent;
3865
- const { sections: _childSections, ...childRest } = child;
3866
- const merged = { ...parentRest, ...childRest };
3867
- if (sections !== void 0) merged.sections = sections;
3868
- return merged;
3869
- }
3870
- function mergeTemplates(parent, child) {
3871
- const sections = mergeRuleLists(parent.sections, child.sections);
3872
- const { sections: _parentSections, ...parentRest } = parent;
3873
- const { sections: _childSections, ...childRest } = child;
3874
- const merged = { ...parentRest, ...childRest };
3875
- if (sections !== void 0) merged.sections = sections;
3876
- delete merged.extends;
3877
- return merged;
3878
- }
3879
- async function resolveExtends(template, load2 = loadTemplate, chain = []) {
3880
- const parentRef = template.extends;
3881
- if (parentRef === void 0) return template;
3882
- if (chain.includes(parentRef)) {
3883
- throw new LintError(
3884
- `Template "extends" cycle: ${[...chain, parentRef].join(" -> ")}.`
3885
- );
3886
- }
3887
- const parent = await resolveExtends(await load2(parentRef), load2, [...chain, parentRef]);
3888
- return mergeTemplates(parent, template);
3889
- }
3890
- function refRelativeTo(baseRef, ref) {
3891
- const { base, fragment } = splitFragment(ref);
3892
- if (classifyRef(base).kind !== "file" || isAbsolute(base)) return ref;
3893
- const { base: fromBase } = splitFragment(baseRef);
3894
- const from = classifyRef(fromBase).kind;
3895
- if (from === "url") {
3896
- const rebased2 = new URL(base, fromBase).href;
3897
- return fragment === null ? rebased2 : `${rebased2}#${fragment}`;
3898
- }
3899
- if (from !== "file") return ref;
3900
- const rebased = resolvePath(dirname(fromBase), base);
3901
- return fragment === null ? rebased : `${rebased}#${fragment}`;
3902
- }
3903
- async function loadResolvedTemplate(ref, options = {}, chain = []) {
3904
- const seen = chain.length === 0 ? [ref] : chain;
3905
- const template = await loadTemplate(ref, options);
3906
- const parentRef = template.extends;
3907
- if (parentRef === void 0) return template;
3908
- const absolute = refRelativeTo(ref, parentRef);
3909
- if (seen.includes(absolute)) {
3910
- throw new LintError(
3911
- `Template "extends" cycle: ${[...seen, absolute].join(" -> ")}.`
3912
- );
3913
- }
3914
- const parent = await loadResolvedTemplate(absolute, options, [
3915
- ...seen,
3916
- absolute
3917
- ]);
3918
- return mergeTemplates(parent, template);
3919
- }
3920
-
3921
- // src/lint/core/resolve-template.ts
3922
- import picomatch from "picomatch";
3923
- var FILE_TEMPLATE_KEY = "$template";
3924
- var TYPE_KEY = "type";
3925
- function buildTypeIndex(params) {
3926
- const index = /* @__PURE__ */ new Map();
3927
- for (const builtin of params.builtins ?? []) {
3928
- for (const type of builtin.types) {
3929
- index.set(type, { ref: builtin.id, source: "builtin" });
3930
- }
3931
- }
3932
- for (const { ref, file } of params.userFiles ?? []) {
3933
- for (const [name, template] of Object.entries(file.templates ?? {})) {
3934
- for (const type of template.types ?? []) {
3935
- index.set(type, { ref: `${ref}#${name}`, source: "user" });
3936
- }
3937
- }
3938
- }
3939
- for (const [type, ref] of Object.entries(params.explicitTypes ?? {})) {
3940
- index.set(type, { ref, source: "config" });
3941
- }
3942
- return index;
3943
- }
3944
- var matcherCache = /* @__PURE__ */ new Map();
3945
- function clearMatcherCache() {
3946
- matcherCache.clear();
3947
- }
3948
- function matchesGlob(glob, filePath) {
3949
- let m = matcherCache.get(glob);
3950
- if (!m) {
3951
- m = picomatch(glob, { dot: true });
3952
- matcherCache.set(glob, m);
3953
- }
3954
- return m(filePath.replace(/\\/g, "/"));
3955
- }
3956
- function suggestTypes(unknown, known) {
3957
- const target = unknown.toLowerCase();
3958
- const scored = [];
3959
- for (const type of known) {
3960
- const distance = editDistance(target, type.toLowerCase());
3961
- const threshold = target.length <= 4 ? 1 : 3;
3962
- if (distance <= threshold) scored.push({ type, distance });
3963
- }
3964
- return scored.sort((a, b) => a.distance - b.distance || a.type.localeCompare(b.type)).slice(0, 3).map((s) => s.type);
3965
- }
3966
- function editDistance(a, b) {
3967
- if (a === b) return 0;
3968
- let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
3969
- let last = b.length;
3970
- for (let i = 0; i < a.length; i++) {
3971
- const row = [i + 1];
3972
- let left = i + 1;
3973
- let diagonal = i;
3974
- for (const [j, above] of prev.slice(1).entries()) {
3975
- const cost = a.charCodeAt(i) === b.charCodeAt(j) ? 0 : 1;
3976
- const cell = Math.min(left + 1, above + 1, diagonal + cost);
3977
- row.push(cell);
3978
- diagonal = above;
3979
- left = cell;
3980
- }
3981
- prev = row;
3982
- last = left;
3983
- }
3984
- return last;
3985
- }
3986
- function refFromFrontmatter(frontmatter, key) {
3987
- const value = frontmatter?.[key];
3988
- return typeof value === "string" && value.length > 0 ? value : null;
3989
- }
3990
- function resolveTemplateRef(params) {
3991
- const steps = [];
3992
- const typeIndex = params.typeIndex ?? /* @__PURE__ */ new Map();
3993
- const decide = (stage, ref) => ({
3994
- ref,
3995
- stage,
3996
- steps
3997
- });
3998
- if (params.cliTemplate) {
3999
- steps.push({ stage: "cli", ref: params.cliTemplate, detail: "--template" });
4000
- return decide("cli", params.cliTemplate);
4001
- }
4002
- steps.push({ stage: "cli", ref: null, detail: "--template not given" });
4003
- const fileTemplate = refFromFrontmatter(params.frontmatter, FILE_TEMPLATE_KEY);
4004
- if (fileTemplate) {
4005
- steps.push({
4006
- stage: "frontmatter-template",
4007
- ref: fileTemplate,
4008
- detail: `${FILE_TEMPLATE_KEY} in frontmatter`
4009
- });
4010
- return decide("frontmatter-template", fileTemplate);
4011
- }
4012
- steps.push({
4013
- stage: "frontmatter-template",
4014
- ref: null,
4015
- detail: `no ${FILE_TEMPLATE_KEY} in frontmatter`
4016
- });
4017
- for (const override of params.overrides ?? []) {
4018
- if (matchesGlob(override.files, params.filePath) || params.absolutePath !== void 0 && matchesGlob(override.files, params.absolutePath)) {
4019
- steps.push({
4020
- stage: "config-override",
4021
- ref: override.template,
4022
- detail: `matched override "${override.files}"`
4023
- });
4024
- return decide("config-override", override.template);
4025
- }
4026
- }
4027
- steps.push({
4028
- stage: "config-override",
4029
- ref: null,
4030
- detail: (params.overrides ?? []).length ? "no override glob matched" : "no overrides configured"
4031
- });
4032
- const type = refFromFrontmatter(params.frontmatter, TYPE_KEY);
4033
- if (type) {
4034
- const entry = typeIndex.get(type);
4035
- if (entry) {
4036
- steps.push({
4037
- stage: "type",
4038
- ref: entry.ref,
4039
- detail: `type: ${type} -> ${entry.source} template`
4040
- });
4041
- return decide("type", entry.ref);
4042
- }
4043
- steps.push({
4044
- stage: "type",
4045
- ref: null,
4046
- detail: `type: ${type} matches no template`
4047
- });
4048
- return {
4049
- ref: null,
4050
- stage: null,
4051
- cause: "unknown-type",
4052
- unknownType: type,
4053
- suggestions: suggestTypes(type, typeIndex.keys()),
4054
- knownTypes: knownTypes(typeIndex),
4055
- steps
4056
- };
4057
- }
4058
- steps.push({ stage: "type", ref: null, detail: "no type in frontmatter" });
4059
- if (params.defaultTemplate) {
4060
- steps.push({
4061
- stage: "config-default",
4062
- ref: params.defaultTemplate,
4063
- detail: "config default"
4064
- });
4065
- return decide("config-default", params.defaultTemplate);
4066
- }
4067
- steps.push({ stage: "config-default", ref: null, detail: "no default configured" });
4068
- return { ref: null, stage: null, cause: "no-type", steps };
4069
- }
4070
- function knownTypes(index) {
4071
- return [...index.keys()].sort();
4072
- }
4073
-
4074
- // src/lint/core/validator.ts
4075
- function origin2() {
4076
- return {
4077
- start: { line: 1, column: 1, offset: 0 },
4078
- end: { line: 1, column: 1, offset: 0 }
4079
- };
4080
- }
4081
- function span(sections) {
4082
- const first = sections[0];
4083
- const last = sections[sections.length - 1];
4084
- if (!first || !last) return origin2();
4085
- return { start: { ...first.position.start }, end: { ...last.position.end } };
4086
- }
4087
- function pageSection(tree) {
4088
- const [only] = tree.sections;
4089
- if (tree.sections.length === 1 && only && only.level <= 1) return only;
4090
- return {
4091
- slug: "",
4092
- title: "",
4093
- level: 0,
4094
- order: 0,
4095
- parentSlug: null,
4096
- titlePosition: null,
4097
- position: span(tree.sections),
4098
- children: [],
4099
- sections: tree.sections
4100
- };
4101
- }
4102
- var BLOCK_NOUNS = {
4103
- paragraphs: "paragraphs",
4104
- codeBlocks: "code blocks",
4105
- lists: "lists",
4106
- tables: "tables",
4107
- admonitions: "admonitions",
4108
- images: "images",
4109
- blockquotes: "blockquotes",
4110
- definitionLists: "definition lists",
4111
- elements: "elements"
4112
- };
4113
- var Pruner = class {
4114
- emitted;
4115
- /** Keyed by message, so one rule asked twice is reported once. */
4116
- missing = /* @__PURE__ */ new Map();
4117
- constructor(kinds) {
4118
- this.emitted = new Set(kinds);
4119
- }
4120
- get gaps() {
4121
- return [...this.missing.values()];
4122
- }
4123
- emits(kind) {
4124
- return this.emitted.has(kind);
4125
- }
4126
- drop(rule, noun) {
4127
- this.missing.set(`${rule}: ${noun}`, { rule, noun });
4128
- }
4129
- /** A table rule without its `columns`, when the parser reports no cells. */
4130
- tables(rule) {
4131
- if (rule.columns === void 0 || this.emits("tableCell")) return rule;
4132
- this.drop("columns", "table columns");
4133
- const { columns: _columns, ...kept } = rule;
4134
- return kept;
4135
- }
4136
- /**
4137
- * A list rule with its `items` pruned.
4138
- *
4139
- * `items` has no capability of its own to check. A parser that reports a
4140
- * list reports its items, since `ListNode.items` is not optional, and
4141
- * `listItem` is not one of the `BLOCK_KINDS` a block rule can target. So
4142
- * the `lists` gap covers the whole rule, and only what the items *hold* can
4143
- * go unchecked.
4144
- *
4145
- * `listItem` is a `ContentKind` and parsers do declare it. That is a fact
4146
- * about the node model rather than about this pruner: a template names
4147
- * `lists`, and reaches an item only through `items`, so there is no rule
4148
- * whose capability would be `listItem` on its own.
4149
- */
4150
- lists(rule) {
4151
- const items = rule.items;
4152
- if (items === void 0) return rule;
4153
- const pruned = this.listItems(items);
4154
- return pruned === items ? rule : { ...rule, items: pruned };
4155
- }
4156
- listItems(rule) {
4157
- return this.holder(rule);
4158
- }
4159
- elements(rule) {
4160
- return this.holder(rule);
4161
- }
4162
- /** The `sequence`/`contains` pair every content holder carries. */
4163
- holder(rule) {
4164
- const kept = { ...rule };
4165
- let changed = false;
4166
- if (rule.sequence) {
4167
- const sequence = this.sequence(rule.sequence);
4168
- if (sequence === void 0) {
4169
- delete kept.sequence;
4170
- changed = true;
4171
- } else if (sequence !== rule.sequence) {
4172
- kept.sequence = sequence;
4173
- changed = true;
4174
- }
4175
- }
4176
- if (rule.contains) {
4177
- const contains = this.block(rule.contains);
4178
- if (contains === void 0) {
4179
- delete kept.contains;
4180
- changed = true;
4181
- } else if (contains !== rule.contains) {
4182
- kept.contains = contains;
4183
- changed = true;
4184
- }
4185
- }
4186
- return changed ? kept : rule;
4187
- }
4188
- /**
4189
- * One `sequence`, with the runs this format cannot see taken out.
4190
- *
4191
- * Taking the entry out rather than leaving it empty is what keeps the rest of
4192
- * the sequence meaningful: a run of a kind the parser never emits can never
4193
- * appear among the actual runs, so an entry left standing would report an
4194
- * order error on every file of that format. `undefined` means the key goes.
4195
- */
4196
- sequence(entries) {
4197
- const kept = [];
4198
- let changed = false;
4199
- for (const entry of entries) {
4200
- const pruned = this.block(entry);
4201
- if (pruned === void 0) {
4202
- changed = true;
4203
- continue;
4204
- }
4205
- if (pruned !== entry) changed = true;
4206
- kept.push(pruned);
4207
- }
4208
- if (!changed) return entries;
4209
- return kept.length > 0 ? kept : void 0;
4210
- }
4211
- /** One block rule, or `undefined` when nothing in it can be checked. */
4212
- block(block) {
4213
- const kept = { ...block };
4214
- let changed = false;
4215
- for (const key of BLOCK_KINDS) {
4216
- if (kept[key] === void 0) continue;
4217
- if (this.emits(BLOCK_KIND_NODE[key])) continue;
4218
- this.drop(key, BLOCK_NOUNS[key]);
4219
- Object.assign(kept, { [key]: void 0 });
4220
- changed = true;
4221
- }
4222
- const tables = kept.tables;
4223
- if (tables) {
4224
- const pruned = this.tables(tables);
4225
- if (pruned !== tables) {
4226
- kept.tables = pruned;
4227
- changed = true;
4228
- }
4229
- }
4230
- const lists = kept.lists;
4231
- if (lists) {
4232
- const pruned = this.lists(lists);
4233
- if (pruned !== lists) {
4234
- kept.lists = pruned;
4235
- changed = true;
4236
- }
4237
- }
4238
- const elements = kept.elements;
4239
- if (elements) {
4240
- const pruned = this.elements(elements);
4241
- if (pruned !== elements) {
4242
- kept.elements = pruned;
4243
- changed = true;
4244
- }
4245
- }
4246
- if (!changed) return block;
4247
- return BLOCK_KINDS.some((key) => kept[key] !== void 0) ? kept : void 0;
4248
- }
4249
- rules(rules) {
4250
- const kept = [];
4251
- let changed = false;
4252
- for (const rule of rules) {
4253
- const pruned = this.rule(rule);
4254
- if (pruned !== rule) changed = true;
4255
- kept.push(pruned);
4256
- }
4257
- return changed ? kept : rules;
4258
- }
4259
- rule(rule) {
4260
- const kept = this.holder(rule);
4261
- let changed = kept !== rule;
4262
- const out = changed ? kept : { ...rule };
4263
- if (rule.repeat) {
4264
- const repeat = this.rules(rule.repeat);
4265
- if (repeat !== rule.repeat) {
4266
- out.repeat = repeat;
4267
- changed = true;
4268
- }
4269
- }
4270
- if (rule.sections) {
4271
- const sections = this.rules(rule.sections);
4272
- if (sections !== rule.sections) {
4273
- out.sections = sections;
4274
- changed = true;
4275
- }
4276
- }
4277
- return changed ? out : rule;
4278
- }
4279
- };
4280
- var pruneCache = /* @__PURE__ */ new WeakMap();
4281
- function prune(template, kinds) {
4282
- const key = [...kinds].sort().join(",");
4283
- let byKinds = pruneCache.get(template);
4284
- if (!byKinds) {
4285
- byKinds = /* @__PURE__ */ new Map();
4286
- pruneCache.set(template, byKinds);
4287
- }
4288
- const hit = byKinds.get(key);
4289
- if (hit) return hit;
4290
- const pruner = new Pruner(kinds);
4291
- const pruned = { template: pruner.rule(template), gaps: pruner.gaps };
4292
- byKinds.set(key, pruned);
4293
- return pruned;
4294
- }
4295
- function uncheckedFinding(format, name, gap) {
4296
- return {
4297
- type: "unsupported_content_kind",
4298
- heading: null,
4299
- message: `The ${format} parser does not report ${gap.noun}, so the "${gap.rule}" rule in template "${name}" is not checked for this file.`,
4300
- position: origin2(),
4301
- // A warning, because nothing about the document is wrong: the rule asks
4302
- // about something this format cannot express. It never changes the exit
4303
- // code - `runLint` counts a file as failed only on an error-severity
4304
- // finding - so a docset of mixed formats stays green while still saying
4305
- // which rules went unchecked, and where.
4306
- severity: "warning"
4307
- };
4308
- }
4309
- function checkSection(section, rule) {
4310
- return [
4311
- ...checkHeading(section, rule.heading),
4312
- ...checkSequence(section, rule.sequence),
4313
- ...checkContains(section, rule.contains)
4314
- ];
4315
- }
4316
- function validateSections(sections, rules, options = {}) {
4317
- const { matches, findings } = matchSections(sections, rules, options);
4318
- const all = [...findings];
4319
- for (const match of matches) {
4320
- all.push(...checkSection(match.section, match.rule));
4321
- all.push(
4322
- ...validateSections(match.section.sections, match.rule.sections, {
4323
- ...options,
4324
- parent: match.section
4325
- })
4326
- );
4327
- }
4328
- return all;
4329
- }
4330
- function validateDocument(tree, template, options = {}) {
4331
- const kinds = options.kinds ?? parserByName(tree.format)?.kinds ?? [];
4332
- const pruned = prune(template, kinds);
4333
- const rules = pruned.template;
4334
- const name = options.template ?? template.title ?? "unnamed";
4335
- const findings = pruned.gaps.map(
4336
- (gap) => uncheckedFinding(tree.format, name, gap)
4337
- );
4338
- const page = pageSection(tree);
4339
- const ctx = {
4340
- heading: page.title || null,
4341
- position: page.position
4342
- };
4343
- findings.push(...checkHeading(page, rules.heading));
4344
- findings.push(...checkSequenceIn(page.children, rules.sequence, ctx));
4345
- findings.push(...checkContainsIn(page.children, rules.contains, ctx));
4346
- findings.push(
4347
- ...validateSections(page.sections, rules.sections, {
4348
- parent: page,
4349
- source: options.source,
4350
- template: options.template
4351
- })
4352
- );
4353
- return findings.sort(
4354
- (a, b) => a.position.start.offset - b.position.start.offset
4355
- );
4356
- }
4357
-
4358
- // src/lint/core/config.ts
4359
- import { dirname as dirname2, isAbsolute as isAbsolute2, resolve } from "node:path";
4360
- import { parse as parseYaml2 } from "yaml";
4361
- import * as AjvNs2 from "ajv";
4362
-
4363
- // schemas/lint/config.json
4364
- var config_default = {
4365
- $schema: "http://json-schema.org/draft-07/schema#",
4366
- $id: "manni-lint:config:1",
4367
- title: "manni lint config section",
4368
- description: "The `lint:` section of a shared `manni.config.yaml`. The file's root is a mapping of tool name to that tool's settings, so this schema describes the section only - never the file. The document set is not here: proposal 0041 declares it once for every tool, under the family-level `collections:` key, and `paths`/`exclude` under `lint:` are refused by name. Every key is optional: a repo that configures no manni lint at all is a legitimate config, and so is one that carries only a sibling tool's section. `additionalProperties: false` applies at every level below `lint:`, which is what turns a typo'd key into a loud failure instead of a silent default.",
4369
- type: "object",
4370
- additionalProperties: false,
4371
- properties: {
4372
- allowEmpty: {
4373
- description: "Treat a run that matched zero files as a success rather than an operational error. The same key, with the same meaning, as `cite.allowEmpty`: for a shared CI template or a pre-commit hook whose file list may legitimately be empty. `--allow-empty` on the command line sets it for one run.",
4374
- type: "boolean"
4375
- },
4376
- overrides: {
4377
- description: "Repo policy: everything under a glob is linted with one template, whatever the pages say they are. First matching entry wins, and an override outranks a page's own `type`.",
4378
- type: "array",
4379
- items: {
4380
- description: "One policy entry. Both keys are required: an entry naming no template, or no files, is a line that silently does nothing.",
4381
- type: "object",
4382
- additionalProperties: false,
4383
- required: ["files", "template"],
4384
- properties: {
4385
- files: {
4386
- description: "Glob matched against each file's path, written with forward slashes whatever the platform uses.",
4387
- type: "string",
4388
- minLength: 1
4389
- },
4390
- template: {
4391
- description: "Template ref applied to the files this entry matches.",
4392
- type: "string",
4393
- minLength: 1
4394
- }
4395
- }
4396
- }
4397
- },
4398
- structure: {
4399
- description: "The structure job, which `manni lint structure` and `manni lint check` both run. Declaring it with no keys is how a repo says the job applies. Its one setting is the tool that performs it.",
4400
- type: "object",
4401
- additionalProperties: false,
4402
- properties: {
4403
- tool: {
4404
- description: "Which tool performs the job. `manni` is manni's own template-structure engine; `dita-ot` is DITA Open Toolkit, which validates a DITA map and the topics it reaches and is located through the top-level `tools.dita-ot.home`. `--tool <name>` on the command line names the same thing.",
4405
- type: "string",
4406
- enum: ["manni", "dita-ot"]
4407
- }
4408
- }
4409
- },
4410
- template: {
4411
- description: "Template ref applied to a page that declares no doctype and matches no override. The last stage of the resolution chain, not an override: `--template`, a page's `$template`, an `overrides` entry, and the page's own `type` all outrank it.",
4412
- type: "string",
4413
- minLength: 1
4414
- },
4415
- templates: {
4416
- description: "Template files to load. Their templates' `types:` join the doctype routing table, outranking the built-ins, so adopting a repo's own how-to shape is one entry here and no flag.",
4417
- type: "array",
4418
- items: {
4419
- type: "string",
4420
- minLength: 1
4421
- }
4422
- },
4423
- types: {
4424
- description: "Explicit doctype -> template ref map, for a doctype no template file claims with its own `types:`. Keys are the vocabulary a page's `type` frontmatter is written in, so they are the author's names rather than this schema's - constrained only to the shape a doctype slug takes, not to a fixed set.",
4425
- type: "object",
4426
- additionalProperties: false,
4427
- patternProperties: {
4428
- "^[A-Za-z0-9._:-]+$": {
4429
- description: "A template ref: a built-in id, a path, or a URL, optionally with a `#name` fragment.",
4430
- type: "string",
4431
- minLength: 1
4432
- }
4433
- }
4434
- }
4435
- }
4436
- };
4437
-
4438
- // src/lint/core/config.ts
4439
- var Ajv2 = AjvNs2.default;
4440
- var LINT_TOOLS = ["manni", "dita-ot"];
4441
- var LINT_JOBS = ["structure"];
4442
- var TOOLS_BY_JOB = {
4443
- structure: ["manni", "dita-ot"]
4444
- };
4445
- function isLintTool(value) {
4446
- return LINT_TOOLS.includes(value);
4447
- }
4448
- var SECTION_KEY = "lint";
4449
- var CONFIG_FILE = {
4450
- section: SECTION_KEY,
4451
- legacyNames: [],
4452
- toError: (message) => new LintError(message)
4453
- };
4454
- var SECTION_KEYS = Object.keys(config_default.properties);
4455
- var STRUCTURE_KEYS = Object.keys(
4456
- config_default.properties.structure.properties
4457
- );
4458
- var CONFIG_REF = "https://hawkeyexl.github.io/manni/meta/reference/configuration/";
4459
- var MOVED_KEYS = ["paths", "exclude"];
4460
- function assertNoMovedKeys(raw, source) {
4461
- for (const key of MOVED_KEYS) {
4462
- if (Object.hasOwn(raw, key)) {
4463
- throw new LintError(
4464
- `${source}: "${key}" is no longer a ${SECTION_KEY} key. Document sets are declared once for every tool, under a top-level collections: list. See ${CONFIG_REF}#collections`
4465
- );
4466
- }
4467
- }
4468
- }
4469
- function rejectUnknownKeys(obj, allowed, where, source) {
4470
- for (const key of Object.keys(obj)) {
4471
- if (!allowed.includes(key)) {
4472
- throw new LintError(
4473
- `Unknown key "${key}" under ${where}: in ${source}. Supported keys: ${allowed.join(", ")}.`
4474
- );
4475
- }
4476
- }
4477
- }
4478
- function checkJobs(section, source) {
4479
- for (const job of LINT_JOBS) {
4480
- const value = section[job];
4481
- if (value == null || !isRecord2(value)) continue;
4482
- rejectUnknownKeys(value, STRUCTURE_KEYS, `${SECTION_KEY}.${job}`, source);
4483
- const tool = value.tool;
4484
- if (tool === void 0) continue;
4485
- if (typeof tool !== "string" || !TOOLS_BY_JOB[job].includes(tool)) {
4486
- throw new LintError(
4487
- `${source}: ${SECTION_KEY}.${job}.tool must be one of: ${TOOLS_BY_JOB[job].join(", ")}.`
4488
- );
4489
- }
4490
- }
4491
- }
4492
- var ajv2 = new Ajv2({ allErrors: true });
4493
- var compiled2 = null;
4494
- function sectionValidator() {
4495
- compiled2 ??= ajv2.compile(config_default);
4496
- return compiled2;
4497
- }
4498
- function isRecord2(value) {
4499
- return typeof value === "object" && value !== null && !Array.isArray(value);
4500
- }
4501
- function describeError2(error) {
4502
- const params = error.params;
4503
- const extra = typeof params.additionalProperty === "string" ? ` ("${params.additionalProperty}")` : "";
4504
- return `${SECTION_KEY}${error.instancePath} ${error.message ?? "is invalid"}${extra}`;
4505
- }
4506
- function rejectUnNested(root, source) {
4507
- const stray = SECTION_KEYS.filter((key) => key in root);
4508
- if (stray.length === 0) return;
4509
- throw new LintError(
4510
- `${source}: found ${stray.map((key) => `"${key}:"`).join(", ")} at the top level, but no "${SECTION_KEY}:" key. This file is shared across the manni family, so every manni lint setting belongs under a top-level "${SECTION_KEY}:" key. Indent the file's contents one level and add that key.`
4511
- );
4512
- }
4513
- function rejectMiscasedWrapper(root, source) {
4514
- const miscased = Object.keys(root).find(
4515
- (key) => key !== SECTION_KEY && key.toLowerCase() === SECTION_KEY
4516
- );
4517
- if (miscased === void 0) return;
4518
- throw new LintError(
4519
- `${source}: found "${miscased}:" at the top level, but the key is case-sensitive and manni lint reads "${SECTION_KEY}:" exactly. Rename "${miscased}:" to "${SECTION_KEY}:".`
4520
- );
4521
- }
4522
- function parseConfigSection(section, source) {
4523
- if (section == null) return {};
4524
- if (isRecord2(section)) {
4525
- assertNoMovedKeys(section, source);
4526
- rejectUnknownKeys(section, SECTION_KEYS, SECTION_KEY, source);
4527
- checkJobs(section, source);
4528
- }
4529
- const validate = sectionValidator();
4530
- if (!validate(section)) {
4531
- const detail = (validate.errors ?? []).map(describeError2).join("; ") || `does not match the ${SECTION_KEY} config schema`;
4532
- throw new LintError(
4533
- `${source}: invalid "${SECTION_KEY}" section: ${detail}.`
4534
- );
4535
- }
4536
- return section;
4537
- }
4538
- function parseConfig(text, source) {
4539
- let raw;
4540
- try {
4541
- raw = parseYaml2(text);
4542
- } catch (err) {
4543
- throw new LintError(
4544
- `${source}: invalid YAML: ${errorMessage(err)}`
4545
- );
4546
- }
4547
- if (raw == null) return {};
4548
- if (!isRecord2(raw)) {
4549
- throw new LintError(
4550
- `${source}: top level must be a mapping of tool name to that tool's settings, with manni lint's under "${SECTION_KEY}:".`
4551
- );
4552
- }
4553
- if (!(SECTION_KEY in raw)) {
4554
- rejectUnNested(raw, source);
4555
- rejectMiscasedWrapper(raw, source);
4556
- return {};
4557
- }
4558
- return parseConfigSection(raw[SECTION_KEY], source);
4559
- }
4560
- async function loadConfig(explicitPath, cwd = process.cwd()) {
4561
- const file = await readLintConfigFile(explicitPath, cwd);
4562
- if (file === null) return null;
4563
- return { config: sectionOf(file), path: file.path };
4564
- }
4565
- function readLintConfigFile(explicitPath, cwd) {
4566
- return explicitPath ? readConfigFile(explicitPath, cwd, CONFIG_FILE) : findConfigFile(cwd, CONFIG_FILE);
4567
- }
4568
- function sectionOf(file) {
4569
- if (!file.wrapped && isRecord2(file.value)) {
4570
- rejectMiscasedWrapper(file.value, file.source);
4571
- }
4572
- return parseConfigSection(file.value, file.source);
4573
- }
4574
- function rebaseConfig(found) {
4575
- if (!found) return {};
4576
- const dir = dirname2(resolve(found.path));
4577
- const config = found.config;
4578
- const ref = (value) => refRelativeTo(found.path, value);
4579
- const glob = (value) => isAbsolute2(value) ? value : resolve(dir, value).replace(/\\/g, "/");
4580
- return {
4581
- ...config,
4582
- ...config.templates ? { templates: config.templates.map(ref) } : {},
4583
- ...config.template ? { template: ref(config.template) } : {},
4584
- ...config.types ? {
4585
- types: Object.fromEntries(
4586
- Object.entries(config.types).map(([type, value]) => [type, ref(value)])
4587
- )
4588
- } : {},
4589
- ...config.overrides ? {
4590
- overrides: config.overrides.map((o) => ({
4591
- files: glob(o.files),
4592
- template: ref(o.template)
4593
- }))
4594
- } : {}
4595
- };
4596
- }
4597
- var STDIN = "-";
4598
- async function resolveLintRun(opts) {
4599
- const cwd = resolve(opts.cwd);
4600
- const wanted = opts.collection ?? [];
4601
- if (wanted.length > 0 && opts.inputs.some((input) => input !== STDIN)) {
4602
- throw new LintError(
4603
- "--collection selects a configured collection; it cannot be combined with paths."
4604
- );
4605
- }
4606
- const file = opts.noConfig ? null : await readLintConfigFile(opts.configPath, cwd);
4607
- if (wanted.length > 0 && file === null) {
4608
- throw new LintError("--collection needs a config file to select from.");
4609
- }
4610
- if (file !== null) opts.onConfigLoaded?.({ path: file.path, dir: file.dir });
4611
- const config = rebaseConfig(
4612
- file === null ? null : { config: sectionOf(file), path: file.path }
4613
- );
4614
- const collections = file === null ? [] : selectCollections(
4615
- file.collections,
4616
- wanted,
4617
- file.source,
4618
- (message) => new LintError(message)
4619
- );
4620
- const fromCollections = wanted.length > 0 || opts.inputs.length === 0;
4621
- const collectionPaths = fromCollections ? collections.flatMap((c) => c.paths) : [];
4622
- const inputs = fromCollections ? [...opts.inputs, ...collectionPaths] : opts.inputs;
4623
- const base = collectionPaths.length > 0 && file ? file.dir : cwd;
4624
- return {
4625
- config,
4626
- inputs,
4627
- base,
4628
- collections,
4629
- fromCollections,
4630
- tools: file?.tools ?? {},
4631
- ...file === null ? {} : { configDir: file.dir, configPath: file.path, configSource: file.source }
4632
- };
4633
- }
4634
-
4635
- // src/lint/tools/dita-ot.ts
4636
- import { execFile } from "node:child_process";
4637
- import { mkdtemp, readFile as readFile2, rm, stat } from "node:fs/promises";
4638
- import { tmpdir } from "node:os";
4639
- import { delimiter, join as join2 } from "node:path";
4640
- import { fileURLToPath } from "node:url";
4641
- var MAX_BUFFER = 64 * 1024 * 1024;
4642
- var NOT_ON_PATH = "dita is not on PATH. Install DITA Open Toolkit to lint structure, or set tools.dita-ot.home.";
4643
- var noLauncher = (home) => `no DITA Open Toolkit at "${home}". tools.dita-ot.home must be a DITA-OT installation directory.`;
4644
- function ditaOtLauncher(home, platform = process.platform) {
4645
- const name = platform === "win32" ? "dita.bat" : "dita";
4646
- return home === void 0 ? name : join2(home, "bin", name);
4647
- }
4648
- function isBatch(launcher) {
4649
- const lower2 = launcher.toLowerCase();
4650
- return lower2.endsWith(".bat") || lower2.endsWith(".cmd");
4651
- }
4652
- function quoteForCmd(value) {
4653
- if (value.includes('"')) {
4654
- throw new LintError(
4655
- `DITA Open Toolkit cannot be given an argument containing a quote: ${value}`
4656
- );
4657
- }
4658
- return `"${value}"`;
4659
- }
4660
- function ditaOtCommandLine(launcher, args) {
4661
- if (!isBatch(launcher)) {
4662
- return { command: launcher, argv: args, verbatim: false };
4663
- }
4664
- const line = [launcher, ...args].map(quoteForCmd).join(" ");
4665
- return {
4666
- command: process.env["COMSPEC"] ?? "cmd.exe",
4667
- argv: ["/d", "/s", "/c", `"${line}"`],
4668
- verbatim: true
4669
- };
4670
- }
4671
- var spawnDitaOt = (launcher, args, opts) => new Promise((settle, reject) => {
4672
- const line = ditaOtCommandLine(launcher, args);
4673
- execFile(
4674
- line.command,
4675
- line.argv,
4676
- {
4677
- cwd: opts.cwd,
4678
- windowsHide: true,
4679
- windowsVerbatimArguments: line.verbatim,
4680
- maxBuffer: MAX_BUFFER,
4681
- encoding: "utf8"
4682
- },
4683
- (error, stdout, stderr) => {
4684
- if (!error) {
4685
- settle({ code: 0, stdout, stderr });
4686
- return;
4687
- }
4688
- if (typeof error.code === "number") {
4689
- settle({ code: error.code, stdout, stderr });
4690
- return;
4691
- }
4692
- if (error.code == null && typeof error.signal === "string") {
4693
- settle({ code: null, signal: error.signal, stdout, stderr });
4694
- return;
4695
- }
4696
- const failure = error;
4697
- reject(failure);
4698
- }
4699
- );
4700
- });
4701
- function isEnoent(err) {
4702
- return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
4703
- }
4704
- function isMapping(value) {
4705
- return typeof value === "object" && value !== null && !Array.isArray(value);
4706
- }
4707
- async function isFile(path) {
4708
- try {
4709
- return (await stat(path)).isFile();
4710
- } catch {
4711
- return false;
4712
- }
4713
- }
4714
- async function isOnPath(name) {
4715
- for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
4716
- if (dir === "") continue;
4717
- if (await isFile(join2(dir, name))) return true;
4718
- }
4719
- return false;
4720
- }
4721
- function foldDitaOtSeverity(level) {
4722
- switch (level.toUpperCase()) {
4723
- case "FATAL":
4724
- case "ERROR":
4725
- return "error";
4726
- case "WARN":
4727
- return "warning";
4728
- case "INFO":
4729
- return "notice";
4730
- default:
4731
- return null;
4732
- }
4733
- }
4734
- function pathFromFileUri(location) {
4735
- let url;
4736
- try {
4737
- url = new URL(location);
4738
- } catch {
4739
- return location;
4740
- }
4741
- if (url.protocol !== "file:") return location;
4742
- try {
4743
- return fileURLToPath(url);
4744
- } catch {
4745
- return decodeURIComponent(url.pathname);
4746
- }
4747
- }
4748
- function readMessage(raw) {
4749
- if (!isMapping(raw)) return null;
4750
- const code = raw["code"];
4751
- if (typeof code !== "string" || code === "") return null;
4752
- const level = raw["level"];
4753
- const severity = typeof level === "string" ? foldDitaOtSeverity(level) : null;
4754
- if (severity === null) return null;
4755
- const msg = raw["msg"];
4756
- const location = raw["location"];
4757
- const line = raw["line"];
4758
- const row = raw["row"];
4759
- return {
4760
- code,
4761
- severity,
4762
- // A coded message with no text is still a finding; the code says what it is.
4763
- message: typeof msg === "string" && msg !== "" ? msg : code,
4764
- ...typeof location === "string" && location !== "" ? { file: pathFromFileUri(location) } : {},
4765
- ...typeof line === "number" ? { line } : {},
4766
- ...typeof row === "number" ? { column: row } : {}
4767
- };
4768
- }
4769
- function parseDitaOtLog(text) {
4770
- let parsed;
4771
- try {
4772
- parsed = JSON.parse(text);
4773
- } catch {
4774
- return null;
4775
- }
4776
- if (!Array.isArray(parsed)) return null;
4777
- const messages = [];
4778
- for (const raw of parsed) {
4779
- const message = readMessage(raw);
4780
- if (message !== null) messages.push(message);
4781
- }
4782
- return messages;
4783
- }
4784
- function parseDitaOtVersion(output) {
4785
- const match = /\b(\d+(?:\.\d+)+(?:[-.][0-9A-Za-z]+)*)/.exec(output);
4786
- return match?.[1] ?? null;
4787
- }
4788
- function failureText(result) {
4789
- const stderr = result.stderr.trim();
4790
- if (stderr !== "") return stderr;
4791
- if (result.signal != null) return `killed by ${result.signal}`;
4792
- return `exit code ${String(result.code)}`;
4793
- }
4794
- async function readLog(path) {
4795
- let text;
4796
- try {
4797
- text = await readFile2(path, "utf8");
4798
- } catch {
4799
- return null;
4800
- }
4801
- return parseDitaOtLog(text);
4802
- }
4803
- async function runDitaOtValidate(opts) {
4804
- const spawn = opts.spawn ?? spawnDitaOt;
4805
- const launcher = ditaOtLauncher(opts.home);
4806
- if (opts.home !== void 0 && !await isFile(launcher)) {
4807
- throw new LintError(noLauncher(opts.home));
4808
- }
4809
- const scratch = await mkdtemp(join2(tmpdir(), "manni-dita-ot-"));
4810
- try {
4811
- const results = [];
4812
- for (const [index, target] of opts.targets.entries()) {
4813
- const logfile = join2(scratch, `${String(index)}.json`);
4814
- const args = [
4815
- `--input=${target}`,
4816
- "--format=dita",
4817
- `--output=${join2(scratch, `out-${String(index)}`)}`,
4818
- `--temp=${join2(scratch, `temp-${String(index)}`)}`,
4819
- "--logger=json",
4820
- `--logfile=${logfile}`,
4821
- "--verbose"
4822
- ];
4823
- let result;
4824
- try {
4825
- result = await spawn(launcher, args, { cwd: opts.cwd });
4826
- } catch (err) {
4827
- if (isEnoent(err)) {
4828
- throw new LintError(
4829
- opts.home === void 0 ? NOT_ON_PATH : noLauncher(opts.home)
4830
- );
4831
- }
4832
- throw err;
4833
- }
4834
- const messages = await readLog(logfile);
4835
- const explained = messages !== null && messages.some((m) => m.severity === "error");
4836
- if (result.code !== 0 && !explained) {
4837
- if (opts.home === void 0 && !await isOnPath(launcher)) {
4838
- throw new LintError(NOT_ON_PATH);
4839
- }
4840
- throw new LintError(
4841
- `DITA Open Toolkit failed on ${target}: ${failureText(result)}. Run it directly on that file to see why.`
4842
- );
4843
- }
4844
- results.push({ target, messages: messages ?? [] });
4845
- }
4846
- return results;
4847
- } finally {
4848
- await rm(scratch, { recursive: true, force: true });
4849
- }
4850
- }
4851
- async function probeDitaOt(ctx, spawn = spawnDitaOt) {
4852
- const home = ctx.tools === void 0 ? void 0 : ditaOtHome(ctx.tools, ctx.configDir ?? ctx.cwd);
4853
- const launcher = ditaOtLauncher(home);
4854
- if (home !== void 0 && !await isFile(launcher)) {
4855
- return { available: false, version: null };
4856
- }
4857
- let result;
4858
- try {
4859
- result = await spawn(launcher, ["--version"], { cwd: ctx.cwd });
4860
- } catch {
4861
- return { available: false, version: null };
4862
- }
4863
- if (result.code !== 0) return { available: false, version: null };
4864
- return {
4865
- available: true,
4866
- // Both channels, because which one carries the banner is not fixed: the
4867
- // shell launcher prints it on stdout, and the Windows `.bat` run through
4868
- // `cmd.exe` has been seen to put it on stderr. Reading one would report a
4869
- // toolkit that is plainly there as versionless on the other platform.
4870
- version: parseDitaOtVersion(`${result.stdout}
4871
- ${result.stderr}`)
4872
- };
4873
- }
4874
- var DITA_FORMAT = {
4875
- name: "dita",
4876
- label: "DITA",
4877
- extensions: [".ditamap", ".dita", ".xml"],
4878
- kinds: []
4879
- };
4880
- var ditaOt = {
4881
- name: "dita-ot",
4882
- label: "DITA Open Toolkit",
4883
- formats: () => [{ ...DITA_FORMAT }],
4884
- // Maps only. DITA-OT takes one `--input` per invocation, so walking
4885
- // every topic in a docset would pay a JVM start per topic. Naming a `.dita`
4886
- // explicitly still works: a named file bypasses the extension filter.
4887
- walkExtensions: () => [".ditamap"],
4888
- probe: (ctx) => probeDitaOt(ctx),
4889
- // None. Every option manni owns is refused under this tool, stdin included.
4890
- ownedOptions: []
4891
- };
4892
-
4893
- // src/lint/tools/index.ts
4894
- var manni = {
4895
- name: "manni",
4896
- label: "manni's own structure engine",
4897
- formats: () => listFormats(),
4898
- walkExtensions: () => supportedExtensions(),
4899
- // Nothing to look for: the engine is this package, so it is available
4900
- // wherever the CLI is. A tool that shells out answers this by looking.
4901
- probe: () => Promise.resolve({ available: true, version: package_default.version }),
4902
- // `-` is stdin, and it is in this list so the derivation in `lint.ts` has
4903
- // one place to learn that manni owns it. The refusal itself is not shaped
4904
- // like the others, because "option" is the wrong word for a positional:
4905
- // `assertOptionsOwned` special-cases it to say "cannot read stdin" instead.
4906
- // A tool that copies this list without reading that guard gets the right
4907
- // ownership and the wrong sentence.
4908
- ownedOptions: ["--template", "--templates", "--explain", "--as", "-"]
4909
- };
4910
- var descriptors = /* @__PURE__ */ new Map([
4911
- [manni.name, manni],
4912
- [ditaOt.name, ditaOt]
4913
- ]);
4914
- function structureTools() {
4915
- return [...descriptors.values()];
4916
- }
4917
- function resolveStructureTool(name) {
4918
- const descriptor = descriptors.get(name);
4919
- if (descriptor) return descriptor;
4920
- throw new LintError(
4921
- `Unknown --tool "${name}" for structure. Use ${TOOLS_BY_JOB.structure.join(", ")}.`
4922
- );
4923
- }
4924
-
4925
- // src/lint/commands/lint.ts
4926
- var STDIN_LABEL = "<stdin>";
4927
- var TEMPLATE_FILE_EXTENSIONS = [".yaml", ".yml", ".json"];
4928
- async function asLintError(run) {
4929
- try {
4930
- return await run();
4931
- } catch (err) {
4932
- if (err instanceof LintError) throw err;
4933
- if (err instanceof ToolError) throw new LintError(err.message);
4934
- throw err;
4935
- }
4936
- }
4937
- function templateFiles(value) {
4938
- if (value == null) return [];
4939
- return Array.isArray(value) ? value : [value];
4940
- }
4941
- var TOOL_OPTIONS = [
4942
- { spelling: "--template", given: (o) => o.template !== void 0 },
4943
- {
4944
- spelling: "--templates",
4945
- given: (o) => templateFiles(o.templates).length > 0
4946
- },
4947
- { spelling: "--explain", given: (o) => o.explain === true },
4948
- { spelling: "--as", given: (o) => o.as !== void 0 }
4949
- ];
4950
- function foreignOptions(tool) {
4951
- const foreign = /* @__PURE__ */ new Map();
4952
- for (const other of structureTools()) {
4953
- if (other === tool) continue;
4954
- for (const spelling of other.ownedOptions) {
4955
- if (tool.ownedOptions.includes(spelling)) continue;
4956
- if (!foreign.has(spelling)) foreign.set(spelling, other);
4957
- }
4958
- }
4959
- return foreign;
4960
- }
4961
- function assertOptionsOwned(opts, tool, usingStdin) {
4962
- const foreign = foreignOptions(tool);
4963
- if (foreign.size === 0) return;
4964
- for (const { spelling, given } of TOOL_OPTIONS) {
4965
- const owner = foreign.get(spelling);
4966
- if (owner === void 0 || !given(opts)) continue;
4967
- throw new LintError(
4968
- `${spelling} is an option of the ${owner.name} tool; the structure job's tool is ${tool.name}.`
4969
- );
4970
- }
4971
- if (usingStdin && foreign.has(STDIN_TOKEN)) {
4972
- throw new LintError(`${tool.name} cannot read stdin. Name files or a map.`);
4973
- }
4974
- }
4975
- function origin3() {
4976
- return {
4977
- start: { line: 1, column: 1, offset: 0 },
4978
- end: { line: 1, column: 1, offset: 0 }
4979
- };
4980
- }
4981
- function skip(file, reason, cause = "unsupported-format") {
4982
- return {
4983
- file,
4984
- // `success` means "linted and produced no findings". A file that was never
4985
- // linted is neither passing nor failing, and calling it a pass would hide
4986
- // the gap from anyone reading CI output.
4987
- success: false,
4988
- findings: [],
4989
- template: null,
4990
- skipped: cause,
4991
- reason
4992
- };
4993
- }
4994
- function parseFinding(message) {
4995
- return {
4996
- type: "parse_error",
4997
- heading: null,
4998
- message,
4999
- position: origin3(),
5000
- severity: "error"
5001
- };
5002
- }
5003
- function templateLoader() {
5004
- const cache = /* @__PURE__ */ new Map();
5005
- return (ref) => {
5006
- let pending = cache.get(ref);
5007
- if (!pending) {
5008
- pending = loadResolvedTemplate(ref);
5009
- cache.set(ref, pending);
5010
- }
5011
- return pending;
5012
- };
5013
- }
5014
- function cliTemplateRef(template, templatePaths) {
5015
- if (!template) return void 0;
5016
- if (template.includes("#")) return template;
5017
- if (classifyRef(template).kind !== "file") return template;
5018
- if (template.includes("/") || template.includes("\\")) return template;
5019
- if (TEMPLATE_FILE_EXTENSIONS.some((e) => template.toLowerCase().endsWith(e))) {
5020
- return template;
5021
- }
5022
- if (templatePaths.length === 0) return template;
5023
- return `${templatePaths[templatePaths.length - 1] ?? ""}#${template}`;
5024
- }
5025
- function templateIdentity(ref) {
5026
- const hash = ref.lastIndexOf("#");
5027
- if (hash === -1) return { source: ref, template: ref };
5028
- return { source: ref.slice(0, hash), template: ref.slice(hash + 1) };
5029
- }
5030
- function unknownTypeFinding(resolution, typeIndex, position) {
5031
- const suggestions = resolution.suggestions ?? [];
5032
- const known = resolution.knownTypes ?? knownTypes(typeIndex);
5033
- const hint = suggestions.length ? ` Did you mean ${suggestions.map((s) => `"${s}"`).join(", ")}?` : ` Known doctypes: ${known.join(", ") || "(none)"}.`;
5034
- const declared = resolution.unknownType ?? "(none recorded)";
5035
- return {
5036
- type: "unknown_type",
5037
- heading: null,
5038
- message: `No template serves type "${declared}".${hint} Declare it on a template with "types:", then pass that file with --templates.`,
5039
- position,
5040
- severity: "error"
5041
- };
5042
- }
5043
- async function lintOne(label, content, parser, ctx) {
5044
- let tree;
5045
- try {
5046
- tree = parser.parse(content, label);
5047
- } catch (err) {
5048
- return {
5049
- file: label,
5050
- success: false,
5051
- findings: [parseFinding(errorMessage(err))],
5052
- template: null
5053
- };
5054
- }
5055
- const resolution = resolveTemplateRef({
5056
- filePath: label,
5057
- absolutePath: ctx.absoluteOf?.(label),
5058
- frontmatter: tree.frontmatter,
5059
- cliTemplate: ctx.cliTemplate,
5060
- overrides: ctx.overrides,
5061
- typeIndex: ctx.typeIndex,
5062
- defaultTemplate: ctx.defaultTemplate
5063
- });
5064
- const withResolution = (result) => ctx.explain ? { ...result, resolution } : result;
5065
- const metaPosition = tree.frontmatterPosition ?? origin3();
5066
- if (resolution.ref === null) {
5067
- if (resolution.cause === "unknown-type") {
5068
- return withResolution({
5069
- file: label,
5070
- success: false,
5071
- findings: [unknownTypeFinding(resolution, ctx.typeIndex, metaPosition)],
5072
- template: null
5073
- });
5074
- }
5075
- return withResolution(
5076
- skip(
5077
- label,
5078
- "no type in frontmatter and no template resolved",
5079
- "no-template"
5080
- )
5081
- );
5082
- }
5083
- const brokenTemplate = (err) => withResolution({
5084
- file: label,
5085
- success: false,
5086
- findings: [
5087
- {
5088
- type: "template_error",
5089
- heading: null,
5090
- message: errorMessage(err),
5091
- position: metaPosition,
5092
- severity: "error"
5093
- }
5094
- ],
5095
- template: resolution.ref
5096
- });
5097
- if (resolution.stage === "frontmatter-template" && classifyRef(resolution.ref).kind === "url") {
5098
- return brokenTemplate(
5099
- new LintError(
5100
- `${FILE_TEMPLATE_KEY} may not name a URL ("${resolution.ref}"): a document must not choose what the linter fetches. Declare it in manni.config.yaml under "templates:" or "types:", or pass it with --template.`
5101
- )
5102
- );
5103
- }
5104
- const ref = resolution.stage === "frontmatter-template" ? refRelativeTo(ctx.absoluteOf?.(label) ?? label, resolution.ref) : resolution.ref;
5105
- let template;
5106
- try {
5107
- template = await ctx.getTemplate(ref);
5108
- } catch (err) {
5109
- return brokenTemplate(err);
5110
- }
5111
- if (ctx.explain) {
5112
- return withResolution({
5113
- file: label,
5114
- success: true,
5115
- findings: [],
5116
- template: resolution.ref,
5117
- // The tree and the loaded template, for the alignment block. Both are
5118
- // already in hand here, and neither survives this function otherwise.
5119
- alignment: { tree, template }
5120
- });
5121
- }
5122
- let findings;
5123
- try {
5124
- findings = validateDocument(tree, template, {
5125
- ...templateIdentity(ref),
5126
- kinds: parser.kinds
5127
- });
5128
- } catch (err) {
5129
- if (err instanceof LintError) return brokenTemplate(err);
5130
- throw err;
5131
- }
5132
- return withResolution({
5133
- file: label,
5134
- // "Linted and found nothing wrong", which is not the same as "produced no
5135
- // findings". A `warning` - today, a rule this file's format cannot answer -
5136
- // says something about the run rather than about the document, and the
5137
- // exit code follows `success`, so counting it as a failure would turn a
5138
- // docset of mixed formats permanently red. The family's rule, and the one
5139
- // meta and cite already apply: `ok` is "no error-severity finding".
5140
- success: !findings.some(isErrorSeverity),
5141
- findings,
5142
- template: resolution.ref
5143
- });
5144
- }
5145
- async function lintWithManni(run) {
5146
- const { opts, config, cwd, files, forcedParser, usingStdin } = run;
5147
- const templatePaths = templateFiles(opts.templates ?? config.templates);
5148
- const getTemplate = templateLoader();
5149
- const userFiles = [];
5150
- for (const ref of templatePaths) {
5151
- const file = await loadTemplateFile(ref);
5152
- const templates = {};
5153
- for (const name of Object.keys(file.templates ?? {})) {
5154
- templates[name] = await getTemplate(`${ref}#${name}`);
5155
- }
5156
- userFiles.push({ ref, file: { ...file, templates } });
5157
- }
5158
- const typeIndex = buildTypeIndex({
5159
- builtins: listBuiltins(),
5160
- userFiles,
5161
- explicitTypes: opts.types ?? config.types
5162
- });
5163
- const ctx = {
5164
- getTemplate,
5165
- absoluteOf: (file) => resolve2(cwd, file).replace(/\\/g, "/"),
5166
- typeIndex,
5167
- cliTemplate: cliTemplateRef(opts.template, templatePaths),
5168
- overrides: opts.overrides ?? config.overrides,
5169
- // `template:` in config is the default for a page that declares no type -
5170
- // the bottom of the chain, not the top. `--template` is the top.
5171
- defaultTemplate: opts.defaultTemplate ?? config.template,
5172
- explain: opts.explain === true
5173
- };
5174
- if (ctx.cliTemplate != null) await getTemplate(ctx.cliTemplate);
5175
- const results = [];
5176
- if (usingStdin && forcedParser) {
5177
- results.push(
5178
- await lintOne(STDIN_LABEL, opts.stdinContent ?? "", forcedParser, ctx)
5179
- );
5180
- }
5181
- for (const file of files) {
5182
- const ext = extname(file);
5183
- const parser = forcedParser ?? parserForExtension(ext);
5184
- if (!parser) {
5185
- results.push(
5186
- skip(
5187
- file,
5188
- `no parser is registered for "${ext || file}". Supported extensions: ${supportedExtensions().join(", ")}. Use --as to override.`
5189
- )
5190
- );
5191
- continue;
5192
- }
5193
- let content;
5194
- try {
5195
- content = await readFile3(resolve2(cwd, file), "utf8");
5196
- } catch (err) {
5197
- results.push(
5198
- skip(file, `could not be read: ${errorMessage(err)}`, "unreadable")
5199
- );
5200
- continue;
5201
- }
5202
- results.push(await lintOne(file, content, parser, ctx));
5203
- }
5204
- return results;
5205
- }
5206
- function ditaOtFinding(message) {
5207
- const point = message.line === void 0 ? null : {
5208
- line: message.line,
5209
- // The log calls the column `row`; see `readMessage` in the seam.
5210
- column: message.column ?? 1,
5211
- // DITA-OT reports no byte offset, and nothing downstream needs one.
5212
- offset: 0
5213
- };
5214
- return {
5215
- type: message.code,
5216
- heading: null,
5217
- message: message.message,
5218
- position: point === null ? origin3() : { start: point, end: point },
5219
- severity: message.severity
5220
- };
5221
- }
5222
- async function lintWithDitaOt(run) {
5223
- const { cwd, files, onNotice } = run;
5224
- const readable = ditaOt.formats().flatMap((format) => format.extensions);
5225
- const reads = new Set(readable.map((ext) => ext.toLowerCase()));
5226
- const targets = [];
5227
- const unsupported = [];
5228
- for (const file of files) {
5229
- if (reads.has(extname(file).toLowerCase())) targets.push(file);
5230
- else {
5231
- unsupported.push(
5232
- skip(
5233
- file,
5234
- `dita-ot does not read "${extname(file) || file}". It reads ${readable.join(", ")}.`
5235
- )
5236
- );
5237
- }
5238
- }
5239
- const labels = /* @__PURE__ */ new Map();
5240
- for (const file of targets) {
5241
- const absolute = resolve2(cwd, file);
5242
- if (!labels.has(absolute)) labels.set(absolute, file);
5243
- }
5244
- const unique = [...labels.values()];
5245
- const labelOf = (absolute) => {
5246
- const known = labels.get(absolute);
5247
- if (known !== void 0) return known;
5248
- const near = relative(cwd, absolute).replace(/\\/g, "/");
5249
- return near === "" ? absolute : near;
5250
- };
5251
- for (const file of unique) {
5252
- if (extname(file).toLowerCase() !== ".ditamap") {
5253
- onNotice?.(
5254
- `checked ${file} without a map; references outside it are not resolved.`
5255
- );
5256
- }
5257
- }
5258
- if (unique.length === 0) return unsupported;
5259
- const results = await run.validate({
5260
- targets: [...labels.keys()],
5261
- cwd,
5262
- ...run.home === void 0 ? {} : { home: run.home }
5263
- });
5264
- const byFile = new Map(unique.map((file) => [file, []]));
5265
- const findingsFor = (file) => {
5266
- let list = byFile.get(file);
5267
- if (list === void 0) {
5268
- list = [];
5269
- byFile.set(file, list);
5270
- }
5271
- return list;
5272
- };
5273
- for (const { target, messages } of results) {
5274
- const invoked = labelOf(target);
5275
- for (const message of messages) {
5276
- const file = message.file === void 0 ? invoked : labelOf(resolve2(cwd, message.file));
5277
- findingsFor(file).push(ditaOtFinding(message));
5278
- }
5279
- }
5280
- return [
5281
- ...unsupported,
5282
- ...[...byFile].map(([file, findings]) => ({
5283
- file,
5284
- // The family's rule, as manni's branch applies it: `ok` is "no
5285
- // error-severity finding", so a warning-only file stays green.
5286
- success: !findings.some(isErrorSeverity),
5287
- findings,
5288
- template: null
5289
- }))
5290
- ];
5291
- }
5292
- function assertSomethingWasChecked(results, explain, tool) {
5293
- const skipped = results.filter((r) => r.skipped != null).length;
5294
- const checked = results.length - skipped;
5295
- if (explain || checked > 0 || skipped === 0) return;
5296
- const unrouted = results.filter((r) => r.skipped === "no-template").length;
5297
- const unsupported = results.filter(
5298
- (r) => r.skipped === "unsupported-format"
5299
- ).length;
5300
- const unreadable = results.filter((r) => r.skipped === "unreadable").length;
5301
- const advice = [
5302
- unrouted > 0 ? `${unrouted} had no template: give a page a "type:" that a template serves, pass -t/--template <ref>, or set "lint.template" as a default.` : null,
5303
- unsupported > 0 ? (
5304
- // `--as` belongs to manni's engine, so offering it to someone running
5305
- // another tool is advice that answers with a usage error. The formats
5306
- // line is true for every tool, because every tool declares one.
5307
- `${unsupported} ${unsupported === 1 ? "is" : "are"} in a format ${tool.name} does not read: ` + (tool.ownedOptions.includes("--as") ? `pass --as <format> to force one, or target files in a format ` : `target files in a format `) + `"manni lint tools" lists.`
5308
- ) : null,
5309
- unreadable > 0 ? `${unreadable} could not be read: check the permissions on those paths, or drop them from the run with --exclude <glob>.` : null
5310
- ].filter((line) => line !== null).join(" ");
5311
- throw new LintError(
5312
- `Nothing was checked: all ${skipped} file(s) were skipped. ${advice} Run "manni lint structure <paths> --explain" to see how each file resolved.`
5313
- );
5314
- }
5315
- async function runLint(opts) {
5316
- const run = await resolveLintRun({
5317
- cwd: opts.cwd ?? process.cwd(),
5318
- configPath: opts.configPath,
5319
- noConfig: opts.noConfig,
5320
- inputs: opts.inputs,
5321
- collection: opts.collection,
5322
- onConfigLoaded: opts.onConfigLoaded
5323
- });
5324
- const config = run.config;
5325
- const cwd = run.base;
5326
- if (run.inputs.length === 0) {
5327
- throw new LintError(
5328
- "No files to check. Pass paths/globs, or declare a collection under `collections:` in manni.config.yaml."
5329
- );
5330
- }
5331
- const tool = resolveStructureTool(
5332
- opts.tool ?? config.structure?.tool ?? "manni"
5333
- );
5334
- const usingStdin = run.inputs.includes(STDIN_TOKEN);
5335
- assertOptionsOwned(opts, tool, usingStdin);
5336
- const forcedParser = opts.as != null ? parserByName(opts.as) : void 0;
5337
- if (opts.as != null && !forcedParser) {
5338
- throw new LintError(
5339
- `Unknown format "${opts.as}". Run "manni lint tools" to see the formats manni lint reads.`
5340
- );
5341
- }
5342
- if (usingStdin && !forcedParser) {
5343
- throw new LintError(
5344
- "Reading from stdin (-) requires --as <format> to choose a parser."
5345
- );
5346
- }
5347
- const fileInputs = run.inputs.filter((input) => input !== STDIN_TOKEN);
5348
- const exts = opts.exts ?? forcedParser?.extensions ?? tool.walkExtensions();
5349
- const allowEmpty = opts.allowEmpty ?? config.allowEmpty;
5350
- const exclude = [
5351
- .../* @__PURE__ */ new Set([
5352
- ...opts.exclude ?? [],
5353
- ...run.fromCollections ? run.collections.flatMap((c) => c.exclude) : []
5354
- ])
5355
- ];
5356
- const { files, gitignoreSkipped } = await asLintError(
5357
- () => resolveTargetSet({
5358
- inputs: fileInputs,
5359
- exts,
5360
- exclude,
5361
- cwd,
5362
- allowEmpty,
5363
- ...gitignoreOptions({
5364
- flag: opts.respectGitignore,
5365
- onNotice: opts.onNotice
5366
- })
5367
- })
5368
- );
5369
- await asLintError(() => {
5370
- assertNonEmpty({
5371
- files,
5372
- inputs: fileInputs,
5373
- usingStdin,
5374
- allowEmpty,
5375
- exclude,
5376
- exts,
5377
- gitignoreSkipped,
5378
- action: "linted"
5379
- });
5380
- });
5381
- const name = tool.name;
5382
- let results;
5383
- if (name === "manni") {
5384
- results = await lintWithManni({
5385
- opts,
5386
- config,
5387
- cwd,
5388
- files,
5389
- forcedParser,
5390
- usingStdin
5391
- });
5392
- } else if (name === "dita-ot") {
5393
- const home = ditaOtHome(run.tools, run.configDir ?? cwd);
5394
- results = await lintWithDitaOt({
5395
- cwd,
5396
- files,
5397
- ...home === void 0 ? {} : { home },
5398
- ...opts.onNotice === void 0 ? {} : { onNotice: opts.onNotice },
5399
- validate: opts.runDitaOt ?? runDitaOtValidate
5400
- });
5401
- } else {
5402
- throw new LintError(`No implementation is registered for the ${name} tool.`);
5403
- }
5404
- assertSomethingWasChecked(results, opts.explain === true, tool);
5405
- const skipped = results.filter((r) => r.skipped != null).length;
5406
- const failed = results.filter((r) => r.skipped == null && !r.success).length;
5407
- const checked = results.length - skipped;
5408
- return {
5409
- results,
5410
- summary: { checked, passed: checked - failed, failed, skipped }
5411
- };
5412
- }
5413
-
5414
3243
  export {
5415
3244
  parserForExtension,
5416
3245
  parserByName,
5417
3246
  supportedExtensions,
5418
- listFormats,
5419
- listBuiltins,
5420
- classifyRef,
5421
- clearTemplateCaches,
5422
- loadTemplateFile,
5423
- loadTemplate,
5424
- resolveExtends,
5425
- clearMatcherCache,
5426
- validateSections,
5427
- validateDocument,
5428
- LINT_TOOLS,
5429
- LINT_JOBS,
5430
- TOOLS_BY_JOB,
5431
- isLintTool,
5432
- parseConfig,
5433
- loadConfig,
5434
- rebaseConfig,
5435
- resolveLintRun,
5436
- resolveStructureTool,
5437
- STDIN_LABEL,
5438
- runLint
3247
+ unsupportedFormatMessage,
3248
+ listFormats
5439
3249
  };
5440
- //# sourceMappingURL=chunk-WB3QM6PP.js.map
3250
+ //# sourceMappingURL=chunk-ELZ2IIOF.js.map