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
@@ -0,0 +1,2379 @@
1
+ import { S as copy_nav_toc_label, T as copy_warn_unknown_status, _ as copy_home_missing, a as copy_error_doc_id_clash, b as copy_nav_open, c as copy_error_doc_parent_missing, g as copy_error_unknown_directive, h as copy_error_snippet, i as copy_error_doc_id, l as copy_error_frontmatter, o as copy_error_doc_parent_cycle, r as copy_error_directive_syntax, s as copy_error_doc_parent_group, t as copy_error_asset, u as copy_error_home_clash, v as copy_nav_aria_label, w as copy_warn_broken_link, x as copy_nav_title, y as copy_nav_close } from "./copy-C1uzvEnr.js";
2
+ import { t as logger } from "./logger-Dv1i9xSV.js";
3
+ import { t as groupPathSchema } from "./group-path-XAThmzvC.js";
4
+ import { t as toChangeQueue } from "./to-change-queue-DbEnZGNK.js";
5
+ import { readFile, readdir } from "node:fs/promises";
6
+ import { z } from "zod";
7
+ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
8
+ import { basename, dirname, extname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
9
+ import rehypeShiki from "@shikijs/rehype";
10
+ import rehypeRaw from "rehype-raw";
11
+ import rehypeStringify from "rehype-stringify";
12
+ import remarkDirective from "remark-directive";
13
+ import remarkFrontmatter from "remark-frontmatter";
14
+ import remarkGfm from "remark-gfm";
15
+ import remarkParse from "remark-parse";
16
+ import remarkRehype from "remark-rehype";
17
+ import remarkSmartypants from "remark-smartypants";
18
+ import { createCssVariablesTheme } from "shiki";
19
+ import { unified } from "unified";
20
+ import { SKIP, visit } from "unist-util-visit";
21
+ import { createHash } from "node:crypto";
22
+ import Slugger from "github-slugger";
23
+ import { toString } from "hast-util-to-string";
24
+ import { transformerMetaHighlight, transformerNotationDiff, transformerNotationErrorLevel, transformerNotationHighlight, transformerNotationWordHighlight } from "@shikijs/transformers";
25
+ import { toString as toString$1 } from "mdast-util-to-string";
26
+ import { escape, escapeRegExp } from "es-toolkit/string";
27
+ import { findAndReplace } from "mdast-util-find-and-replace";
28
+ import { VFile } from "vfile";
29
+ import { parse } from "yaml";
30
+ import { Hono } from "hono";
31
+ import { html, raw } from "hono/html";
32
+ import { ssgParams } from "hono/ssg";
33
+ import clsx from "clsx";
34
+ import { jsx, jsxs } from "hono/jsx/jsx-runtime";
35
+ import { createHead, escapeHtml } from "unhead/server";
36
+ import { takeWhile } from "es-toolkit/array";
37
+ import { Fragment, useEffect, useLayoutEffect, useRef, useState } from "hono/jsx";
38
+ import { flushSync } from "hono/jsx/dom";
39
+ //#region src/app/_constant/crawling.ts
40
+ /**
41
+ * 크롤러 안내 파일의 사이트 루트 경로
42
+ */
43
+ var crawling_robots_path = "/robots.txt";
44
+ /**
45
+ * 공개 페이지 목록의 사이트 루트 경로
46
+ */
47
+ var crawling_sitemap_path = "/sitemap.xml";
48
+ /**
49
+ * 공개 크롤러 파일과 정적 출력이 충돌하는 문서의 첫 경로 조각
50
+ */
51
+ var crawling_reserved_roots = [crawling_robots_path.slice(1), crawling_sitemap_path.slice(1)];
52
+ /**
53
+ * 내용 지문을 붙일 번들 CSS의 기본 URL
54
+ */
55
+ var asset_style_path = `/_fh/style.css`;
56
+ /**
57
+ * 자체 호스팅 글꼴 폴더
58
+ */
59
+ var asset_font_dir = `/_fh/fonts`;
60
+ /**
61
+ * 자체 호스팅 Mermaid ESM과 지연 청크 폴더
62
+ */
63
+ var asset_mermaid_dir = `/_fh/mermaid`;
64
+ /**
65
+ * 탭 아이콘 URL
66
+ */
67
+ var asset_favicon_path = "/favicon.svg";
68
+ /**
69
+ * dev 자원 응답의 확장자별 content-type
70
+ */
71
+ var asset_content_types = {
72
+ ".css": "text/css; charset=utf-8",
73
+ ".js": "text/javascript; charset=utf-8",
74
+ ".woff2": "font/woff2",
75
+ ".svg": "image/svg+xml",
76
+ ".png": "image/png",
77
+ ".jpg": "image/jpeg",
78
+ ".jpeg": "image/jpeg",
79
+ ".gif": "image/gif",
80
+ ".webp": "image/webp",
81
+ ".avif": "image/avif",
82
+ ".pdf": "application/pdf",
83
+ ".csv": "text/csv; charset=utf-8",
84
+ ".txt": "text/plain; charset=utf-8",
85
+ ".zip": "application/zip"
86
+ };
87
+ /**
88
+ * 문서에서 참조한 그림, 첨부 자원의 예약 경로
89
+ */
90
+ var asset_media_dir = `/_fh/media`;
91
+ /**
92
+ * 명시적 로컬 참조로 배포할 수 있는 파일 형식. 실행 파일, 소스, 설정 제외
93
+ */
94
+ var asset_media_extensions = /* @__PURE__ */ new Set([
95
+ ".svg",
96
+ ".png",
97
+ ".jpg",
98
+ ".jpeg",
99
+ ".gif",
100
+ ".webp",
101
+ ".avif",
102
+ ".pdf",
103
+ ".csv",
104
+ ".txt",
105
+ ".zip"
106
+ ]);
107
+ //#endregion
108
+ //#region src/util/file/to-asset-path.ts
109
+ /**
110
+ * 파일명에 내용 지문 추가. 같은 내용은 같은 URL, 변경한 내용은 새 캐시 키 사용
111
+ */
112
+ var toAssetPath = (options) => {
113
+ const extension = posix.extname(options.path);
114
+ return `${posix.dirname(options.path)}/${posix.basename(options.path, extension)}.${createHash("sha256").update(readFileSync(options.file)).digest("hex")}${extension}`;
115
+ };
116
+ //#endregion
117
+ //#region src/app/_function/create-processor/_rehype-assets.ts
118
+ /**
119
+ * 문서 상대 경로의 이미지와 첨부 수집
120
+ *
121
+ * - 원시 HTML에도 같은 내용 지문 URL 적용
122
+ * - 없는 파일, 폴더 밖 경로와 symlink, 지원하지 않는 이미지 형식은 빌드 실패
123
+ * - 절대 URL, 외부 URL은 작성자가 제공하며 수집하지 않음
124
+ */
125
+ var rehypeAssets = (options) => (tree, file) => {
126
+ visit(tree, "element", (node) => {
127
+ const property = node.tagName === "img" ? "src" : "href";
128
+ const url = node.properties[property];
129
+ if (node.tagName !== "img" && node.tagName !== "a" || typeof url !== "string" || /^(?:[a-z][a-z\d+.-]*:|\/|#)/i.test(url)) return;
130
+ const match = /^([^?#]+)([?#].*)?$/.exec(url);
131
+ if (match === null) return;
132
+ const path = decodeURIComponent(match[1]);
133
+ const extension = extname(path).toLowerCase();
134
+ if (!asset_media_extensions.has(extension)) {
135
+ if (node.tagName === "img") file.fail(`${copy_error_asset}: 지원하지 않는 이미지 형식 · ${url}`, node);
136
+ return;
137
+ }
138
+ const target = resolve(dirname(file.path), path);
139
+ const location = relative(options.root, target);
140
+ if (location === ".." || location.startsWith(`..${sep}`) || isAbsolute(location)) file.fail(`${copy_error_asset}: 문서 폴더 밖 · ${url}`, node);
141
+ if (!existsSync(target)) file.fail(`${copy_error_asset}: 파일이 없다 · ${url}`, node);
142
+ const actual = realpathSync(target);
143
+ const actualLocation = relative(realpathSync(options.root), actual);
144
+ if (actualLocation === ".." || actualLocation.startsWith(`..${sep}`) || isAbsolute(actualLocation) || !statSync(actual).isFile()) file.fail(`${copy_error_asset}: 문서 폴더 밖이거나 일반 파일이 아니다 · ${url}`, node);
145
+ const assetUrl = toAssetPath({
146
+ path: `${asset_media_dir}/asset${extension}`,
147
+ file: actual
148
+ });
149
+ options.files.set(assetUrl, actual);
150
+ node.properties[property] = match[2] === void 0 ? assetUrl : `${assetUrl}${match[2]}`;
151
+ });
152
+ };
153
+ //#endregion
154
+ //#region src/app/_function/create-processor/_rehype-heading-ids.ts
155
+ /**
156
+ * 제목 id와 목차 수집. 같은 제목은 -1, -2 접미사
157
+ *
158
+ * - 작성한 제목 텍스트를 그대로 사용
159
+ */
160
+ var rehypeHeadingIds = () => (tree, file) => {
161
+ const slugger = new Slugger();
162
+ const headings = [];
163
+ visit(tree, "element", (node) => {
164
+ const depth = /^h([1-6])$/.exec(node.tagName)?.[1];
165
+ if (depth === void 0) return;
166
+ const text = toString(node);
167
+ node.properties.id = typeof node.properties.id === "string" ? node.properties.id : slugger.slug(text);
168
+ headings.push({
169
+ depth: Number(depth),
170
+ slug: node.properties.id,
171
+ text
172
+ });
173
+ node.children = [{
174
+ type: "element",
175
+ tagName: "span",
176
+ properties: { className: ["wg_prose__headingText"] },
177
+ children: node.children
178
+ }];
179
+ });
180
+ if (file.data.fh) file.data.fh.headings = headings;
181
+ };
182
+ //#endregion
183
+ //#region src/app/_function/create-processor/_remark-report.ts
184
+ /**
185
+ * 파일 경고의 터미널 출력. 파일:줄 경고. 빌드 계속
186
+ *
187
+ * - 경고 수집 뒤 실행하는 마지막 remark 플러그인
188
+ */
189
+ var remarkReport = (options) => (_tree, file) => {
190
+ const path = relative(options.root, file.path);
191
+ for (const message of file.messages) logger.warn(`${message.line === void 0 ? path : `${path}:${message.line}`} ${message.reason}`);
192
+ };
193
+ //#endregion
194
+ //#region src/app/_constant/snippet.ts
195
+ /**
196
+ * 스니펫 구문, `<<< ./path.ext` 뒤 선택 region과 코드 펜스 메타 허용
197
+ */
198
+ var snippet_pattern = /^<<<\s+(\S+?)(?:#([\w-]+))?(?:\s+(.+?))?\s*$/;
199
+ /**
200
+ * region 경계 구문, 주석 형식과 무관하게 이름으로 대응
201
+ */
202
+ var snippet_region_pattern = /^\s*\S*\s*#(end)?region\s+([\w-]+)/;
203
+ /**
204
+ * 펜스 언어로 치환할 확장자, 미등록 확장자는 원문 사용
205
+ */
206
+ var snippet_language_by_extension = {
207
+ ".cjs": "js",
208
+ ".md": "markdown",
209
+ ".mjs": "js",
210
+ ".mts": "ts",
211
+ ".txt": "text",
212
+ ".yml": "yaml"
213
+ };
214
+ //#endregion
215
+ //#region src/app/_function/create-processor/_remark-snippets.ts
216
+ /**
217
+ * region 지정 시 해당 범위만 추출, 미지정 시 전체 사용
218
+ *
219
+ * - region 경계 줄과 공통 들여쓰기 제거
220
+ */
221
+ var toSnippetBody = (source, region) => {
222
+ const lines = source.replace(/\r\n/g, "\n").split("\n");
223
+ let inside = region === void 0;
224
+ let found = region === void 0;
225
+ const kept = [];
226
+ for (const line of lines) {
227
+ const marker = snippet_region_pattern.exec(line);
228
+ if (marker !== null) {
229
+ if (region !== void 0 && marker[2] === region) {
230
+ inside = marker[1] === void 0;
231
+ found = true;
232
+ }
233
+ continue;
234
+ }
235
+ if (inside) kept.push(line);
236
+ }
237
+ if (!found) return;
238
+ while (kept.length > 0 && kept[0]?.trim() === "") kept.shift();
239
+ while (kept.length > 0 && kept.at(-1)?.trim() === "") kept.pop();
240
+ const indents = kept.filter((line) => line.trim() !== "").map((line) => line.length - line.trimStart().length);
241
+ const indent = indents.length === 0 ? 0 : Math.min(...indents);
242
+ return kept.map((line) => line.slice(Math.min(indent, line.length - line.trimStart().length))).join("\n");
243
+ };
244
+ /**
245
+ * 문서 상대 경로의 코드 스니펫 삽입
246
+ *
247
+ * - 선택 region과 메타를 코드 펜스에 반영
248
+ * - 문서 폴더 밖 경로와 없는 파일은 빌드 실패
249
+ */
250
+ var remarkSnippets = (options) => (tree, file) => {
251
+ visit(tree, "paragraph", (node, index, parent) => {
252
+ const [child] = node.children;
253
+ if (node.children.length !== 1 || child === void 0 || child.type !== "text" || parent === void 0 || index === void 0) return;
254
+ const match = snippet_pattern.exec(child.value.trim());
255
+ if (match === null) return;
256
+ const [, path = "", region, meta] = match;
257
+ const target = resolve(dirname(file.path), path);
258
+ const location = relative(options.root, target);
259
+ if (location === ".." || location.startsWith(`..${sep}`) || isAbsolute(location)) file.fail(`${copy_error_snippet}: 문서 폴더 밖 · ${path}`, node);
260
+ if (!existsSync(target)) file.fail(`${copy_error_snippet}: 파일이 없다 · ${path}`, node);
261
+ const actual = realpathSync(target);
262
+ const actualLocation = relative(realpathSync(options.root), actual);
263
+ if (actualLocation === ".." || actualLocation.startsWith(`..${sep}`) || isAbsolute(actualLocation) || !statSync(actual).isFile()) file.fail(`${copy_error_snippet}: 문서 폴더 밖이거나 일반 파일이 아니다 · ${path}`, node);
264
+ const body = toSnippetBody(readFileSync(actual, "utf8"), region);
265
+ if (body === void 0) {
266
+ file.fail(`${copy_error_snippet}: region 이 없다 · ${path}#${region}`, node);
267
+ return;
268
+ }
269
+ const extension = extname(actual).toLowerCase();
270
+ const lang = snippet_language_by_extension[extension] === void 0 ? extension.slice(1) : snippet_language_by_extension[extension];
271
+ parent.children.splice(index, 1, {
272
+ type: "code",
273
+ lang,
274
+ meta: meta === void 0 ? null : meta,
275
+ value: body
276
+ });
277
+ });
278
+ };
279
+ /**
280
+ * 코드 메타와 줄 지시문의 Shiki 변환기 순서
281
+ */
282
+ var shikiTransformers = [
283
+ transformerNotationHighlight({ matchAlgorithm: "v3" }),
284
+ transformerNotationErrorLevel({ matchAlgorithm: "v3" }),
285
+ transformerNotationDiff({ matchAlgorithm: "v3" }),
286
+ transformerNotationWordHighlight({ matchAlgorithm: "v3" }),
287
+ transformerMetaHighlight(),
288
+ {
289
+ name: "for-humanity:line-numbers",
290
+ pre(node) {
291
+ const meta = this.options.meta?.__raw;
292
+ if (typeof meta === "string" && /\bline-numbers\b/.test(meta)) this.addClassToHast(node, "wg_prose__lineNumbers");
293
+ }
294
+ },
295
+ {
296
+ name: "for-humanity:code-tone",
297
+ pre(node) {
298
+ const meta = this.options.meta?.__raw;
299
+ if (typeof meta !== "string") return;
300
+ for (const word of ["bad", "good"]) if (new RegExp(`\\b${word}\\b`).test(meta)) this.addClassToHast(node, `wg_prose__code--${word}`);
301
+ }
302
+ },
303
+ {
304
+ name: "for-humanity:code-title",
305
+ root(root) {
306
+ const meta = this.options.meta?.__raw;
307
+ const title = typeof meta === "string" ? /\[([^\]]+)\]/.exec(meta)?.[1] : void 0;
308
+ if (title === void 0) return;
309
+ const tones = typeof meta === "string" ? ["bad", "good"].filter((word) => new RegExp(`\\b${word}\\b`).test(meta)) : [];
310
+ const caption = {
311
+ type: "element",
312
+ tagName: "figcaption",
313
+ properties: { className: ["wg_prose__codeFileTitle"] },
314
+ children: [{
315
+ type: "text",
316
+ value: title
317
+ }, {
318
+ type: "element",
319
+ tagName: "span",
320
+ properties: { className: ["wg_prose__codeFileLang"] },
321
+ children: [{
322
+ type: "text",
323
+ value: this.options.lang
324
+ }]
325
+ }]
326
+ };
327
+ root.children = [{
328
+ type: "element",
329
+ tagName: "figure",
330
+ properties: { className: ["wg_prose__codeFile", ...tones.map((tone) => `wg_prose__codeFile--${tone}`)] },
331
+ children: [caption, ...root.children.filter((child) => child.type !== "doctype")]
332
+ }];
333
+ }
334
+ }
335
+ ];
336
+ //#endregion
337
+ //#region src/component/widget/mermaid/_function/remark-mermaid.ts
338
+ /**
339
+ * Mermaid 원문과 브라우저 렌더 영역. JavaScript 비활성 환경에서도 원문 유지
340
+ */
341
+ var remarkMermaid = (options) => (tree) => {
342
+ visit(tree, "code", (node, index, parent) => {
343
+ if (node.lang !== "mermaid" || parent === void 0 || index === void 0) return;
344
+ if (options.assets !== void 0) for (const [url, path] of options.assets.files) options.files.set(url, path);
345
+ parent.children[index] = {
346
+ type: "html",
347
+ value: `<figure class="wg_mermaid__root" data-mermaid${options.assets === void 0 ? "" : ` data-mermaid-module="${options.assets.entry}"`}><div class="wg_mermaid__result" hidden></div><pre class="wg_mermaid__source"><code data-mermaid-source>${node.value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")}</code></pre><p class="wg_mermaid__error" role="status" hidden></p></figure>`
348
+ };
349
+ });
350
+ };
351
+ //#endregion
352
+ //#region src/component/widget/prose/_constant/section.ts
353
+ /**
354
+ * 목차 대상 제목 깊이. h2 절, h3 소제목
355
+ */
356
+ var section_heading_depth = {
357
+ section: 2,
358
+ sub: 3
359
+ };
360
+ //#endregion
361
+ //#region src/component/widget/prose/_function/rehype-head.ts
362
+ /**
363
+ * name 제목과 첫 절 앞 본문의 공통 머리. README는 원문의 h1 사용
364
+ */
365
+ var rehypeHead = () => (tree, file) => {
366
+ const frontmatter = file.data.fh?.frontmatter;
367
+ const firstSection = tree.children.findIndex((node) => node.type === "element" && node.tagName === `h${section_heading_depth.section}`);
368
+ const leadEnd = firstSection === -1 ? tree.children.length : firstSection;
369
+ if (frontmatter === void 0 && leadEnd === 0) return;
370
+ tree.children = [{
371
+ type: "element",
372
+ tagName: "header",
373
+ properties: { className: ["wg_prose__head"] },
374
+ children: [...tree.children.slice(0, leadEnd).filter((node) => node.type !== "doctype")]
375
+ }, ...tree.children.slice(leadEnd)];
376
+ if (frontmatter !== void 0 && tree.children[0].type === "element") tree.children[0].children.unshift({
377
+ type: "element",
378
+ tagName: "h1",
379
+ properties: {},
380
+ children: [{
381
+ type: "text",
382
+ value: frontmatter.name
383
+ }]
384
+ });
385
+ };
386
+ //#endregion
387
+ //#region src/component/widget/prose/_function/rehype-sections/_to-section-headings.ts
388
+ /**
389
+ * 목차 대상 h2, h3. 중첩 블록까지 문서 순서 수집
390
+ *
391
+ * - rehype-heading-ids의 순회 순서와 일치
392
+ */
393
+ var toSectionHeadings = (node) => {
394
+ return node.children.flatMap((child) => {
395
+ if (child.type !== "element") return [];
396
+ if (child.tagName === `h${section_heading_depth.section}` || child.tagName === `h${section_heading_depth.sub}`) return [child];
397
+ return toSectionHeadings(child);
398
+ });
399
+ };
400
+ //#endregion
401
+ //#region src/component/widget/prose/_function/rehype-sections/rehype-sections.ts
402
+ /**
403
+ * h2, h3의 가름 메타데이터 수집. 작성한 제목은 그대로 유지
404
+ *
405
+ * - id, 목차 텍스트는 앞 단계의 rehype-heading-ids 소유
406
+ * - file.data.fh.sections는 headings의 h2, h3 순서와 일치
407
+ */
408
+ var rehypeSections = () => (tree, file) => {
409
+ if (file.data.fh === void 0) return;
410
+ file.data.fh.sections = toSectionHeadings(tree).map((heading) => ({ part: typeof heading.properties.dataPart === "string" ? heading.properties.dataPart : void 0 }));
411
+ };
412
+ //#endregion
413
+ //#region src/component/widget/prose/_function/rehype-tables.ts
414
+ /**
415
+ * 넓은 표의 내부 가로 스크롤 래퍼
416
+ */
417
+ var rehypeTables = () => (tree) => {
418
+ visit(tree, "element", (node, index, parent) => {
419
+ if (node.tagName !== "table" || parent === void 0 || index === void 0) return;
420
+ parent.children[index] = {
421
+ type: "element",
422
+ tagName: "div",
423
+ properties: { className: ["wg_prose__table"] },
424
+ children: [node]
425
+ };
426
+ return [SKIP, index + 1];
427
+ });
428
+ };
429
+ //#endregion
430
+ //#region src/component/widget/prose/_function/has-block-content.ts
431
+ /**
432
+ * 부품 본문의 내용 판정. 참조 정의, 주석, 빈 텍스트와 빈 구조 제외
433
+ *
434
+ * - 이미지, 표, 구분선, 주석 외 원시 HTML은 본문 요소
435
+ */
436
+ var hasBlockContent = (node) => {
437
+ if (node.type === "definition") return false;
438
+ if (node.type === "html") return node.value.replace(/<!--(?:>|->|[\s\S]*?(?:--!?>|$))/g, "").trim() !== "";
439
+ if ("value" in node) return node.value.trim() !== "";
440
+ if (node.type === "table" || node.type === "textDirective") return true;
441
+ if ("children" in node) return node.children.some(hasBlockContent);
442
+ return true;
443
+ };
444
+ //#endregion
445
+ //#region src/component/widget/prose/_function/remark-blocks.ts
446
+ /**
447
+ * Note, Details의 제목, 본문, 속성, 닫힘 계약
448
+ *
449
+ * - label은 첫 자식. summary는 Details의 첫 요소. 원문 위치는 오류 기준
450
+ */
451
+ var remarkBlocks = () => (tree, file) => {
452
+ const source = String(file.value);
453
+ visit(tree, (node, _index, parent) => {
454
+ if (node.type === "leafDirective" && node.name === "part" && parent?.type !== "root") file.fail(`${copy_error_directive_syntax}: part · 문서 최상위에 작성`, node);
455
+ if (node.type !== "containerDirective" && node.type !== "leafDirective" && node.type !== "textDirective") return;
456
+ if (node.name !== "note" && node.name !== "details") return;
457
+ if (node.type !== "containerDirective") file.fail(`${copy_error_directive_syntax}: ${node.name} · ::: 블록으로 작성`, node);
458
+ const label = node.children.at(0);
459
+ if (label?.type !== "paragraph" || label.data?.directiveLabel !== true || label.children.some((child) => child.type !== "text")) file.fail(`${copy_error_directive_syntax}: ${node.name} · 일반 텍스트 제목 필수`, node);
460
+ const title = toString$1(label).trim();
461
+ if (title === "") file.fail(`${copy_error_directive_syntax}: ${node.name} · 비어 있지 않은 제목 필수`, node);
462
+ if (!node.children.slice(1).some(hasBlockContent)) file.fail(`${copy_error_directive_syntax}: ${node.name} · 본문 필수`, node);
463
+ const unsupported = node.attributes ? Object.keys(node.attributes).find((key) => node.name !== "details" || key !== "open") : void 0;
464
+ if (unsupported !== void 0) file.fail(`${copy_error_directive_syntax}: ${node.name} · 미지원 속성 ${unsupported}`, node);
465
+ const open = node.attributes?.open;
466
+ const attributeSource = source.slice(label.position?.end.offset, node.position?.end.offset).split(/\r?\n/, 1)[0].trim();
467
+ if (attributeSource !== "" && (node.name !== "details" || !/^\{[ \t]*open[ \t]*\}$/.test(attributeSource))) file.fail(`${copy_error_directive_syntax}: ${node.name} · 허용 속성은 Details의 {open}, 나머지는 생략`, node);
468
+ const original = source.slice(node.position?.start.offset, node.position?.end.offset);
469
+ const opening = /^(:{3,})/.exec(original);
470
+ const closing = /(?:^|\n)[ \t>]*(:{3,})[ \t]*\r?$/.exec(original);
471
+ if (opening === null || closing === null || closing.index + closing[0].length !== original.length || closing[1].length < opening[1].length || node.position?.end.offset === node.children.at(-1)?.position?.end.offset) file.fail(`${copy_error_directive_syntax}: ${node.name} · 블록을 닫는 fence 필수`, node);
472
+ visit(node, "heading", (heading) => {
473
+ if (heading.depth <= section_heading_depth.section) file.fail(`${copy_error_directive_syntax}: ${node.name} · #·## 제목은 부품 밖에 작성`, heading);
474
+ });
475
+ visit(node, "code", (code) => {
476
+ const originalCode = source.slice(code.position?.start.offset, code.position?.end.offset);
477
+ const codeOpening = /^ {0,3}(`{3,}|~{3,})/.exec(originalCode);
478
+ const codeClosing = /(?:^|\n)[ \t>]*(`{3,}|~{3,})[ \t]*\r?$/.exec(originalCode);
479
+ if (codeOpening !== null && (codeClosing === null || codeClosing.index + codeClosing[0].length !== originalCode.length || codeClosing[1][0] !== codeOpening[1][0] || codeClosing[1].length < codeOpening[1].length)) file.fail(`${copy_error_directive_syntax}: ${node.name} · 코드 fence를 닫고, 코드 안의 :보다 긴 바깥 fence 사용`, code);
480
+ });
481
+ label.children = [{
482
+ type: "text",
483
+ value: title
484
+ }];
485
+ label.data = {
486
+ ...label.data,
487
+ hName: node.name === "note" ? "strong" : "summary",
488
+ hProperties: { className: [node.name === "note" ? "wg_prose__noteTitle" : "wg_prose__detailsSummary"] }
489
+ };
490
+ node.data = {
491
+ ...node.data,
492
+ hName: node.name === "note" ? "aside" : "details",
493
+ hProperties: node.name === "note" ? {
494
+ className: ["wg_prose__note"],
495
+ ariaLabel: title
496
+ } : {
497
+ className: ["wg_prose__details"],
498
+ open: open === ""
499
+ }
500
+ };
501
+ });
502
+ };
503
+ //#endregion
504
+ //#region src/component/widget/prose/_function/remark-links.ts
505
+ /**
506
+ * 상대 Markdown 링크의 사이트 URL 변환, query와 hash 원문 유지
507
+ *
508
+ * - 외부, 절대 URL과 같은 페이지 hash 유지. 없는 파일은 터미널 경고
509
+ */
510
+ var remarkLinks = (options) => (tree, file) => {
511
+ const here = posix.dirname(relative(options.root, file.path).split(sep).join("/"));
512
+ visit(tree, (node) => {
513
+ if (node.type !== "link" && node.type !== "definition") return;
514
+ const match = /^(?![a-z]+:|\/|#)([^#?]+)(\.mdx?)([?#].*|)$/i.exec(node.url);
515
+ if (match) {
516
+ if (!existsSync(resolve(dirname(file.path), `${match[1]}${match[2]}`))) file.message(`${copy_warn_broken_link}: ${node.url}`, node);
517
+ const target = posix.join(here, `${match[1]}${match[2]}`).toLowerCase();
518
+ node.url = target === "readme.md" ? `/${match[3]}` : `/${target.slice(0, -match[2].length)}/${match[3]}`;
519
+ }
520
+ });
521
+ };
522
+ //#endregion
523
+ //#region src/component/widget/prose/_function/remark-parts.ts
524
+ /**
525
+ * ::part[이름]의 본문 가름과 다음 h2의 data-part
526
+ *
527
+ * - rehype-sections에서 같은 지점의 목차 묶음 생성
528
+ */
529
+ var remarkParts = () => (tree) => {
530
+ for (const [index, node] of tree.children.entries()) {
531
+ if (node.type !== "leafDirective" || node.name !== "part") continue;
532
+ const section = tree.children.slice(index + 1).find((next) => next.type === "heading" && next.depth === section_heading_depth.section);
533
+ node.data = {
534
+ hName: "div",
535
+ hProperties: { className: ["wg_prose__part"] }
536
+ };
537
+ if (section) section.data = {
538
+ ...section.data,
539
+ hProperties: {
540
+ ...section.data?.hProperties,
541
+ dataPart: toString$1(node).trim()
542
+ }
543
+ };
544
+ }
545
+ };
546
+ //#endregion
547
+ //#region src/component/widget/prose/_constant/status.ts
548
+ /**
549
+ * 상태 문구 뒤 날짜 패턴. YYYY-MM-DD
550
+ */
551
+ var status_date_pattern = String.raw`\d{4}-\d{2}-\d{2}`;
552
+ /**
553
+ * 미등록 날짜 상태 후보. 등록 문구 치환 뒤 남은 괄호 문구
554
+ */
555
+ var status_candidate_pattern = new RegExp(String.raw`\([^()\n]*${status_date_pattern}\)`, "g");
556
+ //#endregion
557
+ //#region src/component/widget/prose/_function/remark-status.ts
558
+ /**
559
+ * 설정 문구의 상태 표지 변환. 괄호 제거. 제목, 링크 제외
560
+ *
561
+ * - 미등록 날짜 문구는 파일 경고. remark-report에서 출력
562
+ */
563
+ var remarkStatus = (options) => {
564
+ const statuses = options.status.toSorted((a, b) => b.phrase.length - a.phrase.length);
565
+ const phrases = statuses.map((item) => [escapeRegExp(item.phrase), ...item.date ? [`(?: ${status_date_pattern})?`] : []].join("")).join("|");
566
+ const pattern = new RegExp(String.raw`\((${phrases})\)|(${phrases})`, "g");
567
+ const ignore = [
568
+ "link",
569
+ "linkReference",
570
+ "heading"
571
+ ];
572
+ return (tree, file) => {
573
+ findAndReplace(tree, [pattern, (_match, inParens, bare) => {
574
+ const text = inParens ?? bare;
575
+ const item = statuses.find((status) => text?.startsWith(status.phrase));
576
+ if (text === void 0 || item === void 0) return false;
577
+ return {
578
+ type: "html",
579
+ value: `<span class="wg_prose__pill wg_prose__pill--${item.kind}">${escape(text)}</span>`
580
+ };
581
+ }], { ignore });
582
+ findAndReplace(tree, [status_candidate_pattern, (match, info) => {
583
+ file.message(`${copy_warn_unknown_status}: ${match}`, info.stack.findLast((node) => node.position !== void 0));
584
+ return false;
585
+ }], { ignore });
586
+ };
587
+ };
588
+ //#endregion
589
+ //#region src/component/widget/prose/_function/remark-swatch.ts
590
+ /**
591
+ * 인라인 hex 색 값의 칩 표시
592
+ *
593
+ * - 문서별 색은 --wg-prose-chip-color로 전달. 고정 스타일시트 값 불가
594
+ */
595
+ var remarkSwatch = () => (tree) => {
596
+ visit(tree, "inlineCode", (node) => {
597
+ if (/^#[0-9a-f]{3,8}$/i.test(node.value.trim())) node.data = { hProperties: {
598
+ className: ["wg_prose__color"],
599
+ style: `--wg-prose-chip-color: ${node.value.trim()}`
600
+ } };
601
+ });
602
+ };
603
+ //#endregion
604
+ //#region src/component/widget/prose/_function/remark-unknown-directives.ts
605
+ /**
606
+ * hName 없는 블록 지시문은 작성 오류
607
+ *
608
+ * - textDirective는 원문 복원. 예:이것 같은 일반 문장의 오인 방지
609
+ */
610
+ var remarkUnknownDirectives = () => (tree, file) => {
611
+ visit(tree, (node, index, parent) => {
612
+ if (node.type === "textDirective" && parent !== void 0 && index !== void 0) {
613
+ parent.children[index] = {
614
+ type: "text",
615
+ value: String(file.value).slice(node.position?.start.offset, node.position?.end.offset)
616
+ };
617
+ return SKIP;
618
+ }
619
+ if ((node.type === "leafDirective" || node.type === "containerDirective") && node.data?.hName === void 0) file.fail(`${copy_error_unknown_directive}: ${node.name}`, node);
620
+ });
621
+ };
622
+ //#endregion
623
+ //#region src/app/_function/create-processor/create-processor.ts
624
+ /**
625
+ * Markdown 변환과 문서 메타데이터 수집
626
+ *
627
+ * - 코드와 문서 머리 생성 후 제목 ID와 가름 수집
628
+ * - 플러그인 HTML 조각은 마지막 rehype-raw에서 해석
629
+ * - Shiki inline style: token.css의 --app-code-* 참조
630
+ */
631
+ var createProcessor = (options) => {
632
+ return unified().use(remarkParse).use(remarkFrontmatter, ["yaml"]).use(remarkGfm).use(remarkSnippets, { root: options.root }).use(remarkSmartypants).use(remarkDirective).use(remarkBlocks).use(remarkParts).use(remarkMermaid, {
633
+ assets: options.mermaid,
634
+ files: options.files ?? /* @__PURE__ */ new Map()
635
+ }).use(remarkStatus, { status: options.site.status }).use(remarkSwatch).use(remarkLinks, { root: options.root }).use(remarkUnknownDirectives).use(remarkReport, { root: options.root }).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeShiki, {
636
+ theme: createCssVariablesTheme({
637
+ name: "for-humanity",
638
+ variablePrefix: "--app-code-"
639
+ }),
640
+ transformers: shikiTransformers
641
+ }).use(rehypeHead).use(rehypeHeadingIds).use(rehypeSections).use(rehypeTables).use(rehypeRaw).use(rehypeAssets, {
642
+ root: options.root,
643
+ files: options.files ?? /* @__PURE__ */ new Map()
644
+ }).use(rehypeStringify, { allowDangerousHtml: true });
645
+ };
646
+ //#endregion
647
+ //#region src/app/_function/load-mermaid-assets.ts
648
+ /**
649
+ * 패키지에 포함한 Mermaid 진입 모듈과 지연 청크 등록 목록
650
+ */
651
+ var loadMermaidAssets = (kitRoot) => {
652
+ const directory = join(kitRoot, "dist/mermaid");
653
+ const names = readdirSync(directory);
654
+ const entry = names.find((name) => name.startsWith("mermaid-") && name.endsWith(".js"));
655
+ if (entry === void 0) throw new Error("Missing Mermaid browser entry. Run pnpm build:kit before using the CLI.");
656
+ return {
657
+ entry: `${asset_mermaid_dir}/${entry}`,
658
+ files: new Map(names.map((name) => [`${asset_mermaid_dir}/${name}`, join(directory, name)]))
659
+ };
660
+ };
661
+ //#endregion
662
+ //#region src/constant/doc.ts
663
+ /**
664
+ * 기존 frontmatter type의 호환 값. 모두 같은 Markdown 렌더링
665
+ *
666
+ * - 새 문서는 type 생략 권장
667
+ */
668
+ var doc_type = {
669
+ document: "document",
670
+ blueprint: "blueprint"
671
+ };
672
+ /**
673
+ * 순서 미지정 문서는 번호가 있는 문서 뒤에서 이름순 배치
674
+ */
675
+ var doc_order_default = Number.MAX_SAFE_INTEGER;
676
+ //#endregion
677
+ //#region src/type/doc-data.ts
678
+ /**
679
+ * 문서 frontmatter 스키마. 검증 실패 시 빌드 중단
680
+ */
681
+ var docDataSchema = z.object({
682
+ /**
683
+ * 문서 이름. 제목, 목록. 같은 첫 글자 허용
684
+ */
685
+ name: z.string().trim().min(1),
686
+ /**
687
+ * 짧은 문서 설명. 사이드바 툴팁
688
+ */
689
+ label: z.string(),
690
+ /**
691
+ * 기존 type 값 호환. 생략 권장, 렌더링 차이 없음
692
+ */
693
+ type: z.enum(doc_type).default(doc_type.document),
694
+ /**
695
+ * 사이드바의 문서 묶음 경로. 문자열도 배열로 정규화
696
+ */
697
+ group: groupPathSchema,
698
+ /**
699
+ * 같은 묶음 안의 부모 문서 id. 확장자 제외, ./, ../ 상대 경로도 허용
700
+ */
701
+ parent: z.string().trim().min(1).optional(),
702
+ /**
703
+ * 묶음 안의 읽는 순서. 미지정 문서는 뒤에서 이름순
704
+ */
705
+ order: z.number().int().nonnegative().default(doc_order_default)
706
+ });
707
+ //#endregion
708
+ //#region src/app/_function/read-docs.ts
709
+ /**
710
+ * Markdown 수집과 렌더링. 루트 README는 홈, AGENTS, CLAUDE는 작성 지침으로 분리
711
+ *
712
+ * - 문서 목록에서 루트 안내 파일, node_modules, dist 제외
713
+ * - 머리말, 문서 식별자 중복, 플러그인 오류 시 실패
714
+ */
715
+ var readDocs = async (options) => {
716
+ const paths = (await readdir(options.root, { recursive: true })).filter((path) => /\.mdx?$/i.test(path) && !/^(readme|agents|claude)\.md$/i.test(path) && !path.split(sep).includes("node_modules") && !path.startsWith(`dist${sep}`));
717
+ const docs = await Promise.all(paths.map(async (path) => {
718
+ const id = path.replace(/\.mdx?$/i, "").split(sep).join("/").toLowerCase();
719
+ const segments = id.split("/");
720
+ if (segments.some((segment) => segment === "" || segment === "." || segment === ".." || segment === "index.html") || /[#?%*:\\]/.test(id) || /[\p{Cc}]/u.test(id) || options.reserved?.includes(segments[0]) === true || segments[0] === "_fh" || segments[0] === "/favicon.svg".slice(1)) throw new Error(`${copy_error_doc_id}: ${path}`);
721
+ const source = await readFile(join(options.root, path), "utf8");
722
+ const file = new VFile({
723
+ value: source.replace(/^\uFEFF/, ""),
724
+ path: join(options.root, path)
725
+ });
726
+ const tree = options.processor.parse(file);
727
+ const frontmatter = tree.children[0];
728
+ const parsed = docDataSchema.safeParse(frontmatter?.type === "yaml" ? parse(frontmatter.value) : {});
729
+ if (!parsed.success) throw new Error(`${copy_error_frontmatter}: ${path}: ${parsed.error.issues.map((issue) => `${issue.path.join(".")} ${issue.message}`).join(", ")}`);
730
+ const data = {
731
+ ...parsed.data,
732
+ parent: parsed.data.parent !== void 0 && /^\.\.?\//.test(parsed.data.parent) ? posix.join(posix.dirname(id), parsed.data.parent) : parsed.data.parent
733
+ };
734
+ const fh = {
735
+ frontmatter: data,
736
+ headings: [],
737
+ sections: []
738
+ };
739
+ file.data.fh = fh;
740
+ tree.children = tree.children.filter((node) => node.type !== "yaml");
741
+ const rendered = await options.processor.run(tree, file);
742
+ return {
743
+ id,
744
+ data,
745
+ ancestors: [],
746
+ html: options.processor.stringify(rendered, file),
747
+ outline: {
748
+ headings: fh.headings,
749
+ sections: fh.sections
750
+ }
751
+ };
752
+ }));
753
+ const duplicateIds = [...Map.groupBy(docs, (doc) => doc.id)].filter((entry) => entry[1].length > 1);
754
+ if (duplicateIds.length > 0) throw new Error(`${copy_error_doc_id_clash}: ${duplicateIds.map((entry) => entry[0]).join(", ")}`);
755
+ const docsById = new Map(docs.map((doc) => [doc.id, doc]));
756
+ return docs.map((doc) => {
757
+ const ancestors = /* @__PURE__ */ new Set([doc.id]);
758
+ for (let parentId = doc.data.parent; parentId !== void 0;) {
759
+ const parent = docsById.get(parentId);
760
+ if (parent === void 0) throw new Error(`${copy_error_doc_parent_missing}: ${doc.id} → ${parentId}`);
761
+ if (ancestors.has(parent.id)) throw new Error(`${copy_error_doc_parent_cycle}: ${[...ancestors, parent.id].join(" → ")}`);
762
+ if (JSON.stringify(parent.data.group) !== JSON.stringify(doc.data.group)) throw new Error(`${copy_error_doc_parent_group}: ${doc.id} → ${parent.id}`);
763
+ ancestors.add(parent.id);
764
+ parentId = parent.data.parent;
765
+ }
766
+ return {
767
+ ...doc,
768
+ ancestors: [...ancestors].slice(1).toReversed()
769
+ };
770
+ });
771
+ };
772
+ //#endregion
773
+ //#region src/app/_function/read-home.ts
774
+ /**
775
+ * 문서 루트의 README 홈 변환
776
+ *
777
+ * - 대소문자 무시, frontmatter 불필요
778
+ * - 파일 부재 시 생성 안내 표시, 이름만 다른 README 중복은 오류
779
+ */
780
+ var readHome = async (options) => {
781
+ const paths = (await readdir(options.root)).filter((path) => path.toLowerCase() === "readme.md");
782
+ if (paths.length > 1) throw new Error(`${copy_error_home_clash}: ${paths.join(", ")}`);
783
+ if (paths.length === 0) return;
784
+ const path = join(options.root, paths[0]);
785
+ const fh = {
786
+ headings: [],
787
+ sections: []
788
+ };
789
+ const file = new VFile({
790
+ value: (await readFile(path, "utf8")).replace(/^\uFEFF/, ""),
791
+ path,
792
+ data: { fh }
793
+ });
794
+ await options.processor.process(file);
795
+ return {
796
+ html: String(file),
797
+ outline: {
798
+ headings: fh.headings,
799
+ sections: fh.sections
800
+ }
801
+ };
802
+ };
803
+ //#endregion
804
+ //#region src/app/_function/to-crawling-app.ts
805
+ /**
806
+ * dev와 정적 빌드에서 같은 크롤러 파일 생성. 정책, 수정 시각은 추정하지 않음
807
+ */
808
+ var toCrawlingApp = (params) => {
809
+ const app = new Hono();
810
+ app.get(crawling_robots_path, (c) => c.text(`User-agent: *\nAllow: /\n\nSitemap: ${params.url}${crawling_sitemap_path}\n`));
811
+ app.get(crawling_sitemap_path, (_c) => new Response(html`<?xml version="1.0" encoding="UTF-8"?>
812
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
813
+ <url><loc>${params.url}/</loc></url>
814
+ ${params.store.docs.map((doc) => html`<url><loc>${params.url}/${doc.id.split("/").map(encodeURIComponent).join("/")}/</loc></url>`)}
815
+ </urlset>`.toString(), { headers: { "Content-Type": "application/xml; charset=utf-8" } }));
816
+ return app;
817
+ };
818
+ //#endregion
819
+ //#region src/constant/font.ts
820
+ /**
821
+ * 본문 영문, 코드, 흐름도 글꼴 CSS. 패키지 기준 경로. Regular, SemiBold 두 파일
822
+ */
823
+ var font_mono_css = "src/asset/font/monoplex-kr/monoplex-kr.css";
824
+ /**
825
+ * 본문 한글 글꼴 CSS. IBM Plex Sans KR Regular, SemiBold. 한글 unicode-range
826
+ */
827
+ var font_body_css = "src/asset/font/ibm-plex-sans-kr/ibm-plex-sans-kr.css";
828
+ /**
829
+ * 사이트 이름 글꼴 CSS. Architects Daughter Regular. 라틴 subset
830
+ */
831
+ var font_brand_css = "src/asset/font/architects-daughter/architects-daughter.css";
832
+ /**
833
+ * 늦게 받은 글꼴로 이미 보이는 대체 글꼴을 교체하지 않는 표시 정책
834
+ */
835
+ var font_display_strategy = "optional";
836
+ /**
837
+ * 내용 지문이 붙은 글꼴만 장기 재사용. CSS와 브라우저 스크립트는 대상 제외
838
+ */
839
+ var font_cache_control = "public, max-age=31536000, immutable";
840
+ //#endregion
841
+ //#region src/app/_function/to-http-app.ts
842
+ /**
843
+ * 등록 자원과 크롤러 응답, 나머지 요청은 페이지 앱에 위임
844
+ *
845
+ * - 정적 생성은 페이지 앱만 사용, 파일 응답과 정규식 페이지 라우트 분리
846
+ */
847
+ var toHttpApp = (options) => {
848
+ const app = new Hono();
849
+ /**
850
+ * 등록된 URL의 자원 응답. 미등록 시 404
851
+ */
852
+ const handleAsset = async (c) => {
853
+ const file = options.files.get(c.req.path);
854
+ if (file === void 0) return c.notFound();
855
+ c.header("content-type", asset_content_types[extname(file).toLowerCase()] ?? "application/octet-stream");
856
+ if (extname(file) === ".woff2") c.header("Cache-Control", font_cache_control);
857
+ return c.body(await readFile(file));
858
+ };
859
+ app.get(`/_fh/*`, handleAsset);
860
+ app.get(asset_favicon_path, handleAsset);
861
+ if (options.crawling !== void 0) app.on("GET", [crawling_robots_path, crawling_sitemap_path], (c) => options.crawling?.fetch(c.req.raw, c.env) ?? c.notFound());
862
+ app.all("/*", (c) => options.pages.fetch(c.req.raw, c.env));
863
+ return app;
864
+ };
865
+ //#endregion
866
+ //#region src/component/widget/prose/wg-prose.tsx
867
+ var WgProse = (props) => {
868
+ if (props.html !== void 0) return /* @__PURE__ */ jsx("article", {
869
+ className: clsx("wg_prose__root"),
870
+ dangerouslySetInnerHTML: { __html: props.html }
871
+ });
872
+ return /* @__PURE__ */ jsx("article", {
873
+ className: clsx("wg_prose__root"),
874
+ children: props.head !== void 0 && /* @__PURE__ */ jsxs("header", {
875
+ className: clsx("wg_prose__head"),
876
+ children: [/* @__PURE__ */ jsx("h1", { children: props.head.title }), props.head.lead !== void 0 && /* @__PURE__ */ jsx("p", { children: props.head.lead })]
877
+ })
878
+ });
879
+ };
880
+ //#endregion
881
+ //#region src/component/widget/navigation/_function/to-doc-groups/_to-doc-branches.ts
882
+ /**
883
+ * 검증된 부모 관계로 문서 가지를 재귀 구성. 형제 순서와 URL 유지
884
+ */
885
+ var toDocBranches = (options) => {
886
+ return options.docs.toSorted((a, b) => a.data.order - b.data.order || a.data.name.localeCompare(b.data.name, "en") || a.id.localeCompare(b.id, "en")).map((doc) => {
887
+ const children = options.children.get(doc.id);
888
+ return {
889
+ ...doc,
890
+ children: children === void 0 ? void 0 : toDocBranches({
891
+ docs: children,
892
+ children: options.children
893
+ })
894
+ };
895
+ });
896
+ };
897
+ //#endregion
898
+ //#region src/component/widget/navigation/_function/to-doc-groups/_to-doc-group-branches.ts
899
+ /**
900
+ * 각 단계의 직접 문서와 하위 묶음을 분리. 고정된 깊이 제한 없이 구성
901
+ */
902
+ var toDocGroupBranches = (options) => {
903
+ return [...Map.groupBy(options.docs.filter((doc) => doc.data.group.length > options.path.length), (doc) => doc.data.group[options.path.length])].map(([name, docs]) => {
904
+ const path = [...options.path, name];
905
+ return {
906
+ name,
907
+ path,
908
+ docs,
909
+ position: options.positions.get(JSON.stringify(path)) ?? options.unlistedPosition
910
+ };
911
+ }).toSorted((a, b) => a.position - b.position || a.name.localeCompare(b.name, "ko")).map((group) => {
912
+ return {
913
+ name: group.name,
914
+ path: group.path,
915
+ docs: toDocBranches({
916
+ docs: group.docs.filter((doc) => doc.data.group.length === group.path.length && doc.data.parent === void 0),
917
+ children: options.children
918
+ }),
919
+ groups: toDocGroupBranches({
920
+ docs: group.docs,
921
+ children: options.children,
922
+ path: group.path,
923
+ positions: options.positions,
924
+ unlistedPosition: options.unlistedPosition
925
+ })
926
+ };
927
+ });
928
+ };
929
+ //#endregion
930
+ //#region src/component/widget/navigation/_function/to-doc-groups/to-doc-groups.ts
931
+ /**
932
+ * 메타데이터 경로로 읽는 순서를 가진 문서 계층 구성
933
+ *
934
+ * - navigation의 첫 지정을 조상에도 적용. 미지정 형제 묶음은 이름순
935
+ */
936
+ var toDocGroups = (options) => {
937
+ const positions = new Map(options.navigation.flatMap((path, index) => path.map((_name, depth) => [JSON.stringify(path.slice(0, depth + 1)), index])).toReversed());
938
+ const children = Map.groupBy(options.docs, (doc) => doc.data.parent);
939
+ return toDocGroupBranches({
940
+ docs: options.docs,
941
+ children,
942
+ path: [],
943
+ positions,
944
+ unlistedPosition: options.navigation.length
945
+ });
946
+ };
947
+ //#endregion
948
+ //#region src/component/widget/navigation/_function/to-navigation-data/_to-navigation-document.ts
949
+ /**
950
+ * 본문 없는 문서 가지. 재귀에서도 현재 페이지 한 항목만 표시
951
+ */
952
+ var toNavigationDocument = (options) => {
953
+ return {
954
+ id: options.doc.id,
955
+ name: options.doc.data.name,
956
+ label: options.doc.data.label,
957
+ active: options.doc.id === options.current,
958
+ children: options.doc.children === void 0 ? [] : options.doc.children.map((doc) => toNavigationDocument({
959
+ ...options,
960
+ doc
961
+ }))
962
+ };
963
+ };
964
+ //#endregion
965
+ //#region src/component/widget/navigation/_function/to-navigation-data/_to-navigation-group.ts
966
+ /**
967
+ * 전체 묶음 경로와 본문 없는 하위 가지
968
+ */
969
+ var toNavigationGroup = (options) => {
970
+ return {
971
+ name: options.group.name,
972
+ path: options.group.path,
973
+ docs: options.group.docs.map((doc) => toNavigationDocument({
974
+ doc,
975
+ current: options.current
976
+ })),
977
+ groups: options.group.groups.map((group) => toNavigationGroup({
978
+ ...options,
979
+ group
980
+ }))
981
+ };
982
+ };
983
+ //#endregion
984
+ //#region src/component/widget/navigation/_function/to-toc-groups.ts
985
+ /**
986
+ * h2별 h3 수집. 가름 시작 절에서 목차 묶음 분리
987
+ *
988
+ * - 첫 h2 앞의 h3은 목차 제외
989
+ */
990
+ var toTocGroups = (outline) => {
991
+ const entries = outline.headings.filter((heading) => heading.depth === section_heading_depth.section || heading.depth === section_heading_depth.sub).map((heading, index) => ({
992
+ heading,
993
+ section: outline.sections[index]
994
+ }));
995
+ const sections = entries.flatMap((entry, index) => entry.heading.depth === section_heading_depth.section ? [{
996
+ heading: entry.heading,
997
+ part: entry.section.part,
998
+ subs: takeWhile(entries.slice(index + 1), (sub) => sub.heading.depth !== section_heading_depth.section).map((sub) => ({ heading: sub.heading }))
999
+ }] : []);
1000
+ return sections.flatMap((section, index) => index === 0 || section.part !== void 0 ? [{
1001
+ part: section.part,
1002
+ sections: [section, ...takeWhile(sections.slice(index + 1), (next) => next.part === void 0)]
1003
+ }] : []);
1004
+ };
1005
+ //#endregion
1006
+ //#region src/component/widget/navigation/_function/to-navigation-data/to-navigation-data.ts
1007
+ /**
1008
+ * 현재 문서의 이름, 계층, 목차만 전송. 다른 문서 본문은 브라우저 탐색 입력에서 제외
1009
+ */
1010
+ var toNavigationData = (options) => {
1011
+ return {
1012
+ title: options.site.title,
1013
+ groups: toDocGroups({
1014
+ docs: options.docs,
1015
+ navigation: options.site.navigation
1016
+ }).map((group) => toNavigationGroup({
1017
+ group,
1018
+ current: options.current
1019
+ })),
1020
+ outline: options.outline === void 0 ? [] : toTocGroups(options.outline),
1021
+ home: options.current === void 0,
1022
+ cursorFace: options.site.cursorFace
1023
+ };
1024
+ };
1025
+ //#endregion
1026
+ //#region src/component/widget/navigation/_function/to-navigation-json.ts
1027
+ /**
1028
+ * 작성한 이름이 script 종료 태그나 JavaScript 줄 구분자로 해석되지 않는 JSON
1029
+ */
1030
+ var toNavigationJson = (data) => {
1031
+ return JSON.stringify(data).replaceAll("<", "\\u003c").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
1032
+ };
1033
+ //#endregion
1034
+ //#region src/component/widget/cursor-face/_constant/cursor-face.ts
1035
+ /**
1036
+ * 같은 탭의 문서 이동에서 커서 위치를 한 번 이어받는 저장소 키
1037
+ */
1038
+ var cursor_face_storage_key = "fh-cursor-face";
1039
+ //#endregion
1040
+ //#region src/component/widget/cursor-face/wg-cursor-face.tsx
1041
+ var WgCursorFace = (props) => {
1042
+ const faceRef = useRef(null);
1043
+ const mediaRef = useRef(null);
1044
+ const positionRef = useRef({
1045
+ x: 0,
1046
+ y: 0,
1047
+ frame: 0,
1048
+ hasPointer: false
1049
+ });
1050
+ /**
1051
+ * 화면 이탈, 터치 전환에서 예약된 이동까지 취소
1052
+ */
1053
+ const handleCursorHide = () => {
1054
+ cancelAnimationFrame(positionRef.current.frame);
1055
+ positionRef.current.frame = 0;
1056
+ faceRef.current?.classList.remove("wg_cursorFace__root--visible");
1057
+ };
1058
+ /**
1059
+ * 비활성 전환에서만 숨겨 새 마우스 이동을 취소하지 않음
1060
+ */
1061
+ const handleCursorMediaChange = () => {
1062
+ if (!mediaRef.current?.matches) {
1063
+ positionRef.current.hasPointer = false;
1064
+ handleCursorHide();
1065
+ }
1066
+ };
1067
+ /**
1068
+ * 화면 갱신마다 한 번만 반영. transform으로 본문 재배치 방지
1069
+ */
1070
+ const renderCursorFace = () => {
1071
+ if (positionRef.current.frame !== 0) return;
1072
+ positionRef.current.frame = requestAnimationFrame(() => {
1073
+ positionRef.current.frame = 0;
1074
+ const face = faceRef.current;
1075
+ if (face === null) return;
1076
+ face.style.transform = `translate3d(
1077
+ ${Math.max(0, Math.min(positionRef.current.x + 16, document.documentElement.clientWidth - face.width - 16))}px,
1078
+ ${Math.max(0, Math.min(positionRef.current.y + 16, document.documentElement.clientHeight - face.height - 16))}px,
1079
+ 0
1080
+ )`;
1081
+ face.classList.add("wg_cursorFace__root--visible");
1082
+ });
1083
+ };
1084
+ /**
1085
+ * 클릭, 텍스트 선택 중에도 장식 유지. 실제 마우스 입력만 좌표 반영
1086
+ */
1087
+ const handleDocumentPointer = (event) => {
1088
+ if (!mediaRef.current?.matches || event.pointerType !== "mouse") {
1089
+ positionRef.current.hasPointer = false;
1090
+ handleCursorHide();
1091
+ return;
1092
+ }
1093
+ positionRef.current.x = event.clientX;
1094
+ positionRef.current.y = event.clientY;
1095
+ positionRef.current.hasPointer = true;
1096
+ renderCursorFace();
1097
+ };
1098
+ /**
1099
+ * 새 마우스 이동 없이 화면 가장자리의 좌표 재계산
1100
+ */
1101
+ const handleCursorResize = () => {
1102
+ if (faceRef.current?.classList.contains("wg_cursorFace__root--visible")) renderCursorFace();
1103
+ };
1104
+ /**
1105
+ * 같은 탭의 마지막 마우스 좌표 전달, 포커스 이탈의 일시 숨김과 무관
1106
+ */
1107
+ const handlePageHide = () => {
1108
+ if (!positionRef.current.hasPointer) return;
1109
+ try {
1110
+ sessionStorage.setItem(cursor_face_storage_key, JSON.stringify({
1111
+ x: positionRef.current.x,
1112
+ y: positionRef.current.y
1113
+ }));
1114
+ } catch {}
1115
+ };
1116
+ /**
1117
+ * 첫 진입, BFCache 복귀의 좌표를 한 번 소비. 외부 저장 값은 검증 후 사용
1118
+ */
1119
+ const handlePageShow = () => {
1120
+ try {
1121
+ const stored = sessionStorage.getItem(cursor_face_storage_key);
1122
+ sessionStorage.removeItem(cursor_face_storage_key);
1123
+ if (stored === null || !mediaRef.current?.matches) return;
1124
+ const point = JSON.parse(stored);
1125
+ if (typeof point === "object" && point !== null && "x" in point && "y" in point && typeof point.x === "number" && typeof point.y === "number" && Number.isFinite(point.x) && Number.isFinite(point.y)) {
1126
+ positionRef.current.x = point.x;
1127
+ positionRef.current.y = point.y;
1128
+ positionRef.current.hasPointer = true;
1129
+ renderCursorFace();
1130
+ }
1131
+ } catch {}
1132
+ };
1133
+ /**
1134
+ * 첫 paint 전 좌표 복원. 해제 시 전역 이벤트와 예약 frame을 함께 정리
1135
+ */
1136
+ useLayoutEffect(() => {
1137
+ if (!props.ready) return;
1138
+ const lifetime = new AbortController();
1139
+ const media = matchMedia("(hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference)");
1140
+ mediaRef.current = media;
1141
+ handlePageShow();
1142
+ document.addEventListener("pointermove", handleDocumentPointer, {
1143
+ passive: true,
1144
+ signal: lifetime.signal
1145
+ });
1146
+ document.addEventListener("pointerdown", handleDocumentPointer, {
1147
+ passive: true,
1148
+ signal: lifetime.signal
1149
+ });
1150
+ document.documentElement.addEventListener("pointerleave", handleCursorHide, { signal: lifetime.signal });
1151
+ addEventListener("blur", handleCursorHide, { signal: lifetime.signal });
1152
+ addEventListener("resize", handleCursorResize, { signal: lifetime.signal });
1153
+ addEventListener("pagehide", handlePageHide, { signal: lifetime.signal });
1154
+ addEventListener("pageshow", handlePageShow, { signal: lifetime.signal });
1155
+ media.addEventListener("change", handleCursorMediaChange, { signal: lifetime.signal });
1156
+ return () => {
1157
+ lifetime.abort();
1158
+ mediaRef.current = null;
1159
+ handleCursorHide();
1160
+ };
1161
+ }, [props.ready]);
1162
+ return /* @__PURE__ */ jsx("img", {
1163
+ ref: props.ready ? faceRef : void 0,
1164
+ className: clsx("wg_cursorFace__root"),
1165
+ src: asset_favicon_path,
1166
+ width: 24,
1167
+ height: 24,
1168
+ alt: "",
1169
+ "aria-hidden": "true",
1170
+ draggable: false,
1171
+ "data-cursor-face": ""
1172
+ });
1173
+ };
1174
+ //#endregion
1175
+ //#region src/component/widget/mermaid/_constant/copy.ts
1176
+ /**
1177
+ * 모듈 수신 실패의 원문 유지 안내
1178
+ */
1179
+ var copy_error_load = "다이어그램을 불러오지 못했습니다. 원문을 확인해 주세요.";
1180
+ /**
1181
+ * 문법이나 렌더 실패의 원문 유지 안내
1182
+ */
1183
+ var copy_error_render = "다이어그램을 그리지 못했습니다. 원문을 확인해 주세요.";
1184
+ //#endregion
1185
+ //#region src/component/widget/mermaid/_constant/render.ts
1186
+ /**
1187
+ * 원문 크기 제한. Mermaid의 제한 안내 SVG 대신 오류와 원문 표시
1188
+ */
1189
+ var render_source_max_chars = 5e4;
1190
+ /**
1191
+ * 공식 유형별 기본 표현, 자동 실행과 오류 SVG 삽입 제외
1192
+ */
1193
+ var render_config = {
1194
+ startOnLoad: false,
1195
+ securityLevel: "strict",
1196
+ suppressErrorRendering: true,
1197
+ maxTextSize: render_source_max_chars
1198
+ };
1199
+ /**
1200
+ * 공식 어두운 테마, Tree View 기본 검정 글자와 선의 대비 보정
1201
+ *
1202
+ * - 제약: Mermaid 12.2의 Tree View 스타일은 공통 테마 글색 미사용
1203
+ */
1204
+ var render_dark_config = {
1205
+ ...render_config,
1206
+ theme: "redux-dark-color",
1207
+ themeVariables: { treeView: {
1208
+ labelColor: "currentColor",
1209
+ lineColor: "currentColor"
1210
+ } }
1211
+ };
1212
+ //#endregion
1213
+ //#region src/component/widget/mermaid/wg-mermaid.tsx
1214
+ /**
1215
+ * 서버 본문의 Mermaid 영역 렌더 수명. 셸과 본문 DOM 교체 미포함
1216
+ */
1217
+ var WgMermaid = (props) => {
1218
+ /**
1219
+ * 본문 파싱 후 모듈 수신, 테마 변경과 Details 공개의 직렬 렌더
1220
+ */
1221
+ useEffect(() => {
1222
+ const lifetime = new AbortController();
1223
+ const theme = matchMedia("(prefers-color-scheme: dark)");
1224
+ const measurements = /* @__PURE__ */ new Set();
1225
+ const owned = /* @__PURE__ */ new Set();
1226
+ const tooltips = /* @__PURE__ */ new Set();
1227
+ /**
1228
+ * 공유 툴팁 재사용, 테마나 접힘 변경에서 숨긴 안내의 다음 호버 복원
1229
+ */
1230
+ const handleTooltipShow = (event) => {
1231
+ if (event.target instanceof Element && event.target.closest("[title]") !== null) for (const tooltip of tooltips) {
1232
+ tooltip.hidden = false;
1233
+ const bounds = tooltip.getBoundingClientRect();
1234
+ tooltip.style.left = `${scrollX + Math.max(0, Math.min(bounds.left, document.documentElement.clientWidth - bounds.width))}px`;
1235
+ }
1236
+ };
1237
+ /**
1238
+ * SVG 이탈이나 배치 변경에서 툴팁의 이전 절대 좌표와 스크롤 넘침 제외
1239
+ */
1240
+ const handleTooltipHide = () => {
1241
+ for (const tooltip of tooltips) tooltip.hidden = true;
1242
+ };
1243
+ const render = toChangeQueue({
1244
+ run: async () => {
1245
+ const elements = [...document.querySelectorAll(".wg_mermaid__root[data-mermaid]")];
1246
+ const path = elements[0]?.dataset.mermaidModule;
1247
+ if (path === void 0 || lifetime.signal.aborted) return;
1248
+ const mode = theme.matches ? "dark" : "light";
1249
+ const pending = elements.filter((element) => element.checkVisibility({ visibilityProperty: true }) && element.dataset.mermaidTheme !== mode);
1250
+ if (pending.length === 0) return;
1251
+ const library = await import(
1252
+ /* @vite-ignore */
1253
+ path
1254
+ );
1255
+ await document.fonts.ready;
1256
+ if (lifetime.signal.aborted) return;
1257
+ for (const element of pending) {
1258
+ if (lifetime.signal.aborted || !element.isConnected || theme.matches !== (mode === "dark")) return;
1259
+ const source = element.querySelector(".wg_mermaid__source");
1260
+ const result = element.querySelector(".wg_mermaid__result");
1261
+ const error = element.querySelector(".wg_mermaid__error");
1262
+ if (source === null || result === null || error === null) continue;
1263
+ const measurement = document.createElement("div");
1264
+ measurement.className = "wg_mermaid__measurement";
1265
+ measurements.add(measurement);
1266
+ document.body.append(measurement);
1267
+ try {
1268
+ if (source.textContent.length > 5e4) throw new Error(`Mermaid source exceeds ${render_source_max_chars} characters`);
1269
+ library.default.initialize(mode === "dark" ? render_dark_config : render_config);
1270
+ const rendered = await library.default.render(`fh-mermaid-${crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(4)).join("-")}`, source.textContent, measurement);
1271
+ if (lifetime.signal.aborted || !element.isConnected || theme.matches !== (mode === "dark")) return;
1272
+ const shadow = result.shadowRoot ?? result.attachShadow({ mode: "open" });
1273
+ const canvas = document.createElement("div");
1274
+ canvas.innerHTML = rendered.svg;
1275
+ shadow.replaceChildren(canvas);
1276
+ rendered.bindFunctions?.(canvas);
1277
+ canvas.addEventListener("mouseover", handleTooltipShow, { signal: lifetime.signal });
1278
+ canvas.addEventListener("mouseout", handleTooltipHide, { signal: lifetime.signal });
1279
+ const tooltip = document.querySelector(".mermaidTooltip");
1280
+ if (tooltip !== null) {
1281
+ tooltip.classList.add("wg_mermaid__tooltip");
1282
+ tooltips.add(tooltip);
1283
+ }
1284
+ result.hidden = false;
1285
+ source.hidden = true;
1286
+ error.hidden = true;
1287
+ element.dataset.mermaidStatus = "rendered";
1288
+ } catch (cause) {
1289
+ if (lifetime.signal.aborted) return;
1290
+ result.hidden = true;
1291
+ source.hidden = false;
1292
+ error.textContent = copy_error_render;
1293
+ error.hidden = false;
1294
+ element.dataset.mermaidStatus = "error";
1295
+ console.error("for-humanity Mermaid rendering failed", cause);
1296
+ } finally {
1297
+ measurement.remove();
1298
+ measurements.delete(measurement);
1299
+ }
1300
+ element.dataset.mermaidTheme = mode;
1301
+ owned.add(element);
1302
+ }
1303
+ props.onRendered?.();
1304
+ },
1305
+ onError: (cause) => {
1306
+ if (lifetime.signal.aborted) return;
1307
+ for (const element of document.querySelectorAll(".wg_mermaid__root[data-mermaid]")) {
1308
+ const error = element.querySelector(".wg_mermaid__error");
1309
+ if (error !== null) {
1310
+ owned.add(element);
1311
+ error.textContent = copy_error_load;
1312
+ error.hidden = false;
1313
+ }
1314
+ }
1315
+ console.error("for-humanity Mermaid loading failed", cause);
1316
+ }
1317
+ });
1318
+ /**
1319
+ * 연속 이벤트 집약, 직전 렌더 완료 후 최신 테마 반영
1320
+ */
1321
+ const handleRender = () => {
1322
+ handleTooltipHide();
1323
+ render();
1324
+ };
1325
+ theme.addEventListener("change", handleRender, { signal: lifetime.signal });
1326
+ addEventListener("resize", handleTooltipHide, { signal: lifetime.signal });
1327
+ document.addEventListener("toggle", handleRender, {
1328
+ capture: true,
1329
+ signal: lifetime.signal
1330
+ });
1331
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", handleRender, {
1332
+ once: true,
1333
+ signal: lifetime.signal
1334
+ });
1335
+ else handleRender();
1336
+ return () => {
1337
+ lifetime.abort();
1338
+ for (const measurement of measurements) measurement.remove();
1339
+ measurements.clear();
1340
+ for (const tooltip of tooltips) tooltip.remove();
1341
+ tooltips.clear();
1342
+ for (const element of owned) {
1343
+ const source = element.querySelector(".wg_mermaid__source");
1344
+ const result = element.querySelector(".wg_mermaid__result");
1345
+ const error = element.querySelector(".wg_mermaid__error");
1346
+ result?.shadowRoot?.replaceChildren();
1347
+ if (result !== null) result.hidden = true;
1348
+ if (source !== null) source.hidden = false;
1349
+ if (error !== null) error.hidden = true;
1350
+ delete element.dataset.mermaidTheme;
1351
+ delete element.dataset.mermaidStatus;
1352
+ }
1353
+ owned.clear();
1354
+ };
1355
+ }, [props.onRendered]);
1356
+ return null;
1357
+ };
1358
+ //#endregion
1359
+ //#region src/component/widget/navigation/_constant/navigation.ts
1360
+ /**
1361
+ * 모바일 탐색 전환. wg-shell.css의 폭 조건과 일치
1362
+ */
1363
+ var navigation_mobile_query = "(width < 1024px)";
1364
+ /**
1365
+ * 양쪽 200px 탐색, 간격 120px, 최소 800px 본문과 스크롤바가 들어가는 3열 기준
1366
+ */
1367
+ var navigation_wide_query = "(width >= 1360px)";
1368
+ //#endregion
1369
+ //#region src/component/widget/navigation/_function/restore-navigation-scroll.ts
1370
+ /**
1371
+ * 탐색 진입점에서 첫 paint 전 복원. 기존 배치별 저장 자료도 한 번 수용
1372
+ */
1373
+ var restoreNavigationScroll = (options) => {
1374
+ const navigation = document.querySelector("[data-navigation]");
1375
+ const documents = document.querySelector("[data-navigation-scroll=\"documents\"]");
1376
+ const outline = document.querySelector("[data-navigation-scroll=\"outline\"]");
1377
+ if (!navigation || !documents || !outline) return;
1378
+ navigation.dataset.navigationLayout = options.layout;
1379
+ try {
1380
+ const value = sessionStorage.getItem(options.key);
1381
+ const stored = value === null ? void 0 : JSON.parse(value);
1382
+ const state = typeof stored === "object" && stored !== null && "version" in stored && stored.version === options.version && "state" in stored ? options.toState(stored.state) : { positions: {} };
1383
+ const legacy = sessionStorage.getItem(`${options.key}:${options.layout === "drawer" ? "drawer" : "desktop"}`);
1384
+ const previous = state.positions[options.layout] === void 0 && legacy !== null ? options.toState({ positions: { [options.layout]: JSON.parse(legacy) } }).positions[options.layout] : void 0;
1385
+ const position = state.positions[options.layout] ?? previous;
1386
+ const signature = [...navigation.querySelectorAll(".wg_navigationNav__root a[href]")].map((link) => `${link.pathname}:${link.textContent}`).join("\n");
1387
+ if (position === void 0 || position.navigation !== signature) return;
1388
+ documents.scrollTop = position.documents;
1389
+ if (position.pathname === location.pathname) outline.scrollTop = position.outline;
1390
+ } catch {}
1391
+ };
1392
+ //#endregion
1393
+ //#region src/component/widget/navigation/_wg-navigation-nav-list.tsx
1394
+ var WgNavigationNavList = (props) => {
1395
+ const entries = [...props.group.docs.map((doc) => ({
1396
+ kind: "document",
1397
+ key: `doc:${doc.id}`,
1398
+ doc
1399
+ })), ...props.group.groups.map((group) => ({
1400
+ kind: "group",
1401
+ key: `group:${JSON.stringify(group.path)}`,
1402
+ group
1403
+ }))];
1404
+ return /* @__PURE__ */ jsx("ul", {
1405
+ className: clsx("wg_navigationNavList__root"),
1406
+ "aria-label": props.group.name,
1407
+ children: entries.map((entry) => /* @__PURE__ */ jsxs("li", {
1408
+ className: clsx("wg_navigationNavList__item"),
1409
+ children: [entry.kind === "document" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("a", {
1410
+ className: clsx("wg_navigationNavList__link", { "wg_navigationNavList__link--active": entry.doc.active }),
1411
+ href: `/${entry.doc.id}/`,
1412
+ title: entry.doc.label,
1413
+ "aria-current": entry.doc.active ? "page" : void 0,
1414
+ children: entry.doc.name
1415
+ }), entry.doc.children.length > 0 && /* @__PURE__ */ jsx(WgNavigationNavList, { group: {
1416
+ name: entry.doc.name,
1417
+ path: props.group.path,
1418
+ docs: entry.doc.children,
1419
+ groups: []
1420
+ } })] }), entry.kind === "group" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1421
+ className: clsx("wg_navigationNavList__branch"),
1422
+ children: entry.group.name
1423
+ }), /* @__PURE__ */ jsx(WgNavigationNavList, { group: entry.group })] })]
1424
+ }, entry.key))
1425
+ });
1426
+ };
1427
+ //#endregion
1428
+ //#region src/component/widget/navigation/_wg-navigation-nav.tsx
1429
+ var WgNavigationNav = (props) => {
1430
+ return /* @__PURE__ */ jsx("nav", {
1431
+ className: clsx("wg_navigationNav__root"),
1432
+ "aria-label": copy_nav_aria_label,
1433
+ children: /* @__PURE__ */ jsx("div", {
1434
+ className: clsx("wg_navigationNav__docs"),
1435
+ children: props.groups.map((group) => /* @__PURE__ */ jsxs("section", {
1436
+ className: clsx("wg_navigationNav__docGroup"),
1437
+ "aria-label": group.name,
1438
+ children: [/* @__PURE__ */ jsx("div", {
1439
+ className: clsx("wg_navigationNav__group"),
1440
+ children: group.name
1441
+ }), /* @__PURE__ */ jsx(WgNavigationNavList, { group })]
1442
+ }, JSON.stringify(group.path)))
1443
+ })
1444
+ });
1445
+ };
1446
+ //#endregion
1447
+ //#region src/component/widget/navigation/_wg-navigation-scroll.tsx
1448
+ var WgNavigationScroll = (props) => {
1449
+ const [overflow, setOverflow] = useState({
1450
+ above: false,
1451
+ below: false
1452
+ });
1453
+ const frameRef = useRef(0);
1454
+ /**
1455
+ * 소수 픽셀 반올림을 제외한 실제 남은 내용만 위, 아래 흐림으로 알림
1456
+ */
1457
+ const updateOverflow = () => {
1458
+ frameRef.current = 0;
1459
+ if (props.portRef.current === null) return;
1460
+ const above = props.portRef.current.scrollTop > 1;
1461
+ const below = props.portRef.current.scrollHeight - props.portRef.current.clientHeight - props.portRef.current.scrollTop > 1;
1462
+ setOverflow((state) => state.above === above && state.below === below ? state : {
1463
+ above,
1464
+ below
1465
+ });
1466
+ };
1467
+ /**
1468
+ * 연속 스크롤과 관찰자 알림은 프레임당 한 번 측정
1469
+ */
1470
+ const scheduleOverflow = () => {
1471
+ if (frameRef.current === 0) frameRef.current = requestAnimationFrame(updateOverflow);
1472
+ };
1473
+ /**
1474
+ * 네이티브 좌표는 바로 저장하고 시각적 넘침만 프레임에 맞춰 갱신
1475
+ */
1476
+ const handlePortScroll = (_event) => {
1477
+ props.onScroll?.();
1478
+ scheduleOverflow();
1479
+ };
1480
+ /**
1481
+ * 영역과 내용의 크기를 함께 추적. Hono 최초 mount의 분리된 fragment 측정은 연결 직후 실행
1482
+ */
1483
+ useLayoutEffect(() => {
1484
+ if (!props.ready || props.portRef.current === null) return;
1485
+ const observer = new ResizeObserver(scheduleOverflow);
1486
+ observer.observe(props.portRef.current);
1487
+ if (props.portRef.current.firstElementChild !== null) observer.observe(props.portRef.current.firstElementChild);
1488
+ let active = true;
1489
+ addEventListener("pageshow", scheduleOverflow);
1490
+ if (props.portRef.current.isConnected) updateOverflow();
1491
+ else queueMicrotask(() => {
1492
+ if (active) updateOverflow();
1493
+ });
1494
+ return () => {
1495
+ active = false;
1496
+ observer.disconnect();
1497
+ removeEventListener("pageshow", scheduleOverflow);
1498
+ cancelAnimationFrame(frameRef.current);
1499
+ frameRef.current = 0;
1500
+ };
1501
+ }, [props.portRef, props.ready]);
1502
+ /**
1503
+ * 접힘 선택의 렌더가 바뀐 직후 측정. 좌표 복원은 부모의 연결 후 처리보다 앞서 저장하지 않음
1504
+ */
1505
+ useLayoutEffect(() => {
1506
+ if (props.ready && props.portRef.current?.isConnected) updateOverflow();
1507
+ }, [props.children, props.ready]);
1508
+ return /* @__PURE__ */ jsx("div", {
1509
+ ref: props.ready ? props.portRef : void 0,
1510
+ className: clsx("wg_navigation__scroll", props.kind === "documents" && "wg_navigation__documents", props.kind === "outline" && "wg_navigation__headings", props.ready && "wg_navigation__scroll--ready", props.ready && overflow.above && "wg_navigation__scroll--above", props.ready && overflow.below && "wg_navigation__scroll--below"),
1511
+ "data-navigation-scroll": props.kind,
1512
+ onClick: props.onClick,
1513
+ onScroll: handlePortScroll,
1514
+ children: props.children
1515
+ });
1516
+ };
1517
+ //#endregion
1518
+ //#region src/util/dom/find-hash-target.ts
1519
+ /**
1520
+ * URL hash 대상. 한국어 제목의 percent encoding 해석
1521
+ */
1522
+ var findHashTarget = (hash) => {
1523
+ if (!hash) return null;
1524
+ try {
1525
+ return document.getElementById(decodeURIComponent(hash.slice(1)));
1526
+ } catch (error) {
1527
+ if (error instanceof URIError) return null;
1528
+ throw error;
1529
+ }
1530
+ };
1531
+ //#endregion
1532
+ //#region src/component/widget/navigation/_wg-navigation-toc.tsx
1533
+ var WgNavigationToc = (props) => {
1534
+ const [currentSlug, setCurrentSlug] = useState();
1535
+ /**
1536
+ * 연결마다 구독 설치, 초기 위치 갱신은 본문 파싱 이후 실행
1537
+ */
1538
+ useLayoutEffect(() => {
1539
+ const headings = props.groups.flatMap((group) => group.sections).flatMap((section) => [section.heading, ...section.subs.map((sub) => sub.heading)]);
1540
+ const frame = { id: 0 };
1541
+ /**
1542
+ * 접힌 본문 제목은 제외하고 읽는 선을 통과한 마지막 제목 선택
1543
+ */
1544
+ const updateCurrentHeading = () => {
1545
+ frame.id = 0;
1546
+ const visible = headings.flatMap((heading) => {
1547
+ const target = findHashTarget(`#${heading.slug}`);
1548
+ return target?.checkVisibility() ? [{
1549
+ slug: heading.slug,
1550
+ top: target.getBoundingClientRect().top - Number.parseFloat(getComputedStyle(target).scrollMarginTop)
1551
+ }] : [];
1552
+ });
1553
+ setCurrentSlug(visible.findLast((heading) => heading.top <= 1)?.slug ?? visible[0]?.slug);
1554
+ };
1555
+ /**
1556
+ * 연속 스크롤의 프레임당 1회 위치 조회
1557
+ */
1558
+ const handleReadingPositionChange = () => {
1559
+ if (frame.id === 0) frame.id = requestAnimationFrame(updateCurrentHeading);
1560
+ };
1561
+ addEventListener("scroll", handleReadingPositionChange, { passive: true });
1562
+ addEventListener("resize", handleReadingPositionChange);
1563
+ addEventListener("toggle", handleReadingPositionChange, true);
1564
+ addEventListener("pageshow", handleReadingPositionChange);
1565
+ document.addEventListener("DOMContentLoaded", handleReadingPositionChange);
1566
+ if (document.readyState !== "loading") updateCurrentHeading();
1567
+ return () => {
1568
+ cancelAnimationFrame(frame.id);
1569
+ removeEventListener("scroll", handleReadingPositionChange);
1570
+ removeEventListener("resize", handleReadingPositionChange);
1571
+ removeEventListener("toggle", handleReadingPositionChange, true);
1572
+ removeEventListener("pageshow", handleReadingPositionChange);
1573
+ document.removeEventListener("DOMContentLoaded", handleReadingPositionChange);
1574
+ };
1575
+ }, [props.groups]);
1576
+ return /* @__PURE__ */ jsxs("nav", {
1577
+ className: clsx("wg_navigationToc__root"),
1578
+ "aria-label": copy_nav_toc_label,
1579
+ children: [/* @__PURE__ */ jsx("div", {
1580
+ className: clsx("wg_navigationToc__label"),
1581
+ children: copy_nav_toc_label
1582
+ }), props.groups.map((group) => /* @__PURE__ */ jsxs("div", {
1583
+ className: clsx("wg_navigationToc__groupSection", { "wg_navigationToc__groupSection--named": group.part !== void 0 }),
1584
+ children: [group.part !== void 0 && /* @__PURE__ */ jsx("div", {
1585
+ className: clsx("wg_navigationToc__group"),
1586
+ children: group.part
1587
+ }), /* @__PURE__ */ jsx("ul", {
1588
+ className: clsx("wg_navigationToc__list"),
1589
+ children: group.sections.map((section) => /* @__PURE__ */ jsxs("li", {
1590
+ className: clsx("wg_navigationToc__item"),
1591
+ children: [/* @__PURE__ */ jsx("a", {
1592
+ className: clsx("wg_navigationToc__link", { "wg_navigationToc__link--active": section.heading.slug === currentSlug }),
1593
+ href: `#${section.heading.slug}`,
1594
+ "data-toc-link": "",
1595
+ "aria-current": section.heading.slug === currentSlug ? "location" : void 0,
1596
+ children: section.heading.text
1597
+ }), section.subs.length > 0 && /* @__PURE__ */ jsx("ul", {
1598
+ className: clsx("wg_navigationToc__sub"),
1599
+ children: section.subs.map((sub) => /* @__PURE__ */ jsx("li", {
1600
+ className: clsx("wg_navigationToc__subItem"),
1601
+ children: /* @__PURE__ */ jsx("a", {
1602
+ className: clsx("wg_navigationToc__subLink", { "wg_navigationToc__subLink--active": sub.heading.slug === currentSlug }),
1603
+ href: `#${sub.heading.slug}`,
1604
+ "data-toc-sub-link": "",
1605
+ "aria-current": sub.heading.slug === currentSlug ? "location" : void 0,
1606
+ children: sub.heading.text
1607
+ })
1608
+ }, sub.heading.slug))
1609
+ })]
1610
+ }, section.heading.slug))
1611
+ })]
1612
+ }, group.sections[0].heading.slug))]
1613
+ });
1614
+ };
1615
+ //#endregion
1616
+ //#region src/store/navigation/navigation-storage.ts
1617
+ /**
1618
+ * 같은 탭에서 문서 이동 후 탐색 위치 복원. 데스크톱과 드로어는 별도 저장
1619
+ */
1620
+ var navigation_scroll_storage_key = "for-humanity:navigation-scroll";
1621
+ //#endregion
1622
+ //#region src/store/navigation/to-navigation-scroll-state.ts
1623
+ /**
1624
+ * 알 수 없는 저장 형태, 음수, 무한 좌표 제외. 초기 HTML과 Zustand에 같은 검증 적용
1625
+ */
1626
+ var toNavigationScrollState = (value) => {
1627
+ /**
1628
+ * 한 배치의 완전한 좌표만 복원 대상으로 수용
1629
+ */
1630
+ const toPosition = (position) => {
1631
+ if (typeof position !== "object" || position === null || !("navigation" in position) || typeof position.navigation !== "string" || !("pathname" in position) || typeof position.pathname !== "string" || !("documents" in position) || typeof position.documents !== "number" || !Number.isFinite(position.documents) || position.documents < 0 || !("outline" in position) || typeof position.outline !== "number" || !Number.isFinite(position.outline) || position.outline < 0) return;
1632
+ return {
1633
+ navigation: position.navigation,
1634
+ pathname: position.pathname,
1635
+ documents: position.documents,
1636
+ outline: position.outline
1637
+ };
1638
+ };
1639
+ if (typeof value !== "object" || value === null || !("positions" in value) || typeof value.positions !== "object" || value.positions === null) return { positions: {} };
1640
+ const positions = {
1641
+ wide: toPosition("wide" in value.positions ? value.positions.wide : void 0),
1642
+ desktop: toPosition("desktop" in value.positions ? value.positions.desktop : void 0),
1643
+ drawer: toPosition("drawer" in value.positions ? value.positions.drawer : void 0)
1644
+ };
1645
+ return { positions: Object.fromEntries(Object.entries(positions).filter((entry) => entry[1] !== void 0)) };
1646
+ };
1647
+ //#endregion
1648
+ //#region src/util/dom/reveal-hash-target.ts
1649
+ /**
1650
+ * 접힌 내용의 hash 대상과 상위 Details 공개
1651
+ *
1652
+ * - 초기 접속, 문서 안 링크, hash 변경에 동일 적용
1653
+ */
1654
+ var revealHashTarget = (hash) => {
1655
+ const target = findHashTarget(hash);
1656
+ for (let fold = target?.closest("details"); fold; fold = fold.parentElement?.closest("details")) fold.open = true;
1657
+ return target;
1658
+ };
1659
+ //#endregion
1660
+ //#region src/component/widget/navigation/wg-navigation.tsx
1661
+ var WgNavigation = (props) => {
1662
+ const [layout, setLayout] = useState(() => {
1663
+ if (props.stores === void 0) return "desktop";
1664
+ if (matchMedia("(width < 1024px)").matches) return "drawer";
1665
+ return matchMedia("(width >= 1360px)").matches ? "wide" : "desktop";
1666
+ });
1667
+ const [drawerOpen, setDrawerOpen] = useState(false);
1668
+ const navigationRef = useRef(null);
1669
+ const documentsRef = useRef(null);
1670
+ const drawerNavigationRef = useRef(null);
1671
+ const drawerDocumentsRef = useRef(null);
1672
+ const outlineRef = useRef(null);
1673
+ const dialogRef = useRef(null);
1674
+ const openButtonRef = useRef(null);
1675
+ const brandRef = useRef(null);
1676
+ const mobileRef = useRef(null);
1677
+ const wideRef = useRef(null);
1678
+ const layoutRef = useRef(layout);
1679
+ const focusRef = useRef(null);
1680
+ const restorePendingRef = useRef(true);
1681
+ /**
1682
+ * 저장된 배치와 현재 CSS 조건을 구분. 조건 변경 직후 잘린 좌표를 이전 배치에 저장하지 않음
1683
+ */
1684
+ const getNavigationLayout = () => {
1685
+ if (mobileRef.current === null || wideRef.current === null) return layoutRef.current;
1686
+ if (mobileRef.current.matches) return "drawer";
1687
+ return wideRef.current.matches ? "wide" : "desktop";
1688
+ };
1689
+ /**
1690
+ * 배치별 DOM 참조 분리, 이전 노드 정리 시 새 배치 참조 유지
1691
+ */
1692
+ const getNavigationPort = () => {
1693
+ return layoutRef.current === "drawer" ? drawerNavigationRef.current : navigationRef.current;
1694
+ };
1695
+ /**
1696
+ * 배치별 DOM은 분리하되 좌표 저장과 복원은 현재 표시되는 한 영역을 소비
1697
+ */
1698
+ const getDocumentsPort = () => {
1699
+ return layoutRef.current === "drawer" ? drawerDocumentsRef.current : documentsRef.current;
1700
+ };
1701
+ /**
1702
+ * 모든 링크의 URL, 표시명으로 사이트 공통 문서 목록의 동일성 판정
1703
+ */
1704
+ const getNavigationSignature = () => {
1705
+ const navigation = getNavigationPort();
1706
+ if (navigation === null) return void 0;
1707
+ return [...navigation.querySelectorAll(".wg_navigationNav__root a[href]")].map((link) => `${link.pathname}:${link.textContent}`).join("\n");
1708
+ };
1709
+ /**
1710
+ * 닫기, 이동보다 먼저 실제 좌표 보존. 숨긴 모달과 아직 복원하지 않은 배치는 저장하지 않음
1711
+ */
1712
+ const saveScroll = () => {
1713
+ const documents = getDocumentsPort();
1714
+ if (props.stores === void 0 || documents === null || outlineRef.current === null || restorePendingRef.current || layoutRef.current !== getNavigationLayout() || layoutRef.current === "drawer" && dialogRef.current?.open !== true) return;
1715
+ const signature = getNavigationSignature();
1716
+ if (signature === void 0) return;
1717
+ const navigationPosition = {
1718
+ navigation: signature,
1719
+ pathname: location.pathname,
1720
+ documents: documents.scrollTop,
1721
+ outline: outlineRef.current.scrollTop
1722
+ };
1723
+ props.stores.scroll.setState((state) => ({ positions: {
1724
+ ...state.positions,
1725
+ [layoutRef.current]: navigationPosition
1726
+ } }));
1727
+ };
1728
+ /**
1729
+ * 연결된 영역에서 첫 paint 전 복원. 문서는 페이지 간 공유하고 목차는 같은 경로에서만 재사용
1730
+ */
1731
+ const restoreScroll = () => {
1732
+ layoutRef.current = getNavigationLayout();
1733
+ const documents = getDocumentsPort();
1734
+ const navigation = getNavigationPort();
1735
+ if (props.stores === void 0 || documents === null || outlineRef.current === null || navigation === null) return;
1736
+ if (layoutRef.current === "drawer" && dialogRef.current?.open !== true) return;
1737
+ const signature = getNavigationSignature();
1738
+ if (signature === void 0) return;
1739
+ const position = props.stores.scroll.getState().positions[layoutRef.current];
1740
+ documents.scrollTop = 0;
1741
+ outlineRef.current.scrollTop = 0;
1742
+ if (position === void 0) restoreNavigationScroll({
1743
+ key: navigation_scroll_storage_key,
1744
+ version: 1,
1745
+ layout: layoutRef.current,
1746
+ toState: toNavigationScrollState
1747
+ });
1748
+ else if (position.navigation === signature) {
1749
+ documents.scrollTop = position.documents;
1750
+ if (position.pathname === location.pathname) outlineRef.current.scrollTop = position.outline;
1751
+ }
1752
+ navigation.dataset.navigationLayout = layoutRef.current;
1753
+ restorePendingRef.current = false;
1754
+ saveScroll();
1755
+ documents.dispatchEvent(new Event("scroll"));
1756
+ outlineRef.current.dispatchEvent(new Event("scroll"));
1757
+ };
1758
+ /**
1759
+ * 네이티브 close가 이전 열기 이벤트보다 늦게 도착해도 새 모달을 잠금 해제하지 않음
1760
+ */
1761
+ const handleDrawerClose = (_event) => {
1762
+ if (dialogRef.current?.open === true) return;
1763
+ document.body.classList.remove("wg_shell__root--drawerOpen");
1764
+ setDrawerOpen(false);
1765
+ };
1766
+ /**
1767
+ * 네이티브 포커스 복원을 유지하면서 링크의 기본 이동보다 먼저 배경 잠금 해제
1768
+ */
1769
+ const closeNavigation = () => {
1770
+ saveScroll();
1771
+ dialogRef.current?.close();
1772
+ document.body.classList.remove("wg_shell__root--drawerOpen");
1773
+ setDrawerOpen(false);
1774
+ };
1775
+ /**
1776
+ * 모바일에서만 네이티브 모달을 열고 독립된 드로어 좌표를 즉시 복원
1777
+ */
1778
+ const handleOpenButtonClick = (_event) => {
1779
+ if (getNavigationLayout() !== "drawer" || dialogRef.current === null || dialogRef.current.open) return;
1780
+ dialogRef.current.showModal();
1781
+ document.body.classList.add("wg_shell__root--drawerOpen");
1782
+ setDrawerOpen(true);
1783
+ restoreScroll();
1784
+ };
1785
+ const handleCloseButtonClick = (_event) => {
1786
+ closeNavigation();
1787
+ };
1788
+ /**
1789
+ * Escape의 네이티브 포커스 복원을 쓰되 좌표는 닫힌 모달이 되기 전에 보존
1790
+ */
1791
+ const handleDrawerCancel = (event) => {
1792
+ event.preventDefault();
1793
+ closeNavigation();
1794
+ };
1795
+ /**
1796
+ * 대화상자 내부 빈 공간은 유지하고 경계 밖 배경만 닫기
1797
+ */
1798
+ const handleDrawerClick = (event) => {
1799
+ if (dialogRef.current === null || event.target !== dialogRef.current) return;
1800
+ const bounds = dialogRef.current.getBoundingClientRect();
1801
+ if (event.clientX < bounds.left || event.clientX > bounds.right || event.clientY < bounds.top || event.clientY > bounds.bottom) closeNavigation();
1802
+ };
1803
+ /**
1804
+ * 보조키, 새 탭 링크는 모달 유지. 기본 문서 이동만 닫고 같은 문서 제목에 키보드 위치 전달
1805
+ */
1806
+ const handleNavigationClick = (event) => {
1807
+ saveScroll();
1808
+ if (dialogRef.current?.open !== true || event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || !(event.target instanceof Element)) return;
1809
+ const link = event.target.closest("a[href]");
1810
+ if (!(link instanceof HTMLAnchorElement) || link.target !== "" && link.target !== "_self" || link.hasAttribute("download")) return;
1811
+ closeNavigation();
1812
+ if (link.origin === location.origin && link.pathname === location.pathname && link.search === location.search && link.hash) {
1813
+ const target = revealHashTarget(link.hash);
1814
+ if (target !== null) {
1815
+ target.tabIndex = -1;
1816
+ target.focus({ preventScroll: true });
1817
+ }
1818
+ }
1819
+ };
1820
+ /**
1821
+ * 같은 목록을 한 배치에만 렌더. 전환 전 포커스를 기억하고 새 배치가 연결된 뒤 좌표 복원
1822
+ */
1823
+ const handleLayoutChange = () => {
1824
+ const nextLayout = getNavigationLayout();
1825
+ if (nextLayout === layoutRef.current) return;
1826
+ if (document.activeElement instanceof HTMLElement) focusRef.current = {
1827
+ navigation: document.activeElement.closest("[data-navigation]") !== null,
1828
+ drawer: document.activeElement === openButtonRef.current || document.activeElement.closest("[data-navigation-drawer]") !== null,
1829
+ href: document.activeElement.getAttribute("href")
1830
+ };
1831
+ if (nextLayout !== "drawer") closeNavigation();
1832
+ restorePendingRef.current = true;
1833
+ layoutRef.current = nextLayout;
1834
+ flushSync(() => setLayout(nextLayout));
1835
+ };
1836
+ /**
1837
+ * BFCache 복귀 시 저장 좌표로 동기 렌더 후 복원
1838
+ */
1839
+ const handlePageShow = (event) => {
1840
+ if (!event.persisted || props.stores === void 0) return;
1841
+ restorePendingRef.current = true;
1842
+ flushSync(() => props.stores?.scroll.persist.rehydrate());
1843
+ handleLayoutChange();
1844
+ restoreScroll();
1845
+ };
1846
+ /**
1847
+ * Hono 탐색이 전역 구독의 설치, 정리를 소유. 서버 렌더에서는 실행하지 않음
1848
+ */
1849
+ useLayoutEffect(() => {
1850
+ if (props.stores === void 0) return;
1851
+ mobileRef.current = matchMedia(navigation_mobile_query);
1852
+ wideRef.current = matchMedia(navigation_wide_query);
1853
+ mobileRef.current.addEventListener("change", handleLayoutChange);
1854
+ wideRef.current.addEventListener("change", handleLayoutChange);
1855
+ addEventListener("pagehide", saveScroll);
1856
+ addEventListener("pageshow", handlePageShow);
1857
+ return () => {
1858
+ mobileRef.current?.removeEventListener("change", handleLayoutChange);
1859
+ wideRef.current?.removeEventListener("change", handleLayoutChange);
1860
+ mobileRef.current = null;
1861
+ wideRef.current = null;
1862
+ removeEventListener("pagehide", saveScroll);
1863
+ removeEventListener("pageshow", handlePageShow);
1864
+ document.body.classList.remove("wg_shell__root--drawerOpen");
1865
+ };
1866
+ }, [props.stores]);
1867
+ /**
1868
+ * 최초 mount와 반응형 재연결의 paint 전 좌표, 포커스 복원. Hono의 fragment 연결 시점 보정
1869
+ */
1870
+ useLayoutEffect(() => {
1871
+ if (props.stores === void 0) return;
1872
+ let active = true;
1873
+ const restoreNavigation = () => {
1874
+ if (!active) return;
1875
+ restoreScroll();
1876
+ if (focusRef.current === null) return;
1877
+ const navigation = getNavigationPort();
1878
+ if (layout === "drawer") {
1879
+ if (focusRef.current.navigation) openButtonRef.current?.focus({ preventScroll: true });
1880
+ } else if (focusRef.current.navigation && navigation !== null) {
1881
+ const href = focusRef.current.href;
1882
+ [...navigation.querySelectorAll("a[href]")].find((element) => element.getAttribute("href") === href)?.focus({ preventScroll: true });
1883
+ } else if (focusRef.current.drawer) brandRef.current?.focus({ preventScroll: true });
1884
+ focusRef.current = null;
1885
+ };
1886
+ if (getDocumentsPort()?.isConnected) restoreNavigation();
1887
+ else queueMicrotask(restoreNavigation);
1888
+ return () => {
1889
+ active = false;
1890
+ };
1891
+ }, [layout, props.stores]);
1892
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1893
+ /* @__PURE__ */ jsxs("div", {
1894
+ className: clsx("wg_navigation__sidebar", props.stores !== void 0 && "wg_navigation__sidebar--navigationReady"),
1895
+ children: [/* @__PURE__ */ jsxs("div", {
1896
+ className: clsx("wg_navigation__brandRow"),
1897
+ children: [/* @__PURE__ */ jsx("a", {
1898
+ ref: props.stores === void 0 ? void 0 : brandRef,
1899
+ className: clsx("wg_navigation__brand"),
1900
+ href: "/",
1901
+ "aria-current": props.data.home ? "page" : void 0,
1902
+ children: props.data.title
1903
+ }), /* @__PURE__ */ jsx("button", {
1904
+ ref: props.stores === void 0 ? void 0 : openButtonRef,
1905
+ className: clsx("wg_navigation__menuToggle", props.stores !== void 0 && layout === "drawer" && "wg_navigation__menuToggle--visible"),
1906
+ type: "button",
1907
+ "aria-label": copy_nav_open,
1908
+ "aria-controls": "fh-navigation-drawer",
1909
+ "aria-expanded": String(drawerOpen),
1910
+ "data-navigation-open": "",
1911
+ onClick: handleOpenButtonClick,
1912
+ children: /* @__PURE__ */ jsx("svg", {
1913
+ className: clsx("wg_navigation__menuIcon"),
1914
+ viewBox: "0 0 24 24",
1915
+ "aria-hidden": "true",
1916
+ focusable: "false",
1917
+ children: /* @__PURE__ */ jsx("path", { d: "M4 6h16M4 12h16M4 18h16" })
1918
+ })
1919
+ })]
1920
+ }), layout !== "drawer" && /* @__PURE__ */ jsx("div", {
1921
+ ref: props.stores === void 0 ? void 0 : navigationRef,
1922
+ className: clsx("wg_navigation__navigation"),
1923
+ "data-navigation": "",
1924
+ children: /* @__PURE__ */ jsx(WgNavigationScroll, {
1925
+ portRef: documentsRef,
1926
+ kind: "documents",
1927
+ ready: props.stores !== void 0,
1928
+ onScroll: saveScroll,
1929
+ onClick: handleNavigationClick,
1930
+ children: /* @__PURE__ */ jsx(WgNavigationNav, { groups: props.data.groups })
1931
+ })
1932
+ })]
1933
+ }),
1934
+ /* @__PURE__ */ jsx("aside", {
1935
+ className: clsx("wg_navigation__tocRail"),
1936
+ "data-navigation-rail": "",
1937
+ children: /* @__PURE__ */ jsx("div", {
1938
+ className: clsx("wg_navigation__outline"),
1939
+ "data-navigation-outline": "",
1940
+ children: /* @__PURE__ */ jsx(WgNavigationScroll, {
1941
+ portRef: outlineRef,
1942
+ kind: "outline",
1943
+ ready: props.stores !== void 0,
1944
+ onScroll: saveScroll,
1945
+ onClick: handleNavigationClick,
1946
+ children: props.data.outline.length > 0 && /* @__PURE__ */ jsx(WgNavigationToc, { groups: props.data.outline })
1947
+ })
1948
+ })
1949
+ }),
1950
+ /* @__PURE__ */ jsxs("dialog", {
1951
+ ref: props.stores === void 0 ? void 0 : dialogRef,
1952
+ className: clsx("wg_navigation__drawer"),
1953
+ id: "fh-navigation-drawer",
1954
+ "aria-labelledby": "fh-navigation-title",
1955
+ "data-navigation-drawer": "",
1956
+ onClose: handleDrawerClose,
1957
+ onCancel: handleDrawerCancel,
1958
+ onClick: handleDrawerClick,
1959
+ children: [/* @__PURE__ */ jsxs("div", {
1960
+ className: clsx("wg_navigation__drawerHeader"),
1961
+ children: [/* @__PURE__ */ jsx("h2", {
1962
+ className: clsx("wg_navigation__drawerTitle"),
1963
+ id: "fh-navigation-title",
1964
+ children: copy_nav_title
1965
+ }), /* @__PURE__ */ jsx("button", {
1966
+ className: clsx("wg_navigation__drawerClose"),
1967
+ type: "button",
1968
+ "aria-label": copy_nav_close,
1969
+ "data-navigation-close": "",
1970
+ onClick: handleCloseButtonClick,
1971
+ children: /* @__PURE__ */ jsx("svg", {
1972
+ className: clsx("wg_navigation__menuIcon"),
1973
+ viewBox: "0 0 24 24",
1974
+ "aria-hidden": "true",
1975
+ focusable: "false",
1976
+ children: /* @__PURE__ */ jsx("path", { d: "m6 6 12 12M18 6 6 18" })
1977
+ })
1978
+ })]
1979
+ }), /* @__PURE__ */ jsx("div", {
1980
+ className: clsx("wg_navigation__drawerContent"),
1981
+ "data-navigation-content": "",
1982
+ children: layout === "drawer" && /* @__PURE__ */ jsx("div", {
1983
+ ref: props.stores === void 0 ? void 0 : drawerNavigationRef,
1984
+ className: clsx("wg_navigation__navigation"),
1985
+ "data-navigation": "",
1986
+ children: /* @__PURE__ */ jsx(WgNavigationScroll, {
1987
+ portRef: drawerDocumentsRef,
1988
+ kind: "documents",
1989
+ ready: props.stores !== void 0,
1990
+ onScroll: saveScroll,
1991
+ onClick: handleNavigationClick,
1992
+ children: /* @__PURE__ */ jsx(WgNavigationNav, { groups: props.data.groups })
1993
+ })
1994
+ })
1995
+ })]
1996
+ })
1997
+ ] });
1998
+ };
1999
+ //#endregion
2000
+ //#region src/component/widget/prose/_constant/heading-highlight.ts
2001
+ /**
2002
+ * 앵커 이동 후 제목 강조 유지 시간. 이후 CSS로 서서히 해제
2003
+ */
2004
+ var heading_highlight_hold_ms = 3e3;
2005
+ //#endregion
2006
+ //#region src/component/widget/shell-controls/wg-shell-controls.tsx
2007
+ var WgShellControls = (props) => {
2008
+ const headingRef = useRef(null);
2009
+ const timerRef = useRef(0);
2010
+ const arrivalRef = useRef(null);
2011
+ const lifetimeRef = useRef(null);
2012
+ const userScrollRef = useRef(null);
2013
+ /**
2014
+ * 다음 앵커 이동과 해제에서 이전 강조, 도착 감시, 타이머를 함께 정리
2015
+ */
2016
+ const clearHeadingHighlight = () => {
2017
+ clearTimeout(timerRef.current);
2018
+ timerRef.current = 0;
2019
+ arrivalRef.current?.abort();
2020
+ arrivalRef.current = null;
2021
+ headingRef.current?.classList.remove("wg_prose__headingText--highlighted");
2022
+ headingRef.current = null;
2023
+ };
2024
+ /**
2025
+ * 본문 스크롤 도착 후 3초 확보. 사이드바 자체의 scrollend는 제외
2026
+ */
2027
+ const handleHighlightArrival = (event) => {
2028
+ if (event.target !== document) return;
2029
+ clearTimeout(timerRef.current);
2030
+ arrivalRef.current?.abort();
2031
+ arrivalRef.current = null;
2032
+ timerRef.current = window.setTimeout(clearHeadingHighlight, heading_highlight_hold_ms);
2033
+ };
2034
+ /**
2035
+ * 도착 제목만 강조. 같은 hash 재클릭도 유지 시간 재시작
2036
+ */
2037
+ const highlightHeading = (target) => {
2038
+ clearHeadingHighlight();
2039
+ headingRef.current = target === null ? null : target.querySelector(".wg_prose__headingText");
2040
+ if (headingRef.current === null) return;
2041
+ headingRef.current.classList.add("wg_prose__headingText--highlighted");
2042
+ timerRef.current = window.setTimeout(clearHeadingHighlight, heading_highlight_hold_ms);
2043
+ arrivalRef.current = new AbortController();
2044
+ document.addEventListener("scrollend", handleHighlightArrival, { signal: arrivalRef.current.signal });
2045
+ };
2046
+ /**
2047
+ * 탐색 DOM이 다시 만들어져도 같은 hash의 접힌 Details 공개를 유지
2048
+ */
2049
+ const handleHashClick = (event) => {
2050
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
2051
+ const link = event.target instanceof Element ? event.target.closest("a[href]") : null;
2052
+ if (link instanceof HTMLAnchorElement && (link.target === "" || link.target === "_self") && !link.hasAttribute("download") && link.origin === location.origin && link.pathname === location.pathname && link.search === location.search && link.hash) {
2053
+ const target = revealHashTarget(link.hash);
2054
+ if (link.hash === location.hash) highlightHeading(target);
2055
+ }
2056
+ };
2057
+ /**
2058
+ * hash 변경 시 접힌 대상을 공개하고 네이티브 스크롤, 강조 적용
2059
+ */
2060
+ const handleHashChange = () => {
2061
+ const target = revealHashTarget(location.hash);
2062
+ target?.scrollIntoView();
2063
+ highlightHeading(target);
2064
+ };
2065
+ const handleUserScroll = () => {
2066
+ userScrollRef.current?.abort();
2067
+ };
2068
+ /**
2069
+ * 초기 폰트 로드 후 앵커 보정. 해제된 인스턴스와 사용자 이동에 개입하지 않음
2070
+ */
2071
+ const handlePageLoad = async () => {
2072
+ const lifetime = lifetimeRef.current;
2073
+ const hash = location.hash;
2074
+ const target = revealHashTarget(hash);
2075
+ await document.fonts.ready;
2076
+ if (lifetime === null || lifetime.signal.aborted) return;
2077
+ if (target !== null && !userScrollRef.current?.signal.aborted && hash === location.hash) {
2078
+ target.scrollIntoView({ behavior: "instant" });
2079
+ highlightHeading(target);
2080
+ }
2081
+ document.documentElement.style.removeProperty("scroll-behavior");
2082
+ };
2083
+ /**
2084
+ * 셸은 본문 앞에서 마운트. 본문 파싱 이후에만 초기 접힘 대상 조회
2085
+ */
2086
+ const handleBodyReady = () => {
2087
+ revealHashTarget(location.hash);
2088
+ if (document.readyState === "complete") handlePageLoad();
2089
+ };
2090
+ /**
2091
+ * 본문 이벤트의 설치, 정리는 셸이 소유. SSR에서는 DOM에 접근하지 않음
2092
+ */
2093
+ useLayoutEffect(() => {
2094
+ const lifetime = new AbortController();
2095
+ lifetimeRef.current = lifetime;
2096
+ userScrollRef.current = new AbortController();
2097
+ document.documentElement.style.scrollBehavior = "auto";
2098
+ document.addEventListener("click", handleHashClick, { signal: lifetime.signal });
2099
+ addEventListener("hashchange", handleHashChange, { signal: lifetime.signal });
2100
+ addEventListener("load", handlePageLoad, {
2101
+ once: true,
2102
+ signal: lifetime.signal
2103
+ });
2104
+ for (const type of [
2105
+ "wheel",
2106
+ "touchmove",
2107
+ "keydown",
2108
+ "mousedown"
2109
+ ]) addEventListener(type, handleUserScroll, {
2110
+ once: true,
2111
+ passive: true,
2112
+ signal: lifetime.signal
2113
+ });
2114
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", handleBodyReady, {
2115
+ once: true,
2116
+ signal: lifetime.signal
2117
+ });
2118
+ else handleBodyReady();
2119
+ return () => {
2120
+ lifetime.abort();
2121
+ lifetimeRef.current = null;
2122
+ userScrollRef.current?.abort();
2123
+ userScrollRef.current = null;
2124
+ clearHeadingHighlight();
2125
+ document.documentElement.style.removeProperty("scroll-behavior");
2126
+ };
2127
+ }, []);
2128
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2129
+ /* @__PURE__ */ jsx("div", {
2130
+ className: clsx("wg_shellControls__navigation"),
2131
+ children: /* @__PURE__ */ jsx(WgNavigation, {
2132
+ data: props.data,
2133
+ stores: props.stores
2134
+ })
2135
+ }),
2136
+ props.data.cursorFace && /* @__PURE__ */ jsx(WgCursorFace, { ready: props.stores !== void 0 }),
2137
+ /* @__PURE__ */ jsx(WgMermaid, { onRendered: handlePageLoad })
2138
+ ] });
2139
+ };
2140
+ //#endregion
2141
+ //#region src/component/widget/shell/wg-shell.tsx
2142
+ var WgShell = (props) => {
2143
+ const navigationData = toNavigationData({
2144
+ site: props.site,
2145
+ docs: props.docs,
2146
+ current: props.current,
2147
+ outline: props.outline
2148
+ });
2149
+ const head = createHead({ disableDefaults: true });
2150
+ head.push({
2151
+ title: props.title === void 0 ? props.site.title : `${props.title} · ${props.site.title}`,
2152
+ meta: [
2153
+ { charset: "utf-8" },
2154
+ {
2155
+ name: "viewport",
2156
+ content: "width=device-width, initial-scale=1"
2157
+ },
2158
+ {
2159
+ name: "description",
2160
+ content: props.site.description === void 0 ? void 0 : escapeHtml(props.site.description)
2161
+ }
2162
+ ],
2163
+ style: [{ textContent: props.assets.fontCss }],
2164
+ link: [
2165
+ {
2166
+ rel: "icon",
2167
+ href: asset_favicon_path,
2168
+ type: "image/svg+xml"
2169
+ },
2170
+ ...props.assets.preload.map((href) => ({
2171
+ rel: "preload",
2172
+ href,
2173
+ as: "font",
2174
+ type: "font/woff2",
2175
+ crossorigin: ""
2176
+ })),
2177
+ ...props.assets.stylePaths.map((href) => ({
2178
+ rel: "stylesheet",
2179
+ href
2180
+ }))
2181
+ ]
2182
+ });
2183
+ return /* @__PURE__ */ jsxs("html", {
2184
+ lang: "ko",
2185
+ children: [/* @__PURE__ */ jsx("head", { children: raw(head.render().headTags) }), /* @__PURE__ */ jsxs("body", {
2186
+ className: clsx("wg_shell__root"),
2187
+ children: [
2188
+ /* @__PURE__ */ jsx("div", {
2189
+ className: clsx("wg_shell__browserRoot"),
2190
+ "data-shell-browser-root": "",
2191
+ children: /* @__PURE__ */ jsx(WgShellControls, { data: navigationData })
2192
+ }),
2193
+ /* @__PURE__ */ jsx("script", {
2194
+ type: "application/json",
2195
+ id: "fh-navigation-data",
2196
+ dangerouslySetInnerHTML: { __html: toNavigationJson(navigationData) }
2197
+ }),
2198
+ props.assets.browser.kind === "inline" && /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: props.assets.browser.code } }),
2199
+ props.assets.browser.kind === "module" && /* @__PURE__ */ jsx("script", {
2200
+ type: "module",
2201
+ src: props.assets.browser.path
2202
+ }),
2203
+ /* @__PURE__ */ jsx("main", {
2204
+ className: clsx("wg_shell__main"),
2205
+ children: props.children
2206
+ })
2207
+ ]
2208
+ })]
2209
+ });
2210
+ };
2211
+ //#endregion
2212
+ //#region src/page/doc/pg-doc.tsx
2213
+ var PgDoc = (props) => {
2214
+ return /* @__PURE__ */ jsx(WgShell, {
2215
+ site: props.site,
2216
+ docs: props.docs,
2217
+ assets: props.assets,
2218
+ title: props.doc.data.name,
2219
+ current: props.doc.id,
2220
+ outline: props.doc.outline,
2221
+ children: /* @__PURE__ */ jsx(WgProse, { html: props.doc.html })
2222
+ });
2223
+ };
2224
+ //#endregion
2225
+ //#region src/page/home/pg-home.tsx
2226
+ var PgHome = (props) => {
2227
+ return /* @__PURE__ */ jsx(WgShell, {
2228
+ site: props.site,
2229
+ docs: props.docs,
2230
+ assets: props.assets,
2231
+ outline: props.home?.outline,
2232
+ children: props.home === void 0 ? /* @__PURE__ */ jsx(WgProse, { head: {
2233
+ title: props.site.title,
2234
+ lead: copy_home_missing
2235
+ } }) : /* @__PURE__ */ jsx(WgProse, { html: props.home.html })
2236
+ });
2237
+ };
2238
+ //#endregion
2239
+ //#region src/app/_function/to-page-app.tsx
2240
+ /**
2241
+ * Hono JSX 페이지에 HTML 문서 선언 추가. 서버와 정적 출력에서 같은 렌더러 사용
2242
+ */
2243
+ var toHtml = (page) => {
2244
+ return html`<!DOCTYPE html>${page}`;
2245
+ };
2246
+ /**
2247
+ * 첫 화면 /. 문서 /:slug{.+}/. 하위 폴더 id 지원
2248
+ *
2249
+ * - 자원 wildcard와 정규식 매개변수 혼합 시 TrieRouter fallback으로 중첩 경로 불일치
2250
+ * - 자원 응답은 toHttpApp의 별도 앱 소유
2251
+ */
2252
+ var toPageApp = (options) => {
2253
+ const app = new Hono();
2254
+ app.get("/", (c) => c.html(toHtml(/* @__PURE__ */ jsx(PgHome, {
2255
+ site: options.site,
2256
+ docs: options.store.docs,
2257
+ assets: options.assets,
2258
+ home: options.store.home
2259
+ }))));
2260
+ app.get("/:slug{.+}/", ssgParams(() => options.store.docs.map((doc) => ({ slug: doc.id }))), (c) => {
2261
+ const doc = options.store.docs.find((entry) => entry.id === c.req.param("slug"));
2262
+ return doc === void 0 ? c.notFound() : c.html(toHtml(/* @__PURE__ */ jsx(PgDoc, {
2263
+ site: options.site,
2264
+ docs: options.store.docs,
2265
+ assets: options.assets,
2266
+ doc
2267
+ })));
2268
+ });
2269
+ return app;
2270
+ };
2271
+ //#endregion
2272
+ //#region src/util/font/to-font-css.ts
2273
+ /**
2274
+ * 내장 @font-face의 표시 정책과 내용 지문 URL 적용. 원본 파일 수집
2275
+ *
2276
+ * - 늦은 글꼴 교체와 문서 이동마다 같은 글꼴을 다시 받는 현상 방지
2277
+ */
2278
+ var toFontCss = (options) => {
2279
+ const files = /* @__PURE__ */ new Map();
2280
+ return {
2281
+ css: [...readFileSync(options.css, "utf8").matchAll(/@font-face\s*\{[^}]*\}/g)].map((match) => match[0].replace(/font-display:\s*[^;]+;/g, `font-display: ${font_display_strategy};`).replace(/url\(([^)]+)\)/g, (_match, relativeUrl) => {
2282
+ const file = join(dirname(options.css), relativeUrl);
2283
+ const extension = extname(file);
2284
+ const hash = createHash("sha256").update(readFileSync(file)).digest("hex").slice(0, 12);
2285
+ const url = `${options.fontDir}/${basename(file, extension)}.${hash}${extension}`;
2286
+ files.set(url, file);
2287
+ return `url(${url})`;
2288
+ })).join("\n"),
2289
+ files
2290
+ };
2291
+ };
2292
+ //#endregion
2293
+ //#region src/app/load-site.ts
2294
+ /**
2295
+ * 문서, 등록 자원과 Hono 앱의 일관된 스냅샷
2296
+ *
2297
+ * - 실패한 준비 결과는 반환하지 않으며 이전 스냅샷 변경 없음
2298
+ * - 파일 조회와 Markdown 변환은 준비 시에만 실행, 페이지 요청에서는 재사용
2299
+ */
2300
+ var loadSite = async (params) => {
2301
+ const mono = toFontCss({
2302
+ css: join(params.kitRoot, font_mono_css),
2303
+ fontDir: asset_font_dir
2304
+ });
2305
+ const body = toFontCss({
2306
+ css: join(params.kitRoot, font_body_css),
2307
+ fontDir: asset_font_dir
2308
+ });
2309
+ const brand = toFontCss({
2310
+ css: join(params.kitRoot, font_brand_css),
2311
+ fontDir: asset_font_dir
2312
+ });
2313
+ const stylePaths = params.stylePaths ?? [toAssetPath({
2314
+ path: asset_style_path,
2315
+ file: join(params.kitRoot, "dist/cli.css")
2316
+ })];
2317
+ const files = new Map([
2318
+ [asset_favicon_path, join(params.kitRoot, "src/asset/favicon.svg")],
2319
+ ...mono.files,
2320
+ ...body.files,
2321
+ ...brand.files
2322
+ ]);
2323
+ if (params.stylePaths === void 0) files.set(stylePaths[0], join(params.kitRoot, "dist/cli.css"));
2324
+ const processor = createProcessor({
2325
+ site: params.site,
2326
+ root: params.root,
2327
+ files,
2328
+ mermaid: loadMermaidAssets(params.kitRoot)
2329
+ });
2330
+ const [docs, home] = await Promise.all([readDocs({
2331
+ root: params.root,
2332
+ processor,
2333
+ reserved: params.site.url === void 0 ? void 0 : crawling_reserved_roots
2334
+ }), readHome({
2335
+ root: params.root,
2336
+ processor
2337
+ })]);
2338
+ const store = {
2339
+ docs,
2340
+ home
2341
+ };
2342
+ const pages = toPageApp({
2343
+ site: params.site,
2344
+ store,
2345
+ assets: {
2346
+ stylePaths,
2347
+ browser: params.browser === void 0 ? {
2348
+ kind: "inline",
2349
+ code: readFileSync(join(params.kitRoot, "dist/browser.js"), "utf8")
2350
+ } : params.browser,
2351
+ fontCss: [
2352
+ mono.css,
2353
+ body.css,
2354
+ brand.css
2355
+ ].join("\n"),
2356
+ preload: [
2357
+ ...brand.files.keys(),
2358
+ ...mono.files.keys(),
2359
+ ...body.files.keys()
2360
+ ]
2361
+ }
2362
+ });
2363
+ const crawling = params.site.url === void 0 ? void 0 : toCrawlingApp({
2364
+ url: params.site.url,
2365
+ store
2366
+ });
2367
+ return {
2368
+ pages,
2369
+ crawling,
2370
+ files,
2371
+ app: toHttpApp({
2372
+ pages,
2373
+ crawling,
2374
+ files
2375
+ })
2376
+ };
2377
+ };
2378
+ //#endregion
2379
+ export { font_cache_control as n, asset_font_dir as r, loadSite as t };