for-humanity 0.12.0 → 0.13.1

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