for-humanity 0.1.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 (177) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +21 -0
  3. package/README.md +118 -0
  4. package/dist/cli.css +962 -0
  5. package/dist/cli.js +1241 -0
  6. package/dist/client.js +5 -0
  7. package/package.json +92 -0
  8. package/src/app.tsx +57 -0
  9. package/src/asset/favicon.svg +5 -0
  10. package/src/asset/font/architects-daughter/LICENSE.txt +93 -0
  11. package/src/asset/font/architects-daughter/architects-daughter-latin-400-normal.woff2 +0 -0
  12. package/src/asset/font/architects-daughter/architects-daughter.css +10 -0
  13. package/src/asset/font/jetbrains-mono/LICENSE +93 -0
  14. package/src/asset/font/jetbrains-mono/jetbrains-mono-latin-wght-normal.woff2 +0 -0
  15. package/src/asset/font/jetbrains-mono/jetbrains-mono.css +11 -0
  16. package/src/asset/font/pretendard/LICENSE.txt +94 -0
  17. package/src/asset/font/pretendard/pretendardvariable-dynamic-subset.css +1008 -0
  18. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.0.woff2 +0 -0
  19. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.1.woff2 +0 -0
  20. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.10.woff2 +0 -0
  21. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.11.woff2 +0 -0
  22. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.12.woff2 +0 -0
  23. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.13.woff2 +0 -0
  24. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.14.woff2 +0 -0
  25. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.15.woff2 +0 -0
  26. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.16.woff2 +0 -0
  27. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.17.woff2 +0 -0
  28. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.18.woff2 +0 -0
  29. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.19.woff2 +0 -0
  30. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.2.woff2 +0 -0
  31. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.20.woff2 +0 -0
  32. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.21.woff2 +0 -0
  33. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.22.woff2 +0 -0
  34. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.23.woff2 +0 -0
  35. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.24.woff2 +0 -0
  36. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.25.woff2 +0 -0
  37. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.26.woff2 +0 -0
  38. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.27.woff2 +0 -0
  39. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.28.woff2 +0 -0
  40. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.29.woff2 +0 -0
  41. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.3.woff2 +0 -0
  42. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.30.woff2 +0 -0
  43. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.31.woff2 +0 -0
  44. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.32.woff2 +0 -0
  45. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.33.woff2 +0 -0
  46. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.34.woff2 +0 -0
  47. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.35.woff2 +0 -0
  48. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.36.woff2 +0 -0
  49. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.37.woff2 +0 -0
  50. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.38.woff2 +0 -0
  51. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.39.woff2 +0 -0
  52. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.4.woff2 +0 -0
  53. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.40.woff2 +0 -0
  54. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.41.woff2 +0 -0
  55. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.42.woff2 +0 -0
  56. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.43.woff2 +0 -0
  57. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.44.woff2 +0 -0
  58. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.45.woff2 +0 -0
  59. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.46.woff2 +0 -0
  60. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.47.woff2 +0 -0
  61. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.48.woff2 +0 -0
  62. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.49.woff2 +0 -0
  63. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.5.woff2 +0 -0
  64. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.50.woff2 +0 -0
  65. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.51.woff2 +0 -0
  66. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.52.woff2 +0 -0
  67. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.53.woff2 +0 -0
  68. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.54.woff2 +0 -0
  69. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.55.woff2 +0 -0
  70. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.56.woff2 +0 -0
  71. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.57.woff2 +0 -0
  72. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.58.woff2 +0 -0
  73. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.59.woff2 +0 -0
  74. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.6.woff2 +0 -0
  75. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.60.woff2 +0 -0
  76. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.61.woff2 +0 -0
  77. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.62.woff2 +0 -0
  78. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.63.woff2 +0 -0
  79. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.64.woff2 +0 -0
  80. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.65.woff2 +0 -0
  81. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.66.woff2 +0 -0
  82. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.67.woff2 +0 -0
  83. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.68.woff2 +0 -0
  84. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.69.woff2 +0 -0
  85. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.7.woff2 +0 -0
  86. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.70.woff2 +0 -0
  87. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.71.woff2 +0 -0
  88. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.72.woff2 +0 -0
  89. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.73.woff2 +0 -0
  90. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.74.woff2 +0 -0
  91. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.75.woff2 +0 -0
  92. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.76.woff2 +0 -0
  93. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.77.woff2 +0 -0
  94. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.78.woff2 +0 -0
  95. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.79.woff2 +0 -0
  96. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.8.woff2 +0 -0
  97. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.80.woff2 +0 -0
  98. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.81.woff2 +0 -0
  99. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.82.woff2 +0 -0
  100. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.83.woff2 +0 -0
  101. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.84.woff2 +0 -0
  102. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.85.woff2 +0 -0
  103. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.86.woff2 +0 -0
  104. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.87.woff2 +0 -0
  105. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.88.woff2 +0 -0
  106. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.89.woff2 +0 -0
  107. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.9.woff2 +0 -0
  108. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.90.woff2 +0 -0
  109. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.91.woff2 +0 -0
  110. package/src/cli.ts +162 -0
  111. package/src/client.ts +322 -0
  112. package/src/component/widget/prose/_constant/heading-highlight.ts +4 -0
  113. package/src/component/widget/prose/_constant/section.ts +7 -0
  114. package/src/component/widget/prose/_constant/status.ts +9 -0
  115. package/src/component/widget/prose/_function/has-block-content.ts +30 -0
  116. package/src/component/widget/prose/_function/rehype-head.ts +31 -0
  117. package/src/component/widget/prose/_function/rehype-sections/_to-section-headings.ts +20 -0
  118. package/src/component/widget/prose/_function/rehype-sections/rehype-sections.ts +21 -0
  119. package/src/component/widget/prose/_function/rehype-tables.ts +22 -0
  120. package/src/component/widget/prose/_function/remark-blocks.ts +112 -0
  121. package/src/component/widget/prose/_function/remark-flow.ts +26 -0
  122. package/src/component/widget/prose/_function/remark-links.ts +33 -0
  123. package/src/component/widget/prose/_function/remark-parts.ts +26 -0
  124. package/src/component/widget/prose/_function/remark-status.ts +59 -0
  125. package/src/component/widget/prose/_function/remark-swatch.ts +16 -0
  126. package/src/component/widget/prose/_function/remark-unknown-directives.ts +25 -0
  127. package/src/component/widget/prose/_type/doc-section.ts +9 -0
  128. package/src/component/widget/prose/wg-prose.css +381 -0
  129. package/src/component/widget/prose/wg-prose.tsx +43 -0
  130. package/src/component/widget/shell/_constant/cursor-face.ts +9 -0
  131. package/src/component/widget/shell/_constant/navigation.ts +4 -0
  132. package/src/component/widget/shell/_constant/reading-line.ts +4 -0
  133. package/src/component/widget/shell/_function/bind-mobile-navigation.ts +140 -0
  134. package/src/component/widget/shell/_function/to-toc-groups.ts +38 -0
  135. package/src/component/widget/shell/_type/doc-outline.ts +16 -0
  136. package/src/component/widget/shell/_type/toc-group.ts +39 -0
  137. package/src/component/widget/shell/_wg-shell-nav.css +134 -0
  138. package/src/component/widget/shell/_wg-shell-nav.tsx +75 -0
  139. package/src/component/widget/shell/_wg-shell-toc.css +268 -0
  140. package/src/component/widget/shell/_wg-shell-toc.tsx +57 -0
  141. package/src/component/widget/shell/wg-shell.css +261 -0
  142. package/src/component/widget/shell/wg-shell.tsx +147 -0
  143. package/src/constant/asset.ts +48 -0
  144. package/src/constant/cli.ts +19 -0
  145. package/src/constant/copy.ts +108 -0
  146. package/src/constant/doc.ts +13 -0
  147. package/src/constant/font.ts +39 -0
  148. package/src/constant/locale.ts +9 -0
  149. package/src/constant/site.ts +9 -0
  150. package/src/constant/status.ts +15 -0
  151. package/src/content/create-processor.ts +59 -0
  152. package/src/content/read-docs.test.ts +242 -0
  153. package/src/content/read-docs.ts +75 -0
  154. package/src/content/read-home.ts +32 -0
  155. package/src/content/rehype-heading-ids.ts +34 -0
  156. package/src/content/remark-report.ts +15 -0
  157. package/src/content/to-doc-groups.ts +18 -0
  158. package/src/dev.ts +79 -0
  159. package/src/env.d.ts +6 -0
  160. package/src/page/doc/pg-doc.tsx +35 -0
  161. package/src/page/home/pg-home.tsx +40 -0
  162. package/src/style/base.css +28 -0
  163. package/src/style/token.css +162 -0
  164. package/src/type/doc-content.ts +15 -0
  165. package/src/type/doc-data.ts +33 -0
  166. package/src/type/doc-file-data.ts +21 -0
  167. package/src/type/doc-heading.ts +17 -0
  168. package/src/type/doc.ts +16 -0
  169. package/src/type/site-assets.ts +17 -0
  170. package/src/type/site-config.ts +55 -0
  171. package/src/type/vfile-data.d.ts +10 -0
  172. package/src/util/dom/find-hash-target.ts +18 -0
  173. package/src/util/dom/reveal-hash-target.ts +15 -0
  174. package/src/util/error/to-error-message.ts +10 -0
  175. package/src/util/font/to-font-css.test.ts +41 -0
  176. package/src/util/font/to-font-css.ts +28 -0
  177. package/src/util/mermaid/render-flow.js +321 -0
@@ -0,0 +1,59 @@
1
+ import rehypeShiki from "@shikijs/rehype";
2
+ import rehypeRaw from "rehype-raw";
3
+ import rehypeStringify from "rehype-stringify";
4
+ import remarkDirective from "remark-directive";
5
+ import remarkFrontmatter from "remark-frontmatter";
6
+ import remarkGfm from "remark-gfm";
7
+ import remarkParse from "remark-parse";
8
+ import remarkRehype from "remark-rehype";
9
+ import remarkSmartypants from "remark-smartypants";
10
+ import {createCssVariablesTheme} from "shiki";
11
+ import {unified} from "unified";
12
+ import {rehypeHead} from "@/component/widget/prose/_function/rehype-head";
13
+ import {rehypeSections} from "@/component/widget/prose/_function/rehype-sections/rehype-sections";
14
+ import {rehypeTables} from "@/component/widget/prose/_function/rehype-tables";
15
+ import {remarkBlocks} from "@/component/widget/prose/_function/remark-blocks";
16
+ import {remarkFlow} from "@/component/widget/prose/_function/remark-flow";
17
+ import {remarkLinks} from "@/component/widget/prose/_function/remark-links";
18
+ import {remarkParts} from "@/component/widget/prose/_function/remark-parts";
19
+ import {remarkStatus} from "@/component/widget/prose/_function/remark-status";
20
+ import {remarkSwatch} from "@/component/widget/prose/_function/remark-swatch";
21
+ import {remarkUnknownDirectives} from "@/component/widget/prose/_function/remark-unknown-directives";
22
+ import {rehypeHeadingIds} from "@/content/rehype-heading-ids";
23
+ import {remarkReport} from "@/content/remark-report";
24
+ import type {SiteConfig} from "@/type/site-config";
25
+
26
+ /**
27
+ * Markdown 처리 순서: remark → 코드 강조 → 머리 → 제목 id → 가름 수집 → 표
28
+ * 플러그인 HTML 조각은 마지막 rehype-raw에서 해석
29
+ * Shiki inline style: token.css의 --app-code-* 참조
30
+ */
31
+ export const createProcessor = (options: {site: SiteConfig; root: string}) => {
32
+ return unified()
33
+ .use(remarkParse)
34
+ .use(remarkFrontmatter, ["yaml"])
35
+ .use(remarkGfm)
36
+ .use(remarkSmartypants)
37
+ .use(remarkDirective)
38
+ .use(remarkBlocks)
39
+ .use(remarkParts)
40
+ .use(remarkFlow)
41
+ .use(remarkStatus, {status: options.site.status})
42
+ .use(remarkSwatch)
43
+ .use(remarkLinks, {root: options.root})
44
+ .use(remarkUnknownDirectives)
45
+ .use(remarkReport, {root: options.root})
46
+ .use(remarkRehype, {allowDangerousHtml: true})
47
+ .use(rehypeShiki, {theme: createCssVariablesTheme({name: "for-humanity", variablePrefix: "--app-code-"})})
48
+ .use(rehypeHead)
49
+ .use(rehypeHeadingIds)
50
+ .use(rehypeSections)
51
+ .use(rehypeTables)
52
+ .use(rehypeRaw)
53
+ .use(rehypeStringify, {allowDangerousHtml: true});
54
+ };
55
+
56
+ /**
57
+ * 플러그인 조립을 마친 Markdown 처리기
58
+ */
59
+ export type Processor = ReturnType<typeof createProcessor>;
@@ -0,0 +1,242 @@
1
+ import assert from "node:assert/strict";
2
+ import {mkdir, mkdtemp, rm, writeFile} from "node:fs/promises";
3
+ import {tmpdir} from "node:os";
4
+ import {join} from "node:path";
5
+ import {test} from "node:test";
6
+ import {renderToStaticMarkup} from "react-dom/server";
7
+ import {createApp} from "@/app";
8
+ import {WgShellNav} from "@/component/widget/shell/_wg-shell-nav";
9
+ import {createProcessor} from "@/content/create-processor";
10
+ import {readDocs} from "@/content/read-docs";
11
+ import {readHome} from "@/content/read-home";
12
+ import {siteConfigSchema} from "@/type/site-config";
13
+
14
+ test("documents with the same initial retain distinct URLs", async (t) => {
15
+ const root = await mkdtemp(join(tmpdir(), "for-humanity-docs-"));
16
+ const site = siteConfigSchema.parse({});
17
+
18
+ t.after(() => rm(root, {recursive: true, force: true}));
19
+ await Promise.all([
20
+ writeFile(join(root, "api.md"), "---\nname: API\nlabel: Public contracts\ngroup: Reference\n---\n\n## Overview\n\nAPI guide.\n"),
21
+ writeFile(join(root, "architecture.md"), "---\nname: Architecture\nlabel: Engine\ngroup: Development\n---\n\n## Overview\n\nEngine guide.\n"),
22
+ ]);
23
+
24
+ const docs = await readDocs({root, processor: createProcessor({site, root})});
25
+
26
+ assert.deepEqual(docs.map((doc) => doc.id).toSorted(), ["api", "architecture"]);
27
+ assert.deepEqual(docs.map((doc) => doc.data.name).toSorted(), ["API", "Architecture"]);
28
+ });
29
+
30
+ test("normalized document URL collisions still fail", async (t) => {
31
+ const root = await mkdtemp(join(tmpdir(), "for-humanity-docs-"));
32
+ const site = siteConfigSchema.parse({});
33
+
34
+ t.after(() => rm(root, {recursive: true, force: true}));
35
+ await Promise.all([
36
+ writeFile(join(root, "guide.md"), "---\nname: Guide\nlabel: Guide\ngroup: Guide\n---\n\nText.\n"),
37
+ writeFile(join(root, "guide.mdx"), "---\nname: Manual\nlabel: Manual\ngroup: Guide\n---\n\nText.\n"),
38
+ ]);
39
+
40
+ await assert.rejects(readDocs({root, processor: createProcessor({site, root})}), /문서 id가 겹친다: guide/);
41
+ });
42
+
43
+ test("navigation shows every document group in reading order without document icons", async (t) => {
44
+ const root = await mkdtemp(join(tmpdir(), "for-humanity-docs-"));
45
+ const site = siteConfigSchema.parse({navigation: ["Getting started", "Guide"]});
46
+
47
+ t.after(() => rm(root, {recursive: true, force: true}));
48
+ await Promise.all([
49
+ writeFile(join(root, "commands.md"), "---\nname: Commands\nlabel: Commands\ngroup: Getting started\n---\n\nText.\n"),
50
+ writeFile(join(root, "writing.md"), "---\nname: Writing\nlabel: Writing\ngroup: Guide\norder: 1\n---\n\nText.\n"),
51
+ writeFile(join(root, "parts.md"), "---\nname: Parts\nlabel: Parts\ngroup: Guide\norder: 2\n---\n\nText.\n"),
52
+ ]);
53
+
54
+ const docs = await readDocs({root, processor: createProcessor({site, root})});
55
+ const html = renderToStaticMarkup(WgShellNav({site, docs, current: "writing"}));
56
+ const groups = [...html.matchAll(/<section\b[^>]*aria-label="([^"]+)"/g)];
57
+
58
+ assert.deepEqual(
59
+ groups.map((group) => group[1]),
60
+ ["Getting started", "Guide"],
61
+ );
62
+ assert.doesNotMatch(html, /<details\b/);
63
+ assert.doesNotMatch(html, /wg_shellNav__docIcon/);
64
+ assert.doesNotMatch(html, /href="\/"/);
65
+ assert.doesNotMatch(html, />Documents<|>Overview</);
66
+ assert.ok(html.indexOf('href="/writing/"') < html.indexOf('href="/parts/"'));
67
+ assert.match(html, /href="\/writing\/"[^>]*aria-current="page"/);
68
+ });
69
+
70
+ test("frontmatter supports BOM, CRLF and a closing fence at EOF", async (t) => {
71
+ const root = await mkdtemp(join(tmpdir(), "for-humanity-docs-"));
72
+ const site = siteConfigSchema.parse({});
73
+
74
+ t.after(() => rm(root, {recursive: true, force: true}));
75
+ await Promise.all([
76
+ writeFile(join(root, "api.md"), "\uFEFF---\r\nname: API\r\nlabel: Public contracts\r\ngroup: Reference\r\n---\r\n\r\n예:이것\r\n"),
77
+ writeFile(join(root, "empty.md"), "---\nname: Empty\nlabel: Empty\ngroup: Reference\n---"),
78
+ ]);
79
+
80
+ const docs = await readDocs({root, processor: createProcessor({site, root})});
81
+
82
+ const api = docs.find((doc) => doc.id === "api");
83
+ const empty = docs.find((doc) => doc.id === "empty");
84
+
85
+ assert.ok(api);
86
+ assert.ok(empty);
87
+ assert.match(api.html, /예:이것/);
88
+ assert.match(empty.html, /<h1[^>]*><span[^>]*>Empty<\/span><\/h1>/);
89
+ assert.ok(docs.every((doc) => !doc.html.includes("wg_prose__eyebrow")));
90
+ });
91
+
92
+ test("frontmatter preserves source lines in directive errors", async (t) => {
93
+ const root = await mkdtemp(join(tmpdir(), "for-humanity-docs-"));
94
+ const site = siteConfigSchema.parse({});
95
+
96
+ t.after(() => rm(root, {recursive: true, force: true}));
97
+ await writeFile(join(root, "bad.md"), "---\nname: Broken\nlabel: Broken\ngroup: Guide\n---\n\n::unknown[Bad]\n");
98
+
99
+ await assert.rejects(readDocs({root, processor: createProcessor({site, root})}), (error: unknown) => {
100
+ assert.ok(error instanceof Error && "line" in error);
101
+ assert.equal(error.line, 7);
102
+
103
+ return true;
104
+ });
105
+ });
106
+
107
+ test("a plain root README renders at home and relative home links resolve from nested docs", async (t) => {
108
+ const root = await mkdtemp(join(tmpdir(), "for-humanity-home-"));
109
+ const site = siteConfigSchema.parse({});
110
+ const processor = createProcessor({site, root});
111
+
112
+ t.after(() => rm(root, {recursive: true, force: true}));
113
+ await mkdir(join(root, "guide"));
114
+ await Promise.all([
115
+ writeFile(
116
+ join(root, "readme.md"),
117
+ '<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',
118
+ ),
119
+ writeFile(join(root, "guide/setup.md"), "---\nname: Setup\nlabel: Setup\ngroup: Guide\n---\n\n[Home](../readme.md#start)\n\n## Details\n"),
120
+ ]);
121
+
122
+ const [docs, home] = await Promise.all([readDocs({root, processor}), readHome({root, processor})]);
123
+ const app = createApp({site, store: {docs, home}, assets: {fontCss: "", preload: [], reload: false}});
124
+ const response = await app.request("/");
125
+ const html = await response.text();
126
+
127
+ assert.equal(response.status, 200);
128
+ assert.deepEqual(
129
+ docs.map((doc) => doc.id),
130
+ ["guide/setup"],
131
+ );
132
+ assert.match(html, /<h1[^>]*><span[^>]*>My library<\/span><\/h1>/);
133
+ assert.match(html, /Welcome home\./);
134
+ assert.match(html, /<div align="center">/);
135
+ assert.match(html, /src="\/favicon\.svg" width="96" height="96"/);
136
+ assert.match(html, /href="\/guide\/setup\/"/);
137
+ assert.match(html, /href="#start" data-toc-link/);
138
+ assert.match(html, /class="shiki/);
139
+ assert.match(html, /wg_shell__brand" href="\/" aria-current="page"/);
140
+ assert.equal([...html.matchAll(/href="\/"/g)].length, 1);
141
+ assert.match(docs[0].html, /href="\/#start"/);
142
+ assert.doesNotMatch(html, /wg_prose__eyebrow|pg_home__card/);
143
+ assert.equal((await app.request("/readme/")).status, 404);
144
+ });
145
+
146
+ test("a missing README provides a creation hint without generated overview cards", async (t) => {
147
+ const root = await mkdtemp(join(tmpdir(), "for-humanity-home-"));
148
+ const site = siteConfigSchema.parse({});
149
+
150
+ t.after(() => rm(root, {recursive: true, force: true}));
151
+ const home = await readHome({root, processor: createProcessor({site, root})});
152
+ const app = createApp({site, store: {docs: [], home}, assets: {fontCss: "", preload: [], reload: false}});
153
+ const html = await (await app.request("/")).text();
154
+
155
+ assert.equal(home, undefined);
156
+ assert.match(html, /문서 폴더에 README\.md를 추가/);
157
+ assert.doesNotMatch(html, /pg_home__card|>Overview<|>Documents</);
158
+ });
159
+
160
+ test("root agent instructions stay out of the rendered documents", async (t) => {
161
+ const root = await mkdtemp(join(tmpdir(), "for-humanity-instructions-"));
162
+ const site = siteConfigSchema.parse({});
163
+
164
+ t.after(() => rm(root, {recursive: true, force: true}));
165
+ await mkdir(join(root, "guide"));
166
+ await Promise.all([
167
+ writeFile(join(root, "README.md"), "# Home\n"),
168
+ writeFile(join(root, "AGENTS.md"), "# Agent instructions\n\nKeep commit titles concise.\n"),
169
+ writeFile(join(root, "claude.md"), "# Claude instructions\n\nCheck documents before committing.\n"),
170
+ writeFile(join(root, "guide/agents.md"), "---\nname: Agent guide\nlabel: Agent guide\ngroup: Guide\n---\n\nGuide.\n"),
171
+ writeFile(join(root, "guide/claude.md"), "---\nname: Claude guide\nlabel: Claude guide\ngroup: Guide\n---\n\nGuide.\n"),
172
+ ]);
173
+
174
+ const docs = await readDocs({root, processor: createProcessor({site, root})});
175
+
176
+ assert.deepEqual(new Set(docs.map((doc) => doc.id)), new Set(["guide/agents", "guide/claude"]));
177
+ });
178
+
179
+ test("relative Markdown links preserve query strings and fragments", async (t) => {
180
+ const root = await mkdtemp(join(tmpdir(), "for-humanity-links-"));
181
+ const site = siteConfigSchema.parse({});
182
+
183
+ t.after(() => rm(root, {recursive: true, force: true}));
184
+ await mkdir(join(root, "guide"));
185
+ await Promise.all([
186
+ writeFile(join(root, "README.md"), "# Home\n\n## Start\n"),
187
+ writeFile(join(root, "api.mdx"), "---\nname: API\nlabel: API\ngroup: Guide\n---\n\n## Details\n"),
188
+ writeFile(
189
+ join(root, "guide/setup.md"),
190
+ [
191
+ "---\nname: Setup\nlabel: Setup\ngroup: Guide\n---",
192
+ "[API](../api.mdx?mode=compact&lang=ko#details)",
193
+ "[Query only](../api.mdx?mode=compact)",
194
+ "[Home](../README.md?lang=ko#start)",
195
+ "[Reference][api]\n\n[api]: ../api.mdx?mode=reference#details",
196
+ "[Fragment](../api.mdx#details?literal=yes)",
197
+ "[External](https://example.com/api.md?mode=compact#details)",
198
+ "[Absolute](/api.md?mode=compact#details)",
199
+ "[Asset](diagram.svg?version=2#label)",
200
+ "[Local](#details?literal=yes)",
201
+ ].join("\n\n"),
202
+ ),
203
+ ]);
204
+
205
+ const docs = await readDocs({root, processor: createProcessor({site, root})});
206
+ const setup = docs.find((doc) => doc.id === "guide/setup");
207
+
208
+ assert.ok(setup);
209
+ assert.match(setup.html, /href="\/api\/\?mode=compact&#x26;lang=ko#details"/);
210
+ assert.match(setup.html, /href="\/api\/\?mode=compact"/);
211
+ assert.match(setup.html, /href="\/\?lang=ko#start"/);
212
+ assert.match(setup.html, /href="\/api\/\?mode=reference#details"/);
213
+ assert.match(setup.html, /href="\/api\/#details\?literal=yes"/);
214
+ assert.match(setup.html, /href="https:\/\/example\.com\/api\.md\?mode=compact#details"/);
215
+ assert.match(setup.html, /href="\/api\.md\?mode=compact#details"/);
216
+ assert.match(setup.html, /href="diagram\.svg\?version=2#label"/);
217
+ assert.match(setup.html, /href="#details\?literal=yes"/);
218
+ });
219
+
220
+ test("headings and contents preserve authored text without automatic numbering", async (t) => {
221
+ const root = await mkdtemp(join(tmpdir(), "for-humanity-headings-"));
222
+ const site = siteConfigSchema.parse({});
223
+
224
+ t.after(() => rm(root, {recursive: true, force: true}));
225
+ await writeFile(
226
+ join(root, "guide.md"),
227
+ "---\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",
228
+ );
229
+
230
+ const docs = await readDocs({root, processor: createProcessor({site, root})});
231
+ const app = createApp({site, store: {docs}, assets: {fontCss: "", preload: [], reload: false}});
232
+ const html = await (await app.request("/guide/")).text();
233
+
234
+ assert.match(html, /<h2[^>]*id="overview"[^>]*><span class="wg_prose__headingText">Overview<\/span><\/h2>/);
235
+ assert.match(html, /<h3[^>]*id="01-keep-this-number"[^>]*><span class="wg_prose__headingText">01\. Keep this number<\/span><\/h3>/);
236
+ assert.match(html, /href="#overview"[^>]*>Overview<\/a>/);
237
+ assert.match(html, /href="#01-keep-this-number"[^>]*>01\. Keep this number<\/a>/);
238
+ assert.match(html, /href="#nested-heading"[^>]*>Nested heading<\/a>/);
239
+ assert.match(html, /href="#overview-1"[^>]*>Overview<\/a>/);
240
+ assert.match(html, /wg_shellToc__group">Setup<\/div>/);
241
+ assert.doesNotMatch(html, /wg_prose__num|wg_shellToc__mark/);
242
+ });
@@ -0,0 +1,75 @@
1
+ import {readdir, readFile} from "node:fs/promises";
2
+ import {join, sep} from "node:path";
3
+ import {VFile} from "vfile";
4
+ import {parse as parseYaml} from "yaml";
5
+ import {asset_dir, asset_favicon_path} from "@/constant/asset";
6
+ import {copy_error_doc_id, copy_error_doc_id_clash, copy_error_frontmatter} from "@/constant/copy";
7
+ import type {Processor} from "@/content/create-processor";
8
+ import type {Doc} from "@/type/doc";
9
+ import {docDataSchema} from "@/type/doc-data";
10
+ import type {DocFileData} from "@/type/doc-file-data";
11
+
12
+ /**
13
+ * Markdown 수집과 렌더링 · 루트 README는 홈, AGENTS·CLAUDE는 작성 지침으로 분리
14
+ * 문서 목록에서 루트 안내 파일·node_modules·dist 제외
15
+ * 머리말·문서 식별자 중복·플러그인 오류 시 실패
16
+ */
17
+ export const readDocs = async (options: {root: string; processor: Processor}): Promise<Doc[]> => {
18
+ const paths = (await readdir(options.root, {recursive: true})).filter(
19
+ (path) => /\.mdx?$/i.test(path) && !/^(readme|agents|claude)\.md$/i.test(path) && !path.split(sep).includes("node_modules") && !path.startsWith(`dist${sep}`),
20
+ );
21
+ const docs = await Promise.all(
22
+ paths.map(async (path): Promise<Doc> => {
23
+ const id = path
24
+ .replace(/\.mdx?$/i, "")
25
+ .split(sep)
26
+ .join("/")
27
+ .toLowerCase();
28
+ const segments = id.split("/");
29
+
30
+ // Hono·SSG·정적 제공의 공통 URL 계약 · 예약 문자에 의한 페이지 누락 방지
31
+ if (
32
+ segments.some((segment) => segment === "" || segment === "." || segment === ".." || segment === "index.html") ||
33
+ /[#?%*:\\]/.test(id) ||
34
+ /[\p{Cc}]/u.test(id) ||
35
+ segments[0] === asset_dir ||
36
+ segments[0] === asset_favicon_path.slice(1)
37
+ ) {
38
+ throw new Error(`${copy_error_doc_id}: ${path}`);
39
+ }
40
+
41
+ const source = await readFile(join(options.root, path), "utf8");
42
+ // remark가 제외하는 UTF-8 BOM을 원문에서도 제거 · 지시문의 offset 기준 일치
43
+ const file = new VFile({value: source.replace(/^\uFEFF/, ""), path: join(options.root, path)});
44
+ const tree = options.processor.parse(file);
45
+ const frontmatter = tree.children[0];
46
+ const parsed = docDataSchema.safeParse(frontmatter?.type === "yaml" ? parseYaml(frontmatter.value) : {});
47
+
48
+ if (!parsed.success) {
49
+ throw new Error(`${copy_error_frontmatter}: ${path}: ${parsed.error.issues.map((issue) => `${issue.path.join(".")} ${issue.message}`).join(", ")}`);
50
+ }
51
+
52
+ // 플러그인의 제목·절 정보 공유
53
+ const fh: DocFileData = {frontmatter: parsed.data, headings: [], sections: []};
54
+ file.data.fh = fh;
55
+
56
+ // YAML 노드만 제외 · 본문 위치와 오류의 원본 줄·열·offset 유지
57
+ tree.children = tree.children.filter((node) => node.type !== "yaml");
58
+ const rendered = await options.processor.run(tree, file);
59
+
60
+ return {
61
+ id,
62
+ data: parsed.data,
63
+ html: options.processor.stringify(rendered, file),
64
+ outline: {headings: fh.headings, sections: fh.sections},
65
+ };
66
+ }),
67
+ );
68
+ const duplicateIds = [...Map.groupBy(docs, (doc) => doc.id)].filter((entry) => entry[1].length > 1);
69
+
70
+ if (duplicateIds.length > 0) {
71
+ throw new Error(`${copy_error_doc_id_clash}: ${duplicateIds.map((entry) => entry[0]).join(", ")}`);
72
+ }
73
+
74
+ return docs;
75
+ };
@@ -0,0 +1,32 @@
1
+ import {readdir, readFile} from "node:fs/promises";
2
+ import {join} from "node:path";
3
+ import {VFile} from "vfile";
4
+ import {copy_error_home_clash} from "@/constant/copy";
5
+ import type {Processor} from "@/content/create-processor";
6
+ import type {DocContent} from "@/type/doc-content";
7
+ import type {DocFileData} from "@/type/doc-file-data";
8
+
9
+ /**
10
+ * 문서 폴더의 README.md → 홈 · 대소문자 무시 · frontmatter 불필요
11
+ * 파일이 없으면 생성 안내 표시, 이름만 다른 README 중복은 오류
12
+ */
13
+ export const readHome = async (options: {root: string; processor: Processor}): Promise<DocContent | undefined> => {
14
+ const paths = (await readdir(options.root)).filter((path) => path.toLowerCase() === "readme.md");
15
+
16
+ if (paths.length > 1) {
17
+ throw new Error(`${copy_error_home_clash}: ${paths.join(", ")}`);
18
+ }
19
+
20
+ if (paths.length === 0) {
21
+ return undefined;
22
+ }
23
+
24
+ const path = join(options.root, paths[0]);
25
+ const fh: DocFileData = {headings: [], sections: []};
26
+ // 파서와 지시문 원문 복원의 offset 기준을 맞추기 위해 BOM 제외
27
+ const file = new VFile({value: (await readFile(path, "utf8")).replace(/^\uFEFF/, ""), path, data: {fh}});
28
+
29
+ await options.processor.process(file);
30
+
31
+ return {html: String(file), outline: {headings: fh.headings, sections: fh.sections}};
32
+ };
@@ -0,0 +1,34 @@
1
+ import Slugger from "github-slugger";
2
+ import type {Root} from "hast";
3
+ import {toString as toPlainText} from "hast-util-to-string";
4
+ import {visit} from "unist-util-visit";
5
+ import type {VFile} from "vfile";
6
+ import type {DocHeading} from "@/type/doc-heading";
7
+
8
+ /**
9
+ * 제목 id와 목차 수집 · 같은 제목은 -1, -2 접미사
10
+ * 작성한 제목 텍스트를 그대로 사용
11
+ */
12
+ export const rehypeHeadingIds = () => (tree: Root, file: VFile) => {
13
+ const slugger = new Slugger();
14
+ const headings: DocHeading[] = [];
15
+
16
+ visit(tree, "element", (node) => {
17
+ const depth = /^h([1-6])$/.exec(node.tagName)?.[1];
18
+
19
+ if (depth === undefined) {
20
+ return;
21
+ }
22
+
23
+ const text = toPlainText(node);
24
+
25
+ node.properties.id = typeof node.properties.id === "string" ? node.properties.id : slugger.slug(text);
26
+ headings.push({depth: Number(depth), slug: node.properties.id, text});
27
+ // 텍스트 노드에는 className을 줄 수 없어 강조 영역을 분리 · 제목의 기존 줄바꿈 유지
28
+ node.children = [{type: "element", tagName: "span", properties: {className: ["wg_prose__headingText"]}, children: node.children}];
29
+ });
30
+
31
+ if (file.data.fh) {
32
+ file.data.fh.headings = headings;
33
+ }
34
+ };
@@ -0,0 +1,15 @@
1
+ import {relative} from "node:path";
2
+ import type {Root} from "mdast";
3
+ import type {VFile} from "vfile";
4
+
5
+ /**
6
+ * 파일 경고의 터미널 출력 · 파일:줄 경고 · 빌드 계속
7
+ * 경고 수집 뒤 실행하는 마지막 remark 플러그인
8
+ */
9
+ export const remarkReport = (options: {root: string}) => (_tree: Root, file: VFile) => {
10
+ const path = relative(options.root, file.path);
11
+
12
+ for (const message of file.messages) {
13
+ console.warn(`${message.line === undefined ? path : `${path}:${message.line}`} ${message.reason}`);
14
+ }
15
+ };
@@ -0,0 +1,18 @@
1
+ import {locale_doc_group, locale_doc_name} from "@/constant/locale";
2
+ import type {Doc} from "@/type/doc";
3
+ import type {SiteConfig} from "@/type/site-config";
4
+
5
+ /**
6
+ * 사이드바의 읽는 순서 · 설정 밖 묶음은 뒤에서 이름순
7
+ * 같은 문서 이름·순서도 파일 id로 구분해 탐색 순서를 고정
8
+ */
9
+ export const toDocGroups = (options: {docs: readonly Doc[]; navigation: SiteConfig["navigation"]}) => {
10
+ const positions = new Map(options.navigation.map((name, index) => [name, index]));
11
+
12
+ return [...Map.groupBy(options.docs, (doc) => doc.data.group)]
13
+ .toSorted((a, b) => (positions.get(a[0]) ?? options.navigation.length) - (positions.get(b[0]) ?? options.navigation.length) || a[0].localeCompare(b[0], locale_doc_group))
14
+ .map(([name, docs]) => ({
15
+ name,
16
+ docs: docs.toSorted((a, b) => a.data.order - b.data.order || a.data.name.localeCompare(b.data.name, locale_doc_name) || a.id.localeCompare(b.id, locale_doc_name)),
17
+ }));
18
+ };
package/src/dev.ts ADDED
@@ -0,0 +1,79 @@
1
+ import type {EventEmitter} from "node:events";
2
+ import {readFile} from "node:fs/promises";
3
+ import {extname} from "node:path";
4
+ import {type Context, Hono} from "hono";
5
+ import {streamSSE} from "hono/streaming";
6
+ import {asset_content_type_default, asset_content_types, asset_dir, asset_favicon_path, asset_reload_path} from "@/constant/asset";
7
+ import {font_cache_control} from "@/constant/font";
8
+
9
+ /**
10
+ * 개발 서버 입력 계약
11
+ */
12
+ export interface DevOptions {
13
+ /**
14
+ * 정적 자원 외 요청을 처리할 페이지 앱
15
+ */
16
+ pages: Hono;
17
+ /**
18
+ * 자원 URL → 파일 경로 · 정적 빌드와 동일 목록
19
+ */
20
+ files: Map<string, string>;
21
+ /**
22
+ * 문서 변경 알림 · SSE 새로고침의 change 이벤트
23
+ */
24
+ reload: EventEmitter;
25
+ }
26
+
27
+ /**
28
+ * 개발 전용 자원·SSE 라우트 · 나머지 요청은 페이지 앱에 위임
29
+ * 정적 빌드의 페이지 라우트와 분리
30
+ */
31
+ export const createDevApp = (options: DevOptions) => {
32
+ const app = new Hono();
33
+
34
+ /**
35
+ * 등록된 URL의 자원 응답 · 미등록 시 404
36
+ */
37
+ const handleAsset = async (c: Context) => {
38
+ const file = options.files.get(c.req.path);
39
+
40
+ if (file === undefined) {
41
+ return c.notFound();
42
+ }
43
+
44
+ c.header("content-type", asset_content_types[extname(file)] ?? asset_content_type_default);
45
+
46
+ if (extname(file) === ".woff2") {
47
+ c.header("Cache-Control", font_cache_control);
48
+ }
49
+
50
+ return c.body(await readFile(file));
51
+ };
52
+
53
+ app.get(asset_reload_path, (c) =>
54
+ streamSSE(
55
+ c,
56
+ (stream) =>
57
+ new Promise((done) => {
58
+ /**
59
+ * 문서 변경의 SSE 전송 · 전송 후 연결 종료
60
+ */
61
+ const handleChange = async () => {
62
+ await stream.writeSSE({data: "change"});
63
+ done();
64
+ };
65
+
66
+ options.reload.once("change", handleChange);
67
+ stream.onAbort(() => {
68
+ options.reload.off("change", handleChange);
69
+ done();
70
+ });
71
+ }),
72
+ ),
73
+ );
74
+ app.get(`/${asset_dir}/*`, handleAsset);
75
+ app.get(asset_favicon_path, handleAsset);
76
+ app.all("/*", (c) => options.pages.fetch(c.req.raw, c.env));
77
+
78
+ return app;
79
+ };
package/src/env.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /// <reference types="mdast-util-to-hast" />
2
+
3
+ /**
4
+ * 컴포넌트 CSS 부수효과 import · esbuild CSS 묶음 대상
5
+ */
6
+ declare module "*.css";
@@ -0,0 +1,35 @@
1
+ import {WgProse} from "@/component/widget/prose/wg-prose";
2
+ import {WgShell} from "@/component/widget/shell/wg-shell";
3
+ import type {Doc} from "@/type/doc";
4
+ import type {SiteAssets} from "@/type/site-assets";
5
+ import type {SiteConfig} from "@/type/site-config";
6
+
7
+ /**
8
+ * /:slug/ 문서 페이지 입력 · 렌더링된 HTML 사용
9
+ */
10
+ export interface PgDocProps {
11
+ /**
12
+ * 검증된 사이트 설정
13
+ */
14
+ site: SiteConfig;
15
+ /**
16
+ * 사이드바의 전체 문서
17
+ */
18
+ docs: Doc[];
19
+ /**
20
+ * HTML 머리의 공통 자원
21
+ */
22
+ assets: SiteAssets;
23
+ /**
24
+ * 현재 페이지의 문서
25
+ */
26
+ doc: Doc;
27
+ }
28
+
29
+ export const PgDoc = (props: PgDocProps) => {
30
+ return (
31
+ <WgShell site={props.site} docs={props.docs} assets={props.assets} title={props.doc.data.name} current={props.doc.id} outline={props.doc.outline}>
32
+ <WgProse html={props.doc.html} />
33
+ </WgShell>
34
+ );
35
+ };
@@ -0,0 +1,40 @@
1
+ import {WgProse} from "@/component/widget/prose/wg-prose";
2
+ import {WgShell} from "@/component/widget/shell/wg-shell";
3
+ import {copy_home_missing} from "@/constant/copy";
4
+ import type {Doc} from "@/type/doc";
5
+ import type {DocContent} from "@/type/doc-content";
6
+ import type {SiteAssets} from "@/type/site-assets";
7
+ import type {SiteConfig} from "@/type/site-config";
8
+
9
+ /**
10
+ * / 첫 화면 입력 · 문서 폴더 README의 본문과 목차
11
+ */
12
+ export interface PgHomeProps {
13
+ /**
14
+ * 검증된 사이트 설정
15
+ */
16
+ site: SiteConfig;
17
+ /**
18
+ * 사이드바의 전체 문서
19
+ */
20
+ docs: Doc[];
21
+ /**
22
+ * HTML 머리의 공통 자원
23
+ */
24
+ assets: SiteAssets;
25
+ /**
26
+ * README 렌더링 결과 · 파일이 없으면 작성 안내
27
+ */
28
+ home?: DocContent;
29
+ }
30
+
31
+ export const PgHome = (props: PgHomeProps) => {
32
+ return (
33
+ <WgShell site={props.site} docs={props.docs} assets={props.assets} outline={props.home?.outline}>
34
+ {/**
35
+ * README 원문 · 없을 때만 파일 작성 안내
36
+ */}
37
+ {props.home === undefined ? <WgProse head={{title: props.site.title, lead: copy_home_missing}} /> : <WgProse html={props.home.html} />}
38
+ </WgShell>
39
+ );
40
+ };