for-humanity 0.11.0 → 0.13.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 (226) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +16 -9
  3. package/dist/browser.js +16 -16
  4. package/dist/cli.css +2 -941
  5. package/dist/cli.js +92 -2290
  6. package/dist/config.js +1 -0
  7. package/dist/constant/copy.d.ts +100 -0
  8. package/dist/constant/site.d.ts +20 -0
  9. package/dist/constant/status.d.ts +19 -0
  10. package/dist/copy-C1uzvEnr.js +103 -0
  11. package/dist/group-path-XAThmzvC.js +10 -0
  12. package/dist/load-site-CbpLEhwu.js +2379 -0
  13. package/dist/logger-Dv1i9xSV.js +9 -0
  14. package/dist/mermaid/THIRD-PARTY-NOTICES.txt +3134 -0
  15. package/dist/mermaid/chunk--TN-qHgG.js +2 -0
  16. package/dist/mermaid/chunk-0kM1Lv4D.js +2 -0
  17. package/dist/mermaid/chunk-3FHei3pH.js +2001 -0
  18. package/dist/mermaid/chunk-3_2p2GqN.js +2 -0
  19. package/dist/mermaid/chunk-6HLei9i0.js +84 -0
  20. package/dist/mermaid/chunk-7Sz3kAwl.js +2214 -0
  21. package/dist/mermaid/chunk-7Tarqbpa.js +18047 -0
  22. package/dist/mermaid/chunk-9kkt8lzL.js +10 -0
  23. package/dist/mermaid/chunk-B--pNBQF.js +884 -0
  24. package/dist/mermaid/chunk-B4F-pysn.js +4241 -0
  25. package/dist/mermaid/chunk-B58sEIY_.js +14 -0
  26. package/dist/mermaid/chunk-B9_UWzSH.js +1517 -0
  27. package/dist/mermaid/chunk-BB8uZHwY.js +75 -0
  28. package/dist/mermaid/chunk-BFfk0LEc.js +717 -0
  29. package/dist/mermaid/chunk-BKri0qr7.js +89 -0
  30. package/dist/mermaid/chunk-BL404pH5.js +26 -0
  31. package/dist/mermaid/chunk-BLeiQSWo.js +354 -0
  32. package/dist/mermaid/chunk-BOmH_UFd.js +2 -0
  33. package/dist/mermaid/chunk-BSej9Nhy.js +1361 -0
  34. package/dist/mermaid/chunk-BYzrzvCI.js +12 -0
  35. package/dist/mermaid/chunk-BariMtWr.js +3020 -0
  36. package/dist/mermaid/chunk-BcrBoMwH.js +2352 -0
  37. package/dist/mermaid/chunk-Bok8z-Yo.js +67 -0
  38. package/dist/mermaid/chunk-BpTHLKQM.js +1970 -0
  39. package/dist/mermaid/chunk-BqjuxRsC.js +733 -0
  40. package/dist/mermaid/chunk-BuvPd-s5.js +1073 -0
  41. package/dist/mermaid/chunk-BvVehae3.js +5069 -0
  42. package/dist/mermaid/chunk-C-JABYpd.js +3075 -0
  43. package/dist/mermaid/chunk-C9XlTm4w.js +2 -0
  44. package/dist/mermaid/chunk-CApNbSU9.js +6 -0
  45. package/dist/mermaid/chunk-CCM2AH-C.js +9 -0
  46. package/dist/mermaid/chunk-CD1E0F_N.js +2 -0
  47. package/dist/mermaid/chunk-CDWvKG7D.js +2 -0
  48. package/dist/mermaid/chunk-CEVPpC1F.js +34 -0
  49. package/dist/mermaid/chunk-CLDr12lf.js +47504 -0
  50. package/dist/mermaid/chunk-CLVYqOBI.js +61 -0
  51. package/dist/mermaid/chunk-CLeOkFb5.js +15 -0
  52. package/dist/mermaid/chunk-CLgRAtOz.js +37 -0
  53. package/dist/mermaid/chunk-CMqFNUBl.js +134 -0
  54. package/dist/mermaid/chunk-CTPaBJXB.js +2 -0
  55. package/dist/mermaid/chunk-CVi6IGBL.js +2 -0
  56. package/dist/mermaid/chunk-CWV_EN-4.js +4745 -0
  57. package/dist/mermaid/chunk-CZ0dAs3g.js +2 -0
  58. package/dist/mermaid/chunk-C_YGy4Fn.js +236 -0
  59. package/dist/mermaid/chunk-CcVH22Yt.js +100 -0
  60. package/dist/mermaid/chunk-CgG6CD9_.js +7175 -0
  61. package/dist/mermaid/chunk-CgOP6IDB.js +3397 -0
  62. package/dist/mermaid/chunk-Cgg2UTSf.js +13 -0
  63. package/dist/mermaid/chunk-ChJfJJ1A.js +214 -0
  64. package/dist/mermaid/chunk-Ck5XRA46.js +447 -0
  65. package/dist/mermaid/chunk-Ckuw16cl.js +203 -0
  66. package/dist/mermaid/chunk-CpAbRCbF.js +1880 -0
  67. package/dist/mermaid/chunk-CuYekbSv.js +2 -0
  68. package/dist/mermaid/chunk-CwS4wZ5a.js +2 -0
  69. package/dist/mermaid/chunk-D-bcrBO3.js +82 -0
  70. package/dist/mermaid/chunk-D3IEAkCW.js +76 -0
  71. package/dist/mermaid/chunk-D8XfBJ_K.js +645 -0
  72. package/dist/mermaid/chunk-D9Ddd9y4.js +2228 -0
  73. package/dist/mermaid/chunk-DBh1mIbK.js +12 -0
  74. package/dist/mermaid/chunk-DD6Df6Ic.js +5 -0
  75. package/dist/mermaid/chunk-DGVDaXjx.js +8 -0
  76. package/dist/mermaid/chunk-DK9KqIXj.js +632 -0
  77. package/dist/mermaid/chunk-DORnIAsy.js +4487 -0
  78. package/dist/mermaid/chunk-DU69_VcF.js +363 -0
  79. package/dist/mermaid/chunk-DVNvSFoz.js +2144 -0
  80. package/dist/mermaid/chunk-DZe-4kde.js +20 -0
  81. package/dist/mermaid/chunk-D_a06Rz4.js +287 -0
  82. package/dist/mermaid/chunk-DfyFJJi2.js +972 -0
  83. package/dist/mermaid/chunk-DhKYPp1Z.js +2 -0
  84. package/dist/mermaid/chunk-DjFqgNTG.js +94 -0
  85. package/dist/mermaid/chunk-DmbDDrr-.js +65 -0
  86. package/dist/mermaid/chunk-DpuxlyL_.js +696 -0
  87. package/dist/mermaid/chunk-DqGhppWe.js +2 -0
  88. package/dist/mermaid/chunk-DqPZxHXa.js +5 -0
  89. package/dist/mermaid/chunk-Dt_1Jyif.js +293 -0
  90. package/dist/mermaid/chunk-DyQBE_NW.js +1935 -0
  91. package/dist/mermaid/chunk-JckyC4mW.js +1009 -0
  92. package/dist/mermaid/chunk-K7Myuj2-.js +1334 -0
  93. package/dist/mermaid/chunk-KuPYb48V.js +275 -0
  94. package/dist/mermaid/chunk-MFVRUoO0.js +4020 -0
  95. package/dist/mermaid/chunk-MbInCS_6.js +28 -0
  96. package/dist/mermaid/chunk-MexOLyKV.js +355 -0
  97. package/dist/mermaid/chunk-NnbDtJzg.js +21856 -0
  98. package/dist/mermaid/chunk-SGpOYZbI.js +2139 -0
  99. package/dist/mermaid/chunk-TbxLDlcU.js +198 -0
  100. package/dist/mermaid/chunk-TftHqGLg.js +490 -0
  101. package/dist/mermaid/chunk-UjQ8vA_Z.js +4138 -0
  102. package/dist/mermaid/chunk-YgmVTKqc.js +16 -0
  103. package/dist/mermaid/chunk-_a9EZE1v.js +1798 -0
  104. package/dist/mermaid/chunk-dTb8jqeP.js +2 -0
  105. package/dist/mermaid/chunk-fFdMfF8S.js +2240 -0
  106. package/dist/mermaid/chunk-feYbMzBj.js +1267 -0
  107. package/dist/mermaid/chunk-g3Pn8DkW.js +46 -0
  108. package/dist/mermaid/chunk-hM77LvCb.js +19832 -0
  109. package/dist/mermaid/chunk-hUDb-eAW.js +527 -0
  110. package/dist/mermaid/chunk-tpABtZ-3.js +4252 -0
  111. package/dist/mermaid/chunk-xIbz3oFK.js +59 -0
  112. package/dist/mermaid/chunk-yBz-vst0.js +976 -0
  113. package/dist/mermaid/mermaid-BhtR3wyB.js +1145 -0
  114. package/dist/run-site-command-C80YoNOe.js +129 -0
  115. package/dist/site-config-bllORGpm.js +90 -0
  116. package/dist/site.js +1 -0
  117. package/dist/start-dev-server-C4_Lu1nZ.js +176 -0
  118. package/dist/to-change-queue-DbEnZGNK.js +34 -0
  119. package/dist/to-error-message-pF6_7wGK.js +16 -0
  120. package/dist/type/group-path.d.ts +7 -0
  121. package/dist/type/site-config.d.ts +57 -0
  122. package/package.json +34 -17
  123. package/src/component/widget/cursor-face/_constant/cursor-face.ts +0 -14
  124. package/src/component/widget/cursor-face/wg-cursor-face.css +0 -15
  125. package/src/component/widget/cursor-face/wg-cursor-face.tsx +0 -153
  126. package/src/component/widget/navigation/_constant/locale.ts +0 -9
  127. package/src/component/widget/navigation/_constant/navigation-data-schema.ts +0 -33
  128. package/src/component/widget/navigation/_constant/navigation.ts +0 -9
  129. package/src/component/widget/navigation/_constant/reading-line.ts +0 -4
  130. package/src/component/widget/navigation/_function/restore-navigation-scroll.ts +0 -59
  131. package/src/component/widget/navigation/_function/to-doc-groups/_to-doc-branches.ts +0 -15
  132. package/src/component/widget/navigation/_function/to-doc-groups/_to-doc-group-branches.ts +0 -70
  133. package/src/component/widget/navigation/_function/to-doc-groups/to-doc-groups.ts +0 -15
  134. package/src/component/widget/navigation/_function/to-navigation-data/_to-navigation-document.ts +0 -13
  135. package/src/component/widget/navigation/_function/to-navigation-data/_to-navigation-group.ts +0 -13
  136. package/src/component/widget/navigation/_function/to-navigation-data/to-navigation-data.ts +0 -18
  137. package/src/component/widget/navigation/_function/to-navigation-json.ts +0 -8
  138. package/src/component/widget/navigation/_function/to-toc-groups.ts +0 -38
  139. package/src/component/widget/navigation/_type/doc-branch.ts +0 -11
  140. package/src/component/widget/navigation/_type/doc-group.ts +0 -24
  141. package/src/component/widget/navigation/_type/navigation-data.ts +0 -75
  142. package/src/component/widget/navigation/_type/navigation-focus-snapshot.ts +0 -17
  143. package/src/component/widget/navigation/_type/toc-group.ts +0 -39
  144. package/src/component/widget/navigation/_wg-navigation-nav-list.css +0 -48
  145. package/src/component/widget/navigation/_wg-navigation-nav-list.tsx +0 -61
  146. package/src/component/widget/navigation/_wg-navigation-nav.css +0 -21
  147. package/src/component/widget/navigation/_wg-navigation-nav.tsx +0 -33
  148. package/src/component/widget/navigation/_wg-navigation-scroll.tsx +0 -117
  149. package/src/component/widget/navigation/_wg-navigation-toc.css +0 -91
  150. package/src/component/widget/navigation/_wg-navigation-toc.tsx +0 -114
  151. package/src/component/widget/navigation/wg-navigation.css +0 -282
  152. package/src/component/widget/navigation/wg-navigation.tsx +0 -420
  153. package/src/component/widget/prose/_constant/heading-highlight.ts +0 -4
  154. package/src/component/widget/prose/_constant/section.ts +0 -7
  155. package/src/component/widget/prose/_constant/status.ts +0 -9
  156. package/src/component/widget/prose/_function/has-block-content.ts +0 -30
  157. package/src/component/widget/prose/_function/rehype-head.ts +0 -31
  158. package/src/component/widget/prose/_function/rehype-sections/_to-section-headings.ts +0 -20
  159. package/src/component/widget/prose/_function/rehype-sections/rehype-sections.ts +0 -21
  160. package/src/component/widget/prose/_function/rehype-tables.ts +0 -22
  161. package/src/component/widget/prose/_function/remark-blocks.ts +0 -112
  162. package/src/component/widget/prose/_function/remark-flow.ts +0 -26
  163. package/src/component/widget/prose/_function/remark-links.ts +0 -33
  164. package/src/component/widget/prose/_function/remark-parts.ts +0 -26
  165. package/src/component/widget/prose/_function/remark-status.ts +0 -59
  166. package/src/component/widget/prose/_function/remark-swatch.ts +0 -16
  167. package/src/component/widget/prose/_function/remark-unknown-directives.ts +0 -25
  168. package/src/component/widget/prose/wg-prose.css +0 -454
  169. package/src/component/widget/prose/wg-prose.tsx +0 -43
  170. package/src/component/widget/shell/wg-shell.css +0 -63
  171. package/src/component/widget/shell/wg-shell.tsx +0 -88
  172. package/src/component/widget/shell-controls/wg-shell-controls.css +0 -4
  173. package/src/component/widget/shell-controls/wg-shell-controls.tsx +0 -170
  174. package/src/constant/asset.ts +0 -63
  175. package/src/constant/copy.ts +0 -133
  176. package/src/constant/doc.ts +0 -13
  177. package/src/constant/font.ts +0 -34
  178. package/src/constant/site.ts +0 -24
  179. package/src/constant/status.ts +0 -15
  180. package/src/entry/browser/browser.tsx +0 -22
  181. package/src/entry/cli/_constant/cli.ts +0 -19
  182. package/src/entry/cli/_constant/crawling.ts +0 -14
  183. package/src/entry/cli/_constant/snippet.ts +0 -21
  184. package/src/entry/cli/_function/create-app.tsx +0 -57
  185. package/src/entry/cli/_function/create-dev-app.ts +0 -87
  186. package/src/entry/cli/_function/create-processor/_rehype-assets.ts +0 -70
  187. package/src/entry/cli/_function/create-processor/_rehype-heading-ids.ts +0 -34
  188. package/src/entry/cli/_function/create-processor/_remark-report.ts +0 -15
  189. package/src/entry/cli/_function/create-processor/_remark-snippets.ts +0 -117
  190. package/src/entry/cli/_function/create-processor/_shiki-transformers.ts +0 -55
  191. package/src/entry/cli/_function/create-processor/create-processor.ts +0 -64
  192. package/src/entry/cli/_function/read-docs.ts +0 -128
  193. package/src/entry/cli/_function/read-home.ts +0 -32
  194. package/src/entry/cli/_function/to-crawling-app.ts +0 -42
  195. package/src/entry/cli/cli.test.ts +0 -782
  196. package/src/entry/cli/cli.ts +0 -206
  197. package/src/env.d.ts +0 -6
  198. package/src/page/doc/pg-doc.tsx +0 -35
  199. package/src/page/home/pg-home.tsx +0 -40
  200. package/src/store/navigation/create-navigation-stores.test.ts +0 -74
  201. package/src/store/navigation/create-navigation-stores.ts +0 -59
  202. package/src/store/navigation/navigation-state.ts +0 -36
  203. package/src/store/navigation/navigation-storage.ts +0 -9
  204. package/src/store/navigation/to-navigation-scroll-state.ts +0 -42
  205. package/src/style/base.css +0 -28
  206. package/src/style/token.css +0 -186
  207. package/src/type/doc-content.ts +0 -15
  208. package/src/type/doc-data.ts +0 -38
  209. package/src/type/doc-file-data.ts +0 -21
  210. package/src/type/doc-heading.ts +0 -17
  211. package/src/type/doc-outline.ts +0 -16
  212. package/src/type/doc-section.ts +0 -9
  213. package/src/type/doc.ts +0 -20
  214. package/src/type/group-path.ts +0 -7
  215. package/src/type/site-assets.ts +0 -25
  216. package/src/type/site-config.ts +0 -73
  217. package/src/type/vfile-data.d.ts +0 -10
  218. package/src/util/async/to-change-queue.test.ts +0 -58
  219. package/src/util/async/to-change-queue.ts +0 -43
  220. package/src/util/dom/find-hash-target.ts +0 -18
  221. package/src/util/dom/reveal-hash-target.ts +0 -15
  222. package/src/util/error/to-error-message.ts +0 -10
  223. package/src/util/file/to-asset-path.ts +0 -26
  224. package/src/util/font/to-font-css.test.ts +0 -41
  225. package/src/util/font/to-font-css.ts +0 -28
  226. package/src/util/mermaid/render-flow.js +0 -321
@@ -1,782 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import {EventEmitter} from "node:events";
3
- import {mkdir, mkdtemp, readFile, rm, symlink, writeFile} from "node:fs/promises";
4
- import {tmpdir} from "node:os";
5
- import {join} from "node:path";
6
- import {test} from "node:test";
7
- import {toSSG} from "hono/ssg";
8
- import {navigationDataSchema} from "@/component/widget/navigation/_constant/navigation-data-schema";
9
- import {toDocGroups} from "@/component/widget/navigation/_function/to-doc-groups/to-doc-groups";
10
- import {toNavigationData} from "@/component/widget/navigation/_function/to-navigation-data/to-navigation-data";
11
- import {toNavigationJson} from "@/component/widget/navigation/_function/to-navigation-json";
12
- import {WgNavigation} from "@/component/widget/navigation/wg-navigation";
13
- import {asset_style_path} from "@/constant/asset";
14
- import {createApp} from "@/entry/cli/_function/create-app";
15
- import {createDevApp} from "@/entry/cli/_function/create-dev-app";
16
- import {createProcessor} from "@/entry/cli/_function/create-processor/create-processor";
17
- import {readDocs} from "@/entry/cli/_function/read-docs";
18
- import {readHome} from "@/entry/cli/_function/read-home";
19
- import {toCrawlingApp} from "@/entry/cli/_function/to-crawling-app";
20
- import {docDataSchema} from "@/type/doc-data";
21
- import {siteConfigSchema} from "@/type/site-config";
22
-
23
- /**
24
- * 브라우저와 같은 공개 탐색 컴포넌트에서 문서 목록만 확인
25
- */
26
- const toNavigationHtml = (options: Parameters<typeof toNavigationData>[0]) => {
27
- const markup = WgNavigation({data: toNavigationData(options)}).toString();
28
- const navigation = markup.match(/<nav[^>]*class="wg_navigationNav__root"[\s\S]*?<\/nav>/)?.[0];
29
- assert.ok(navigation);
30
- return navigation;
31
- };
32
-
33
- test("navigation payload excludes document bodies and safely embeds authored labels", async (t) => {
34
- const root = await mkdtemp(join(tmpdir(), "for-humanity-navigation-"));
35
- t.after(() => rm(root, {recursive: true, force: true}));
36
- const title = "</script><script>window.injected=true</script>\u2028\u2029";
37
- const site = siteConfigSchema.parse({title});
38
- await writeFile(join(root, "guide.md"), "---\nname: Guide\nlabel: Guide\ngroup: Guide\n---\n\n## Read\n\nPrivate body marker.\n");
39
- const docs = await readDocs({root, processor: createProcessor({site, root})});
40
- const data = toNavigationData({site, docs, current: "guide"});
41
- const json = toNavigationJson(data);
42
-
43
- assert.equal(data.title, title);
44
- assert.equal(data.groups[0].docs[0].active, true);
45
- assert.doesNotMatch(json, /Private body marker|<|\u2028|\u2029/);
46
- assert.deepEqual(JSON.parse(json), data);
47
- assert.deepEqual(navigationDataSchema.parse(JSON.parse(json)), data);
48
- });
49
-
50
- test("page resource URLs use the supplied content fingerprints instead of fixed cache keys", async () => {
51
- const site = siteConfigSchema.parse({});
52
- const assets = {fontCss: "", preload: [], reload: false, browserScript: "", stylePath: "/_fh/style.abc123.css"};
53
- const app = createApp({site, store: {docs: []}, assets});
54
- const html = await (await app.request("/")).text();
55
-
56
- assert.match(html, /rel="stylesheet" href="\/_fh\/style\.abc123\.css"/);
57
- assert.doesNotMatch(html, /<script\b[^>]*\bsrc=/, "browser behavior must share one inline JSX entry");
58
- assert.doesNotMatch(html, /(?:href|src)="\/_fh\/(?:style\.css|client\.js)"/);
59
- });
60
-
61
- test("public site URL is opt-in and normalizes an HTTP(S) root URL", () => {
62
- assert.deepEqual(siteConfigSchema.parse({url: "https://docs.example.org/"}), {...siteConfigSchema.parse({}), url: "https://docs.example.org"});
63
- assert.deepEqual(siteConfigSchema.parse({url: "http://localhost:4321"}), {...siteConfigSchema.parse({}), url: "http://localhost:4321"});
64
- assert.equal("url" in siteConfigSchema.parse({}), false);
65
- });
66
-
67
- test("site port defaults to 4321 and accepts only integers in the TCP range", () => {
68
- assert.equal(siteConfigSchema.parse({}).port, 4321);
69
- assert.equal(siteConfigSchema.parse({port: 4322}).port, 4322);
70
-
71
- for (const port of [0, 65536, 4321.5, "4321"]) {
72
- assert.equal(siteConfigSchema.safeParse({port}).success, false, String(port));
73
- }
74
- });
75
-
76
- test("code notation marks lines, meta highlights lines, and line-numbers tags the block", async () => {
77
- const root = await mkdtemp(join(tmpdir(), "for-humanity-code-notation-"));
78
-
79
- try {
80
- await writeFile(
81
- join(root, "guide.md"),
82
- "---\nname: Guide\nlabel: Guide\ngroup: Guide\n---\n\n## Read\n\n```ts line-numbers\nconst a = 1; // [!code error]\nconst b = 2; // [!code ++]\nconst c = 3;\n```\n\n```txt\n# [!code highlight]\nfirst\nsecond\n```\n\n```css {2}\n.a {\n color: red;\n}\n```\n",
83
- );
84
- const site = siteConfigSchema.parse({});
85
- const [doc] = await readDocs({root, processor: createProcessor({site, root})});
86
-
87
- assert.ok(doc);
88
- assert.match(doc.html, /<pre class="[^"]*wg_prose__lineNumbers[^"]*"/);
89
- assert.match(doc.html, /<span class="line highlighted error">/);
90
- assert.match(doc.html, /<span class="line diff add">/);
91
- assert.match(doc.html, /<span class="line highlighted">[^\n]*first/);
92
- assert.equal(doc.html.includes("[!code"), false, "notation comments are removed from the output");
93
- assert.equal(doc.html.split('<span class="line highlighted">').length - 1, 2, "a standalone txt notation line and the css {2} meta each highlight one line");
94
- } finally {
95
- await rm(root, {recursive: true, force: true});
96
- }
97
- });
98
-
99
- test("code snippets import a file or a region from inside the document root and pass fence meta", async () => {
100
- const root = await mkdtemp(join(tmpdir(), "for-humanity-snippets-"));
101
-
102
- try {
103
- await mkdir(join(root, "snippet"));
104
- await writeFile(
105
- join(root, "snippet", "example.ts"),
106
- "const outside = 0;\n// #region picked\n const a = 1; // [!code error]\n const b = 2;\n// #endregion picked\nconst after = 3;\n",
107
- );
108
- await writeFile(
109
- join(root, "guide.md"),
110
- "---\nname: Guide\nlabel: Guide\ngroup: Guide\n---\n\n## Read\n\n<<< ./snippet/example.ts#picked bad line-numbers\n\n<<< ./snippet/example.ts\n",
111
- );
112
- const site = siteConfigSchema.parse({});
113
- const snippets = new Set<string>();
114
- const [doc] = await readDocs({root, processor: createProcessor({site, root, snippets})});
115
-
116
- assert.ok(doc);
117
- assert.match(doc.html, /<pre class="[^"]*wg_prose__lineNumbers[^"]*wg_prose__code--bad[^"]*"|<pre class="[^"]*wg_prose__code--bad[^"]*wg_prose__lineNumbers[^"]*"/);
118
- assert.match(doc.html, /<span class="line highlighted error">[^\n]*const<\/span>[^\n]* a/);
119
- assert.equal(doc.html.includes("#region"), false, "region marker lines are removed");
120
- assert.equal(doc.html.split("const</span>").length - 1, 2 + 4, "the region block has two lines and the whole-file block four");
121
- assert.deepEqual(
122
- [...snippets].map((entry) => entry.endsWith(join("snippet", "example.ts"))),
123
- [true],
124
- );
125
- } finally {
126
- await rm(root, {recursive: true, force: true});
127
- }
128
- });
129
-
130
- test("code snippets reject files outside the document root, missing files and missing regions", async () => {
131
- const root = await mkdtemp(join(tmpdir(), "for-humanity-snippets-reject-"));
132
-
133
- try {
134
- await mkdir(join(root, "docs"));
135
- await writeFile(join(root, "outside.ts"), "const x = 1;\n");
136
- await writeFile(join(root, "docs", "present.ts"), "const y = 2;\n");
137
- const site = siteConfigSchema.parse({});
138
- const docsRoot = join(root, "docs");
139
-
140
- for (const [reference, expected] of [
141
- ["../outside.ts", /문서 폴더 밖/],
142
- ["./missing.ts", /파일이 없다/],
143
- ["./present.ts#nope", /region 이 없다/],
144
- ] as const) {
145
- await writeFile(join(docsRoot, "guide.md"), `---\nname: Guide\nlabel: Guide\ngroup: Guide\n---\n\n<<< ${reference}\n`);
146
- await assert.rejects(readDocs({root: docsRoot, processor: createProcessor({site, root: docsRoot})}), expected, reference);
147
- }
148
- } finally {
149
- await rm(root, {recursive: true, force: true});
150
- }
151
- });
152
-
153
- test("cursor face is off unless the site turns it on", () => {
154
- assert.equal(siteConfigSchema.parse({}).cursorFace, false);
155
- assert.equal(siteConfigSchema.parse({cursorFace: true}).cursorFace, true);
156
- assert.equal(siteConfigSchema.safeParse({cursorFace: "true"}).success, false);
157
- });
158
-
159
- test("public site URL rejects unsupported protocols, credentials and non-root URLs", () => {
160
- for (const url of [
161
- "",
162
- "/",
163
- "file:///docs",
164
- "ftp://docs.example.org",
165
- "https://user:password@docs.example.org",
166
- "https://docs.example.org/docs/",
167
- "https://docs.example.org/?q=guide",
168
- "https://docs.example.org/#guide",
169
- ]) {
170
- assert.equal(siteConfigSchema.safeParse({url}).success, false, url);
171
- }
172
- });
173
-
174
- test("crawler files are absent when the public site URL is omitted", async () => {
175
- const site = siteConfigSchema.parse({});
176
- const assets = {fontCss: "", preload: [], reload: false, browserScript: "", stylePath: asset_style_path};
177
- const app = createDevApp({pages: createApp({site, store: {docs: []}, assets}), files: new Map(), reload: new EventEmitter()});
178
-
179
- assert.equal((await app.request("/robots.txt")).status, 404);
180
- assert.equal((await app.request("/sitemap.xml")).status, 404);
181
- });
182
-
183
- test("crawler files list home and encoded nested document URLs with the current document store", async (t) => {
184
- const root = await mkdtemp(join(tmpdir(), "for-humanity-crawling-"));
185
- t.after(() => rm(root, {recursive: true, force: true}));
186
- await mkdir(join(root, "guide"));
187
- await writeFile(join(root, "guide.md"), "---\nname: Start\nlabel: Start\ngroup: Guide\n---\n\nStart here.\n");
188
- await writeFile(join(root, "guide/한국어 & tools.md"), "---\nname: Guide\nlabel: Guide\ngroup: Guide\n---\n\n## Read\n\nText.\n");
189
- const site = siteConfigSchema.parse({url: "https://docs.example.org/"});
190
- const docs = await readDocs({root, processor: createProcessor({site, root})});
191
- const store = {docs};
192
- const assets = {fontCss: "", preload: [], reload: false, browserScript: "", stylePath: asset_style_path};
193
- assert.ok(site.url);
194
- const app = createDevApp({pages: createApp({site, store, assets}), crawling: toCrawlingApp({url: site.url, store}), files: new Map(), reload: new EventEmitter()});
195
- const robots = await app.request("/robots.txt");
196
- const sitemap = await app.request("/sitemap.xml");
197
-
198
- assert.equal((await app.request("/guide/")).status, 200, "crawler routes must preserve ordinary document routes");
199
- assert.equal((await app.request(`/guide/${encodeURIComponent("한국어 & tools")}/`)).status, 200, "crawler routes must preserve nested document routes");
200
- assert.equal(robots.status, 200);
201
- assert.match(String(robots.headers.get("content-type")), /^text\/plain/);
202
- assert.equal(await robots.text(), "User-agent: *\nAllow: /\n\nSitemap: https://docs.example.org/sitemap.xml\n");
203
- assert.equal(sitemap.status, 200);
204
- assert.match(String(sitemap.headers.get("content-type")), /^application\/xml/);
205
- const xml = await sitemap.text();
206
- assert.match(xml, /^<\?xml version="1\.0" encoding="UTF-8"\?>/);
207
- assert.match(xml, /<urlset xmlns="http:\/\/www\.sitemaps\.org\/schemas\/sitemap\/0\.9">/);
208
- assert.match(xml, /<loc>https:\/\/docs\.example\.org\/<\/loc>/);
209
- assert.ok(xml.includes(`<loc>https://docs.example.org/guide/${encodeURIComponent("한국어 & tools")}/</loc>`));
210
- assert.equal([...xml.matchAll(/<loc>/g)].length, 3);
211
- assert.doesNotMatch(xml, /<lastmod>|<changefreq>|<priority>|robots\.txt|sitemap\.xml|_fh|#read/);
212
-
213
- store.docs = [];
214
- assert.equal([...(await (await app.request("/sitemap.xml")).text()).matchAll(/<loc>/g)].length, 1);
215
- });
216
-
217
- test("SSG writes crawler files and preserves ordinary and nested document pages", async (t) => {
218
- const root = await mkdtemp(join(tmpdir(), "for-humanity-crawling-build-"));
219
- t.after(() => rm(root, {recursive: true, force: true}));
220
- await mkdir(join(root, "guide"));
221
- await writeFile(join(root, "guide.md"), "---\nname: Start\nlabel: Start\ngroup: Guide\n---\n\nStart here.\n");
222
- await writeFile(join(root, "guide/details.md"), "---\nname: Details\nlabel: Details\ngroup: Guide\n---\n\nNested details.\n");
223
- const site = siteConfigSchema.parse({url: "https://docs.example.org"});
224
- const docs = await readDocs({root, processor: createProcessor({site, root})});
225
- const assets = {fontCss: "", preload: [], reload: false, browserScript: "", stylePath: asset_style_path};
226
- const app = createApp({site, store: {docs}, assets});
227
- const result = await toSSG(app, {writeFile, mkdir}, {dir: root});
228
- assert.ok(site.url);
229
- const crawlerResult = await toSSG(toCrawlingApp({url: site.url, store: {docs}}), {writeFile, mkdir}, {dir: root});
230
-
231
- assert.equal(result.success, true);
232
- assert.equal(crawlerResult.success, true);
233
- assert.equal(result.files.length + crawlerResult.files.length, 5, "home, both documents and both crawler files must be emitted");
234
- assert.match(await readFile(join(root, "guide/index.html"), "utf8"), /Start here\./);
235
- assert.match(await readFile(join(root, "guide/details/index.html"), "utf8"), /Nested details\./);
236
- assert.match(await readFile(join(root, "robots.txt"), "utf8"), /Sitemap: https:\/\/docs\.example\.org\/sitemap\.xml/);
237
- assert.match(await readFile(join(root, "sitemap.xml"), "utf8"), /<loc>https:\/\/docs\.example\.org\/<\/loc>/);
238
- });
239
-
240
- test("crawler filenames reject conflicting document roots only when reserved", async (t) => {
241
- const root = await mkdtemp(join(tmpdir(), "for-humanity-crawling-paths-"));
242
- t.after(() => rm(root, {recursive: true, force: true}));
243
- await mkdir(join(root, "sitemap.xml"));
244
- await writeFile(join(root, "Robots.txt.md"), "---\nname: Robots\nlabel: Robots\ngroup: Guide\n---\n\nText.\n");
245
- await writeFile(join(root, "sitemap.xml/guide.md"), "---\nname: Sitemap\nlabel: Sitemap\ngroup: Guide\n---\n\nText.\n");
246
- const site = siteConfigSchema.parse({});
247
- const processor = createProcessor({site, root});
248
- const params = {root, processor, reserved: ["robots.txt", "sitemap.xml"]};
249
-
250
- await assert.rejects(readDocs(params), /문서 id에 사용할 수 없는 경로/);
251
- assert.equal((await readDocs({root, processor})).length, 2);
252
- });
253
-
254
- test("navigation initialization runs after its DOM and before the document body", async () => {
255
- const site = siteConfigSchema.parse({});
256
- const browserScript = "window.navigationFixture = true;";
257
- const assets = {fontCss: "", preload: [], reload: false, stylePath: asset_style_path, browserScript};
258
- const html = await (await createApp({site, store: {docs: []}, assets}).request("/")).text();
259
- const initialization = html.indexOf(`<script>${browserScript}</script>`);
260
-
261
- assert.doesNotMatch(html, /\sref="|\sonClick="/, "server HTML must not serialize browser refs or handlers");
262
- assert.ok(initialization > html.indexOf("fh-navigation-data"), "authored payload must be available before mounting");
263
- assert.ok(initialization > html.indexOf("data-navigation-drawer"), "navigation controls must exist before initialization");
264
- assert.ok(initialization > html.indexOf("data-cursor-face"), "the JSX browser owner must also contain its cursor");
265
- assert.ok(initialization < html.indexOf('<main class="wg_shell__main">'), "navigation must be ready before the main content is parsed");
266
- });
267
-
268
- test("the page outline belongs only to the right rail rather than mobile navigation", async (t) => {
269
- const root = await mkdtemp(join(tmpdir(), "for-humanity-outline-"));
270
- t.after(() => rm(root, {recursive: true, force: true}));
271
- await writeFile(join(root, "README.md"), "# Home\n\n## Read here\n\n### More context\n\nText.\n");
272
- const site = siteConfigSchema.parse({});
273
- const home = await readHome({root, processor: createProcessor({site, root})});
274
- const assets = {fontCss: "", preload: [], reload: false, browserScript: "", stylePath: asset_style_path};
275
- const html = await (await createApp({site, store: {docs: [], home}, assets}).request("/")).text();
276
- const rail = html.match(/<aside\b[^>]*data-navigation-rail=""[^>]*>([\s\S]*?)<\/aside>/)?.[1];
277
-
278
- assert.ok(rail, "the right rail must exist");
279
- assert.ok(rail.includes('data-navigation-scroll="outline"'), "the right rail must own the outline scroll area");
280
- assert.match(rail, /Read here/);
281
- assert.equal([...html.matchAll(/data-navigation-scroll="outline"/g)].length, 1);
282
- });
283
-
284
- test("documents with the same initial retain distinct URLs", async (t) => {
285
- const root = await mkdtemp(join(tmpdir(), "for-humanity-docs-"));
286
- const site = siteConfigSchema.parse({});
287
-
288
- t.after(() => rm(root, {recursive: true, force: true}));
289
- await Promise.all([
290
- writeFile(join(root, "api.md"), "---\nname: API\nlabel: Public contracts\ngroup: Reference\n---\n\n## Overview\n\nAPI guide.\n"),
291
- writeFile(join(root, "architecture.md"), "---\nname: Architecture\nlabel: Engine\ngroup: Development\n---\n\n## Overview\n\nEngine guide.\n"),
292
- ]);
293
-
294
- const docs = await readDocs({root, processor: createProcessor({site, root})});
295
-
296
- assert.deepEqual(docs.map((doc) => doc.id).toSorted(), ["api", "architecture"]);
297
- assert.deepEqual(docs.map((doc) => doc.data.name).toSorted(), ["API", "Architecture"]);
298
- });
299
-
300
- test("normalized document URL collisions still fail", async (t) => {
301
- const root = await mkdtemp(join(tmpdir(), "for-humanity-docs-"));
302
- const site = siteConfigSchema.parse({});
303
-
304
- t.after(() => rm(root, {recursive: true, force: true}));
305
- await Promise.all([
306
- writeFile(join(root, "guide.md"), "---\nname: Guide\nlabel: Guide\ngroup: Guide\n---\n\nText.\n"),
307
- writeFile(join(root, "guide.mdx"), "---\nname: Manual\nlabel: Manual\ngroup: Guide\n---\n\nText.\n"),
308
- ]);
309
-
310
- await assert.rejects(readDocs({root, processor: createProcessor({site, root})}), /문서 id가 겹친다: guide/);
311
- });
312
-
313
- test("navigation shows every document group in reading order without document icons", async (t) => {
314
- const root = await mkdtemp(join(tmpdir(), "for-humanity-docs-"));
315
- const site = siteConfigSchema.parse({navigation: ["Getting started", "Guide"]});
316
-
317
- t.after(() => rm(root, {recursive: true, force: true}));
318
- await Promise.all([
319
- writeFile(join(root, "commands.md"), "---\nname: Commands\nlabel: Commands\ngroup: Getting started\n---\n\nText.\n"),
320
- writeFile(join(root, "writing.md"), "---\nname: Writing\nlabel: Writing\ngroup: Guide\norder: 1\n---\n\nText.\n"),
321
- writeFile(join(root, "parts.md"), "---\nname: Parts\nlabel: Parts\ngroup: Guide\norder: 2\n---\n\nText.\n"),
322
- ]);
323
-
324
- const docs = await readDocs({root, processor: createProcessor({site, root})});
325
- const html = toNavigationHtml({site, docs, current: "writing"});
326
- const groups = [...html.matchAll(/<section\b[^>]*aria-label="([^"]+)"/g)];
327
-
328
- assert.deepEqual(
329
- groups.map((group) => group[1]),
330
- ["Getting started", "Guide"],
331
- );
332
- assert.doesNotMatch(html, /<details\b/);
333
- assert.doesNotMatch(html, /wg_navigationNav__docIcon/);
334
- assert.doesNotMatch(html, /href="\/"/);
335
- assert.doesNotMatch(html, />Documents<|>Overview</);
336
- assert.ok(html.indexOf('href="/writing/"') < html.indexOf('href="/parts/"'));
337
- assert.match(html, /href="\/writing\/"[^>]*aria-current="page"/);
338
- });
339
-
340
- test("group paths preserve literal names and reject empty or invalid segments", () => {
341
- const data = {name: "Guide", label: "Guide"};
342
-
343
- assert.deepEqual(docDataSchema.parse({...data, group: " Guide "}).group, ["Guide"]);
344
- assert.deepEqual(docDataSchema.parse({...data, group: [" Projects ", "Example project", "Research"]}).group, ["Projects", "Example project", "Research"]);
345
- assert.deepEqual(docDataSchema.parse({...data, group: "Research, decisions / notes"}).group, ["Research, decisions / notes"]);
346
-
347
- for (const group of ["", " ", [], ["Projects", " "], ["Projects", 1], [["Projects"]], null]) {
348
- assert.equal(docDataSchema.safeParse({...data, group}).success, false);
349
- }
350
-
351
- assert.equal(siteConfigSchema.safeParse({navigation: ["Guide", [" Guide "]]}).success, false);
352
- assert.equal(siteConfigSchema.safeParse({navigation: [["Projects", ""]]}).success, false);
353
- assert.equal(siteConfigSchema.safeParse({navigation: []}).success, true);
354
- });
355
-
356
- test("nested groups follow metadata paths and keep sibling branches and document URLs distinct", async (t) => {
357
- const root = await mkdtemp(join(tmpdir(), "for-humanity-groups-"));
358
- const site = siteConfigSchema.parse({navigation: ["Guide", ["Projects", "Atlas", "Research"], ["Projects", "Atlas", "Decisions"]]});
359
-
360
- t.after(() => rm(root, {recursive: true, force: true}));
361
- await Promise.all([
362
- writeFile(join(root, "guide.md"), "---\nname: Guide\nlabel: Guide\ngroup: Guide\n---\n\nText.\n"),
363
- writeFile(join(root, "guide-two.md"), "---\nname: More guide\nlabel: Guide\ngroup: [Guide]\n---\n\nText.\n"),
364
- writeFile(join(root, "overview.md"), "---\nname: Overview\nlabel: Overview\ngroup: [Projects, Atlas]\n---\n\nText.\n"),
365
- writeFile(join(root, "parts.md"), "---\nname: Parts\nlabel: Parts\ngroup: [Projects, Atlas, Research]\norder: 1\n---\n\nText.\n"),
366
- writeFile(join(root, "authoring.md"), "---\nname: Authoring\nlabel: Authoring\ngroup: [Projects, Atlas, Research]\norder: 2\n---\n\nText.\n"),
367
- writeFile(join(root, "decision.md"), "---\nname: Decision\nlabel: Decision\ngroup: [Projects, Atlas, Decisions]\n---\n\nText.\n"),
368
- writeFile(join(root, "other.md"), "---\nname: Other research\nlabel: Other research\ngroup: [Projects, Beacon, Research]\n---\n\nText.\n"),
369
- writeFile(join(root, "deep.md"), "---\nname: Deep research\nlabel: Deep research\ngroup: [Projects, Atlas, Research, Archive, Drafts]\n---\n\nText.\n"),
370
- ]);
371
-
372
- const docs = await readDocs({root, processor: createProcessor({site, root})});
373
- const groups = toDocGroups({docs, navigation: site.navigation});
374
-
375
- assert.deepEqual(
376
- groups.map((group) => group.name),
377
- ["Guide", "Projects"],
378
- );
379
- assert.deepEqual(
380
- groups[0].docs.map((doc) => doc.id),
381
- ["guide", "guide-two"],
382
- );
383
- assert.deepEqual(
384
- groups[1].groups.map((group) => group.name),
385
- ["Atlas", "Beacon"],
386
- );
387
- assert.deepEqual(
388
- groups[1].groups[0].docs.map((doc) => doc.id),
389
- ["overview"],
390
- );
391
- assert.deepEqual(
392
- groups[1].groups[0].groups.map((group) => group.name),
393
- ["Research", "Decisions"],
394
- );
395
- assert.deepEqual(
396
- groups[1].groups[0].groups[0].docs.map((doc) => doc.id),
397
- ["parts", "authoring"],
398
- );
399
- assert.deepEqual(
400
- groups[1].groups[1].groups[0].docs.map((doc) => doc.id),
401
- ["other"],
402
- );
403
- assert.equal(groups[1].groups[0].groups[0].groups[0].groups[0].name, "Drafts");
404
- assert.deepEqual(toDocGroups({docs: docs.toReversed(), navigation: site.navigation}), groups);
405
-
406
- const html = toNavigationHtml({site, docs, current: "authoring"});
407
-
408
- assert.match(html, /href="\/authoring\/"[^>]*aria-current="page"/);
409
- assert.match(html, /Atlas<\/span><ul\b/);
410
- assert.match(html, /Research<\/span><ul\b/);
411
- assert.equal([...html.matchAll(/href="\/authoring\/"/g)].length, 1);
412
- assert.equal([...html.matchAll(/>Research<\/span>/g)].length, 2);
413
- assert.doesNotMatch(toNavigationHtml({site, docs}), /aria-current="page"/);
414
- assert.ok(html.indexOf('href="/parts/"') < html.indexOf('href="/authoring/"'));
415
- assert.doesNotMatch(html, /<details\b/);
416
- });
417
-
418
- test("parent is an optional nonempty document ID", () => {
419
- const data = {name: "Child", label: "Child", group: "Guide"};
420
-
421
- assert.equal(docDataSchema.parse(data).parent, undefined);
422
- assert.equal(docDataSchema.parse({...data, parent: " guide/settings "}).parent, "guide/settings");
423
- for (const parent of ["", " ", null, 1, [], ["guide"]]) {
424
- assert.equal(docDataSchema.safeParse({...data, parent}).success, false);
425
- }
426
- });
427
-
428
- test("parent documents contain ordered descendants with independent URLs and one current page", async (t) => {
429
- const root = await mkdtemp(join(tmpdir(), "for-humanity-parents-"));
430
- const site = siteConfigSchema.parse({navigation: [["Projects", "Atlas", "Guide"]]});
431
-
432
- t.after(() => rm(root, {recursive: true, force: true}));
433
- await mkdir(join(root, "guide"));
434
- await Promise.all([
435
- writeFile(join(root, "settings.md"), "---\nname: Settings\nlabel: Settings\ngroup: [Projects, Atlas, Guide]\norder: 10\n---\n\nSettings introduction.\n"),
436
- writeFile(join(root, "guide/site.md"), "---\nname: Site\nlabel: Site\ngroup: [Projects, Atlas, Guide]\nparent: settings\norder: 20\n---\n\nSite introduction.\n"),
437
- writeFile(join(root, "themes.md"), "---\nname: Themes\nlabel: Themes\ngroup: [Projects, Atlas, Guide]\nparent: settings\norder: 10\n---\n\nThemes introduction.\n"),
438
- writeFile(join(root, "details.md"), "---\nname: Details\nlabel: Details\ngroup: [Projects, Atlas, Guide]\nparent: guide/site\n---\n\nDeep child.\n"),
439
- writeFile(join(root, "writing.md"), "---\nname: Writing\nlabel: Writing\ngroup: [Projects, Atlas, Guide]\norder: 20\n---\n\nIndependent guide.\n"),
440
- ]);
441
-
442
- const docs = await readDocs({root, processor: createProcessor({site, root})});
443
- const groups = toDocGroups({docs, navigation: site.navigation});
444
- const guide = groups[0].groups[0].groups[0];
445
-
446
- assert.deepEqual(
447
- guide.docs.map((doc) => doc.id),
448
- ["settings", "writing"],
449
- );
450
- assert.ok(guide.docs[0].children);
451
- assert.deepEqual(
452
- guide.docs[0].children.map((doc) => doc.id),
453
- ["themes", "guide/site"],
454
- );
455
- assert.ok(guide.docs[0].children[1].children);
456
- assert.deepEqual(
457
- guide.docs[0].children[1].children.map((doc) => doc.id),
458
- ["details"],
459
- );
460
- assert.deepEqual(docs.find((doc) => doc.id === "details")?.ancestors, ["settings", "guide/site"]);
461
- assert.deepEqual(toDocGroups({docs: docs.toReversed(), navigation: site.navigation}), groups);
462
-
463
- const html = toNavigationHtml({site, docs, current: "details"});
464
- assert.match(html, /href="\/settings\/"[^>]*>Settings<\/a><ul\b[^>]*aria-label="Settings"/);
465
- assert.match(html, /href="\/guide\/site\/"[^>]*>Site<\/a><ul\b[^>]*aria-label="Site"/);
466
- assert.match(html, /href="\/details\/"[^>]*aria-current="page"/);
467
- assert.equal([...html.matchAll(/aria-current="page"/g)].length, 1);
468
- assert.equal([...html.matchAll(/wg_navigationNavList__link--active/g)].length, 1);
469
- for (const doc of docs) {
470
- assert.equal(html.split(`href="/${doc.id}/"`).length - 1, 1);
471
- }
472
- assert.ok(html.indexOf('href="/themes/"') < html.indexOf('href="/guide/site/"'));
473
- assert.doesNotMatch(toNavigationHtml({site, docs}), /aria-current="page"/);
474
-
475
- const app = createApp({
476
- site,
477
- store: {docs},
478
- assets: {stylePath: asset_style_path, browserScript: "", fontCss: "", preload: [], reload: false},
479
- });
480
- for (const doc of docs) {
481
- assert.equal((await app.request(`/${doc.id}/`)).status, 200);
482
- }
483
- });
484
-
485
- test("relative parent IDs work with both the whole collection and a project document root", async (t) => {
486
- const root = await mkdtemp(join(tmpdir(), "for-humanity-parents-"));
487
- const site = siteConfigSchema.parse({});
488
- t.after(() => rm(root, {recursive: true, force: true}));
489
-
490
- await mkdir(join(root, "project/research"), {recursive: true});
491
- await mkdir(join(root, "project/guide"));
492
- await Promise.all([
493
- writeFile(join(root, "project/research/README.md"), "---\nname: Research\nlabel: Research\ngroup: Guide\n---\n\nResearch introduction.\n"),
494
- writeFile(join(root, "project/research/parts.md"), "---\nname: Parts\nlabel: Parts\ngroup: Guide\nparent: ./readme\n---\n\nText.\n"),
495
- writeFile(join(root, "project/guide/authoring.md"), "---\nname: Authoring\nlabel: Authoring\ngroup: Guide\nparent: ../research/readme\n---\n\nText.\n"),
496
- ]);
497
- for (const scope of [root, join(root, "project")]) {
498
- const docs = await readDocs({root: scope, processor: createProcessor({site, root: scope})});
499
- const parentId = scope === root ? "project/research/readme" : "research/readme";
500
- const groups = toDocGroups({docs, navigation: site.navigation});
501
- assert.deepEqual(
502
- groups[0].docs.map((doc) => doc.id),
503
- [parentId],
504
- );
505
- assert.ok(groups[0].docs[0].children);
506
- assert.equal(groups[0].docs[0].children.length, 2);
507
- assert.ok(groups[0].docs[0].children.every((doc) => doc.data.parent === parentId));
508
- assert.ok(groups[0].docs[0].children.every((doc) => doc.ancestors.length === 1 && doc.ancestors[0] === parentId));
509
- }
510
- });
511
-
512
- test("missing and home document parents fail when loading documents", async (t) => {
513
- const root = await mkdtemp(join(tmpdir(), "for-humanity-parents-"));
514
- const site = siteConfigSchema.parse({});
515
- t.after(() => rm(root, {recursive: true, force: true}));
516
-
517
- await writeFile(join(root, "README.md"), "# Home\n");
518
- for (const parent of ["missing", "readme", "guide.md", "/guide/"]) {
519
- await writeFile(join(root, "child.md"), `---\nname: Child\nlabel: Child\ngroup: Guide\nparent: ${parent}\n---\n\nText.\n`);
520
- await assert.rejects(readDocs({root, processor: createProcessor({site, root})}), /부모 문서가 없다: child/);
521
- }
522
- });
523
-
524
- test("self references and cycles fail when loading documents", async (t) => {
525
- const root = await mkdtemp(join(tmpdir(), "for-humanity-parents-"));
526
- const site = siteConfigSchema.parse({});
527
- t.after(() => rm(root, {recursive: true, force: true}));
528
-
529
- await writeFile(join(root, "first.md"), "---\nname: First\nlabel: First\ngroup: Guide\nparent: first\n---\n\nText.\n");
530
- await assert.rejects(readDocs({root, processor: createProcessor({site, root})}), /문서 parent가 순환한다/);
531
-
532
- await writeFile(join(root, "first.md"), "---\nname: First\nlabel: First\ngroup: Guide\nparent: second\n---\n\nText.\n");
533
- await writeFile(join(root, "second.md"), "---\nname: Second\nlabel: Second\ngroup: Guide\nparent: third\n---\n\nText.\n");
534
- await writeFile(join(root, "third.md"), "---\nname: Third\nlabel: Third\ngroup: Guide\nparent: first\n---\n\nText.\n");
535
- await assert.rejects(readDocs({root, processor: createProcessor({site, root})}), /문서 parent가 순환한다/);
536
- });
537
-
538
- test("parent and child must use the same complete group path", async (t) => {
539
- const root = await mkdtemp(join(tmpdir(), "for-humanity-parents-"));
540
- const site = siteConfigSchema.parse({});
541
- t.after(() => rm(root, {recursive: true, force: true}));
542
-
543
- await writeFile(join(root, "parent.md"), "---\nname: Parent\nlabel: Parent\ngroup: [Projects, Atlas, Research]\n---\n\nText.\n");
544
- await writeFile(join(root, "child.md"), "---\nname: Child\nlabel: Child\ngroup: [Projects, Beacon, Research]\nparent: parent\n---\n\nText.\n");
545
- await assert.rejects(readDocs({root, processor: createProcessor({site, root})}), /부모 문서와 group이 다르다: child/);
546
- });
547
-
548
- test("frontmatter supports BOM, CRLF and a closing fence at EOF", async (t) => {
549
- const root = await mkdtemp(join(tmpdir(), "for-humanity-docs-"));
550
- const site = siteConfigSchema.parse({});
551
-
552
- t.after(() => rm(root, {recursive: true, force: true}));
553
- await Promise.all([
554
- writeFile(join(root, "api.md"), "\uFEFF---\r\nname: API\r\nlabel: Public contracts\r\ngroup: Reference\r\n---\r\n\r\n예:이것\r\n"),
555
- writeFile(join(root, "empty.md"), "---\nname: Empty\nlabel: Empty\ngroup: Reference\n---"),
556
- ]);
557
-
558
- const docs = await readDocs({root, processor: createProcessor({site, root})});
559
-
560
- const api = docs.find((doc) => doc.id === "api");
561
- const empty = docs.find((doc) => doc.id === "empty");
562
-
563
- assert.ok(api);
564
- assert.ok(empty);
565
- assert.match(api.html, /예:이것/);
566
- assert.match(empty.html, /<h1[^>]*><span[^>]*>Empty<\/span><\/h1>/);
567
- assert.ok(docs.every((doc) => !doc.html.includes("wg_prose__eyebrow")));
568
- });
569
-
570
- test("frontmatter preserves source lines in directive errors", async (t) => {
571
- const root = await mkdtemp(join(tmpdir(), "for-humanity-docs-"));
572
- const site = siteConfigSchema.parse({});
573
-
574
- t.after(() => rm(root, {recursive: true, force: true}));
575
- await writeFile(join(root, "bad.md"), "---\nname: Broken\nlabel: Broken\ngroup: Guide\n---\n\n::unknown[Bad]\n");
576
-
577
- await assert.rejects(readDocs({root, processor: createProcessor({site, root})}), (error: unknown) => {
578
- assert.ok(error instanceof Error && "line" in error);
579
- assert.equal(error.line, 7);
580
-
581
- return true;
582
- });
583
- });
584
-
585
- test("a plain root README renders at home and relative home links resolve from nested docs", async (t) => {
586
- const root = await mkdtemp(join(tmpdir(), "for-humanity-home-"));
587
- const site = siteConfigSchema.parse({});
588
- const processor = createProcessor({site, root});
589
-
590
- t.after(() => rm(root, {recursive: true, force: true}));
591
- await mkdir(join(root, "guide"));
592
- await Promise.all([
593
- writeFile(
594
- join(root, "readme.md"),
595
- '<div align="center">\n\n<img src="/favicon.svg" width="96" height="96" alt="Library face">\n\n# My library\n\nWelcome home.\n\n</div>\n\n## Start\n\n[Guide](guide/setup.md)\n\n```js\nconst ready = true;\n```\n',
596
- ),
597
- writeFile(join(root, "guide/setup.md"), "---\nname: Setup\nlabel: Setup\ngroup: Guide\n---\n\n[Home](../readme.md#start)\n\n## Details\n"),
598
- ]);
599
-
600
- const [docs, home] = await Promise.all([readDocs({root, processor}), readHome({root, processor})]);
601
- const app = createApp({
602
- site,
603
- store: {docs, home},
604
- assets: {stylePath: asset_style_path, browserScript: "", fontCss: "", preload: [], reload: false},
605
- });
606
- const response = await app.request("/");
607
- const html = await response.text();
608
-
609
- assert.equal(response.status, 200);
610
- assert.deepEqual(
611
- docs.map((doc) => doc.id),
612
- ["guide/setup"],
613
- );
614
- assert.match(html, /<h1[^>]*><span[^>]*>My library<\/span><\/h1>/);
615
- assert.match(html, /Welcome home\./);
616
- assert.match(html, /<div align="center">/);
617
- assert.match(html, /src="\/favicon\.svg" width="96" height="96"/);
618
- assert.match(html, /href="\/guide\/setup\/"/);
619
- assert.match(html, /href="#start" data-toc-link/);
620
- assert.match(html, /class="shiki/);
621
- assert.match(html, /wg_navigation__brand" href="\/" aria-current="page"/);
622
- assert.equal([...html.matchAll(/href="\/"/g)].length, 1);
623
- assert.match(docs[0].html, /href="\/#start"/);
624
- assert.doesNotMatch(html, /wg_prose__eyebrow|pg_home__card/);
625
- assert.equal((await app.request("/readme/")).status, 404);
626
- });
627
-
628
- test("a missing README provides a creation hint without generated overview cards", async (t) => {
629
- const root = await mkdtemp(join(tmpdir(), "for-humanity-home-"));
630
- const site = siteConfigSchema.parse({});
631
-
632
- t.after(() => rm(root, {recursive: true, force: true}));
633
- const home = await readHome({root, processor: createProcessor({site, root})});
634
- const app = createApp({
635
- site,
636
- store: {docs: [], home},
637
- assets: {stylePath: asset_style_path, browserScript: "", fontCss: "", preload: [], reload: false},
638
- });
639
- const html = await (await app.request("/")).text();
640
-
641
- assert.equal(home, undefined);
642
- assert.match(html, /문서 폴더에 README\.md를 추가/);
643
- assert.doesNotMatch(html, /pg_home__card|>Overview<|>Documents</);
644
- });
645
-
646
- test("root agent instructions stay out of the rendered documents", async (t) => {
647
- const root = await mkdtemp(join(tmpdir(), "for-humanity-instructions-"));
648
- const site = siteConfigSchema.parse({});
649
-
650
- t.after(() => rm(root, {recursive: true, force: true}));
651
- await mkdir(join(root, "guide"));
652
- await Promise.all([
653
- writeFile(join(root, "README.md"), "# Home\n"),
654
- writeFile(join(root, "AGENTS.md"), "# Agent instructions\n\nKeep commit titles concise.\n"),
655
- writeFile(join(root, "claude.md"), "# Claude instructions\n\nCheck documents before committing.\n"),
656
- writeFile(join(root, "guide/agents.md"), "---\nname: Agent guide\nlabel: Agent guide\ngroup: Guide\n---\n\nGuide.\n"),
657
- writeFile(join(root, "guide/claude.md"), "---\nname: Claude guide\nlabel: Claude guide\ngroup: Guide\n---\n\nGuide.\n"),
658
- ]);
659
-
660
- const docs = await readDocs({root, processor: createProcessor({site, root})});
661
-
662
- assert.deepEqual(new Set(docs.map((doc) => doc.id)), new Set(["guide/agents", "guide/claude"]));
663
- });
664
-
665
- test("relative Markdown links preserve query strings and fragments", async (t) => {
666
- const root = await mkdtemp(join(tmpdir(), "for-humanity-links-"));
667
- const site = siteConfigSchema.parse({});
668
-
669
- t.after(() => rm(root, {recursive: true, force: true}));
670
- await mkdir(join(root, "guide"));
671
- await Promise.all([
672
- writeFile(join(root, "README.md"), "# Home\n\n## Start\n"),
673
- writeFile(join(root, "api.mdx"), "---\nname: API\nlabel: API\ngroup: Guide\n---\n\n## Details\n"),
674
- writeFile(
675
- join(root, "guide/setup.md"),
676
- [
677
- "---\nname: Setup\nlabel: Setup\ngroup: Guide\n---",
678
- "[API](../api.mdx?mode=compact&lang=ko#details)",
679
- "[Query only](../api.mdx?mode=compact)",
680
- "[Home](../README.md?lang=ko#start)",
681
- "[Reference][api]\n\n[api]: ../api.mdx?mode=reference#details",
682
- "[Fragment](../api.mdx#details?literal=yes)",
683
- "[External](https://example.com/api.md?mode=compact#details)",
684
- "[Absolute](/api.md?mode=compact#details)",
685
- "[Asset](/diagram.svg?version=2#label)",
686
- "[Local](#details?literal=yes)",
687
- ].join("\n\n"),
688
- ),
689
- ]);
690
-
691
- const docs = await readDocs({root, processor: createProcessor({site, root})});
692
- const setup = docs.find((doc) => doc.id === "guide/setup");
693
-
694
- assert.ok(setup);
695
- assert.match(setup.html, /href="\/api\/\?mode=compact&#x26;lang=ko#details"/);
696
- assert.match(setup.html, /href="\/api\/\?mode=compact"/);
697
- assert.match(setup.html, /href="\/\?lang=ko#start"/);
698
- assert.match(setup.html, /href="\/api\/\?mode=reference#details"/);
699
- assert.match(setup.html, /href="\/api\/#details\?literal=yes"/);
700
- assert.match(setup.html, /href="https:\/\/example\.com\/api\.md\?mode=compact#details"/);
701
- assert.match(setup.html, /href="\/api\.md\?mode=compact#details"/);
702
- assert.match(setup.html, /href="\/diagram\.svg\?version=2#label"/);
703
- assert.match(setup.html, /href="#details\?literal=yes"/);
704
- });
705
-
706
- test("headings and contents preserve authored text without automatic numbering", async (t) => {
707
- const root = await mkdtemp(join(tmpdir(), "for-humanity-headings-"));
708
- const site = siteConfigSchema.parse({});
709
-
710
- t.after(() => rm(root, {recursive: true, force: true}));
711
- await writeFile(
712
- join(root, "guide.md"),
713
- "---\nname: Guide\nlabel: Guide\ngroup: Guide\n---\n\n### Before the first section\n\n::part[Setup]\n\n## Overview\n\n### 01. Keep this number\n\n:::details[More]\n### Nested heading\n\nBody.\n:::\n\n## Overview\n",
714
- );
715
-
716
- const docs = await readDocs({root, processor: createProcessor({site, root})});
717
- const app = createApp({
718
- site,
719
- store: {docs},
720
- assets: {stylePath: asset_style_path, browserScript: "", fontCss: "", preload: [], reload: false},
721
- });
722
- const html = await (await app.request("/guide/")).text();
723
-
724
- assert.match(html, /<h2[^>]*id="overview"[^>]*><span class="wg_prose__headingText">Overview<\/span><\/h2>/);
725
- assert.match(html, /<h3[^>]*id="01-keep-this-number"[^>]*><span class="wg_prose__headingText">01\. Keep this number<\/span><\/h3>/);
726
- assert.match(html, /href="#overview"[^>]*>Overview<\/a>/);
727
- assert.match(html, /href="#01-keep-this-number"[^>]*>01\. Keep this number<\/a>/);
728
- assert.match(html, /href="#nested-heading"[^>]*>Nested heading<\/a>/);
729
- assert.match(html, /href="#overview-1"[^>]*>Overview<\/a>/);
730
- assert.match(html, /wg_navigationToc__group">Setup<\/div>/);
731
- assert.doesNotMatch(html, /wg_prose__num|wg_navigationToc__mark/);
732
- });
733
-
734
- test("local Markdown and raw HTML images share fingerprinted assets with attachments", async (t) => {
735
- const root = await mkdtemp(join(tmpdir(), "for-humanity-media-"));
736
- t.after(() => rm(root, {recursive: true, force: true}));
737
- await mkdir(join(root, "guide"));
738
- await mkdir(join(root, "img"));
739
- await writeFile(join(root, "img", "sample.svg"), '<svg xmlns="http://www.w3.org/2000/svg"/>');
740
- await writeFile(join(root, "guide", "report.csv"), "name,value\nsample,1\n");
741
- await writeFile(
742
- join(root, "guide", "page.md"),
743
- '---\nname: Media\nlabel: Media\ngroup: Guide\n---\n\n![Image](../img/sample.svg)\n\n![Reference][shot]\n\n[shot]: ../img/sample.svg\n\n<img src="../img/sample.svg" alt="Raw" />\n\n[Download](report.csv?download=1#rows)\n\n![External](https://example.com/image.png)\n',
744
- );
745
- const files = new Map<string, string>();
746
- const docs = await readDocs({root, processor: createProcessor({site: siteConfigSchema.parse({}), root, files})});
747
- assert.equal(files.size, 2);
748
- assert.equal(docs[0].html.match(/src="\/_fh\/media\/asset\.[a-f0-9]+\.svg"/g)?.length, 3);
749
- assert.match(docs[0].html, /href="\/_fh\/media\/asset\.[a-f0-9]+\.csv\?download=1#rows"/);
750
- assert.match(docs[0].html, /src="https:\/\/example.com\/image.png"/);
751
- });
752
-
753
- test("missing local images and paths outside the document root fail instead of producing broken output", async (t) => {
754
- const root = await mkdtemp(join(tmpdir(), "for-humanity-media-"));
755
- t.after(() => rm(root, {recursive: true, force: true}));
756
- await writeFile(join(root, "page.md"), "---\nname: Media\nlabel: Media\ngroup: Guide\n---\n\n![Missing](missing.png)\n");
757
- await assert.rejects(readDocs({root, processor: createProcessor({site: siteConfigSchema.parse({}), root})}), /자원/);
758
- await writeFile(join(root, "page.md"), "---\nname: Media\nlabel: Media\ngroup: Guide\n---\n\n![Outside](../outside.png)\n");
759
- await assert.rejects(readDocs({root, processor: createProcessor({site: siteConfigSchema.parse({}), root})}), /문서 폴더 밖/);
760
- });
761
-
762
- test("home images use the chosen root and symlink escapes cannot publish outside files", async (t) => {
763
- const root = await mkdtemp(join(tmpdir(), "for-humanity-media-"));
764
- const outside = await mkdtemp(join(tmpdir(), "for-humanity-outside-"));
765
- t.after(() => Promise.all([rm(root, {recursive: true, force: true}), rm(outside, {recursive: true, force: true})]));
766
- await writeFile(join(root, "local image.png"), "first");
767
- await writeFile(join(root, "README.md"), "# Home\n\n![Home](local%20image.png)\n");
768
- const files = new Map<string, string>();
769
- const site = siteConfigSchema.parse({});
770
- const home = await readHome({root, processor: createProcessor({site, root, files})});
771
- assert.equal(files.size, 1);
772
- const before = [...files.keys()][0];
773
- assert.ok(home?.html.includes(before));
774
- await writeFile(join(root, "local image.png"), "changed");
775
- const updated = new Map<string, string>();
776
- await readHome({root, processor: createProcessor({site, root, files: updated})});
777
- assert.notEqual([...updated.keys()][0], before);
778
- await writeFile(join(outside, "private.png"), "private");
779
- await symlink(join(outside, "private.png"), join(root, "linked.png"));
780
- await writeFile(join(root, "README.md"), "# Home\n\n![Private](linked.png)\n");
781
- await assert.rejects(readHome({root, processor: createProcessor({site, root})}), /문서 폴더 밖/);
782
- });