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.
- package/CHANGELOG.md +39 -0
- package/README.md +16 -9
- package/dist/browser.js +14 -16
- package/dist/cli.css +2 -968
- package/dist/cli.js +92 -2318
- package/dist/config.js +1 -0
- package/dist/constant/copy.d.ts +100 -0
- package/dist/constant/site.d.ts +20 -0
- package/dist/constant/status.d.ts +19 -0
- package/dist/copy-C1uzvEnr.js +103 -0
- package/dist/group-path-XAThmzvC.js +10 -0
- package/dist/load-site-B-u5-jv0.js +2343 -0
- package/dist/logger-Dv1i9xSV.js +9 -0
- package/dist/mermaid/THIRD-PARTY-NOTICES.txt +3134 -0
- package/dist/mermaid/chunk--TN-qHgG.js +2 -0
- package/dist/mermaid/chunk-0kM1Lv4D.js +2 -0
- package/dist/mermaid/chunk-3FHei3pH.js +2001 -0
- package/dist/mermaid/chunk-3_2p2GqN.js +2 -0
- package/dist/mermaid/chunk-6HLei9i0.js +84 -0
- package/dist/mermaid/chunk-7Sz3kAwl.js +2214 -0
- package/dist/mermaid/chunk-7Tarqbpa.js +18047 -0
- package/dist/mermaid/chunk-9kkt8lzL.js +10 -0
- package/dist/mermaid/chunk-B--pNBQF.js +884 -0
- package/dist/mermaid/chunk-B4F-pysn.js +4241 -0
- package/dist/mermaid/chunk-B58sEIY_.js +14 -0
- package/dist/mermaid/chunk-B9_UWzSH.js +1517 -0
- package/dist/mermaid/chunk-BB8uZHwY.js +75 -0
- package/dist/mermaid/chunk-BFfk0LEc.js +717 -0
- package/dist/mermaid/chunk-BKri0qr7.js +89 -0
- package/dist/mermaid/chunk-BL404pH5.js +26 -0
- package/dist/mermaid/chunk-BLeiQSWo.js +354 -0
- package/dist/mermaid/chunk-BOmH_UFd.js +2 -0
- package/dist/mermaid/chunk-BSej9Nhy.js +1361 -0
- package/dist/mermaid/chunk-BYzrzvCI.js +12 -0
- package/dist/mermaid/chunk-BariMtWr.js +3020 -0
- package/dist/mermaid/chunk-BcrBoMwH.js +2352 -0
- package/dist/mermaid/chunk-Bok8z-Yo.js +67 -0
- package/dist/mermaid/chunk-BpTHLKQM.js +1970 -0
- package/dist/mermaid/chunk-BqjuxRsC.js +733 -0
- package/dist/mermaid/chunk-BuvPd-s5.js +1073 -0
- package/dist/mermaid/chunk-BvVehae3.js +5069 -0
- package/dist/mermaid/chunk-C-JABYpd.js +3075 -0
- package/dist/mermaid/chunk-C9XlTm4w.js +2 -0
- package/dist/mermaid/chunk-CApNbSU9.js +6 -0
- package/dist/mermaid/chunk-CCM2AH-C.js +9 -0
- package/dist/mermaid/chunk-CD1E0F_N.js +2 -0
- package/dist/mermaid/chunk-CDWvKG7D.js +2 -0
- package/dist/mermaid/chunk-CEVPpC1F.js +34 -0
- package/dist/mermaid/chunk-CLDr12lf.js +47504 -0
- package/dist/mermaid/chunk-CLVYqOBI.js +61 -0
- package/dist/mermaid/chunk-CLeOkFb5.js +15 -0
- package/dist/mermaid/chunk-CLgRAtOz.js +37 -0
- package/dist/mermaid/chunk-CMqFNUBl.js +134 -0
- package/dist/mermaid/chunk-CTPaBJXB.js +2 -0
- package/dist/mermaid/chunk-CVi6IGBL.js +2 -0
- package/dist/mermaid/chunk-CWV_EN-4.js +4745 -0
- package/dist/mermaid/chunk-CZ0dAs3g.js +2 -0
- package/dist/mermaid/chunk-C_YGy4Fn.js +236 -0
- package/dist/mermaid/chunk-CcVH22Yt.js +100 -0
- package/dist/mermaid/chunk-CgG6CD9_.js +7175 -0
- package/dist/mermaid/chunk-CgOP6IDB.js +3397 -0
- package/dist/mermaid/chunk-Cgg2UTSf.js +13 -0
- package/dist/mermaid/chunk-ChJfJJ1A.js +214 -0
- package/dist/mermaid/chunk-Ck5XRA46.js +447 -0
- package/dist/mermaid/chunk-Ckuw16cl.js +203 -0
- package/dist/mermaid/chunk-CpAbRCbF.js +1880 -0
- package/dist/mermaid/chunk-CuYekbSv.js +2 -0
- package/dist/mermaid/chunk-CwS4wZ5a.js +2 -0
- package/dist/mermaid/chunk-D-bcrBO3.js +82 -0
- package/dist/mermaid/chunk-D3IEAkCW.js +76 -0
- package/dist/mermaid/chunk-D8XfBJ_K.js +645 -0
- package/dist/mermaid/chunk-D9Ddd9y4.js +2228 -0
- package/dist/mermaid/chunk-DBh1mIbK.js +12 -0
- package/dist/mermaid/chunk-DD6Df6Ic.js +5 -0
- package/dist/mermaid/chunk-DGVDaXjx.js +8 -0
- package/dist/mermaid/chunk-DK9KqIXj.js +632 -0
- package/dist/mermaid/chunk-DORnIAsy.js +4487 -0
- package/dist/mermaid/chunk-DU69_VcF.js +363 -0
- package/dist/mermaid/chunk-DVNvSFoz.js +2144 -0
- package/dist/mermaid/chunk-DZe-4kde.js +20 -0
- package/dist/mermaid/chunk-D_a06Rz4.js +287 -0
- package/dist/mermaid/chunk-DfyFJJi2.js +972 -0
- package/dist/mermaid/chunk-DhKYPp1Z.js +2 -0
- package/dist/mermaid/chunk-DjFqgNTG.js +94 -0
- package/dist/mermaid/chunk-DmbDDrr-.js +65 -0
- package/dist/mermaid/chunk-DpuxlyL_.js +696 -0
- package/dist/mermaid/chunk-DqGhppWe.js +2 -0
- package/dist/mermaid/chunk-DqPZxHXa.js +5 -0
- package/dist/mermaid/chunk-Dt_1Jyif.js +293 -0
- package/dist/mermaid/chunk-DyQBE_NW.js +1935 -0
- package/dist/mermaid/chunk-JckyC4mW.js +1009 -0
- package/dist/mermaid/chunk-K7Myuj2-.js +1334 -0
- package/dist/mermaid/chunk-KuPYb48V.js +275 -0
- package/dist/mermaid/chunk-MFVRUoO0.js +4020 -0
- package/dist/mermaid/chunk-MbInCS_6.js +28 -0
- package/dist/mermaid/chunk-MexOLyKV.js +355 -0
- package/dist/mermaid/chunk-NnbDtJzg.js +21856 -0
- package/dist/mermaid/chunk-SGpOYZbI.js +2139 -0
- package/dist/mermaid/chunk-TbxLDlcU.js +198 -0
- package/dist/mermaid/chunk-TftHqGLg.js +490 -0
- package/dist/mermaid/chunk-UjQ8vA_Z.js +4138 -0
- package/dist/mermaid/chunk-YgmVTKqc.js +16 -0
- package/dist/mermaid/chunk-_a9EZE1v.js +1798 -0
- package/dist/mermaid/chunk-dTb8jqeP.js +2 -0
- package/dist/mermaid/chunk-fFdMfF8S.js +2240 -0
- package/dist/mermaid/chunk-feYbMzBj.js +1267 -0
- package/dist/mermaid/chunk-g3Pn8DkW.js +46 -0
- package/dist/mermaid/chunk-hM77LvCb.js +19832 -0
- package/dist/mermaid/chunk-hUDb-eAW.js +527 -0
- package/dist/mermaid/chunk-tpABtZ-3.js +4252 -0
- package/dist/mermaid/chunk-xIbz3oFK.js +59 -0
- package/dist/mermaid/chunk-yBz-vst0.js +976 -0
- package/dist/mermaid/mermaid-BhtR3wyB.js +1145 -0
- package/dist/run-site-command-cqCjpIlc.js +129 -0
- package/dist/site-config-bllORGpm.js +90 -0
- package/dist/site.js +1 -0
- package/dist/start-dev-server-x7Ja5uwv.js +184 -0
- package/dist/to-change-queue-DbEnZGNK.js +34 -0
- package/dist/to-error-message-pF6_7wGK.js +16 -0
- package/dist/type/group-path.d.ts +7 -0
- package/dist/type/site-config.d.ts +57 -0
- package/package.json +34 -17
- package/src/component/widget/cursor-face/_constant/cursor-face.ts +0 -14
- package/src/component/widget/cursor-face/wg-cursor-face.css +0 -15
- package/src/component/widget/cursor-face/wg-cursor-face.tsx +0 -153
- package/src/component/widget/navigation/_constant/locale.ts +0 -9
- package/src/component/widget/navigation/_constant/navigation-data-schema.ts +0 -33
- package/src/component/widget/navigation/_constant/navigation.ts +0 -9
- package/src/component/widget/navigation/_constant/reading-line.ts +0 -4
- package/src/component/widget/navigation/_function/restore-navigation-scroll.ts +0 -59
- package/src/component/widget/navigation/_function/to-doc-groups/_to-doc-branches.ts +0 -15
- package/src/component/widget/navigation/_function/to-doc-groups/_to-doc-group-branches.ts +0 -70
- package/src/component/widget/navigation/_function/to-doc-groups/to-doc-groups.ts +0 -15
- package/src/component/widget/navigation/_function/to-navigation-data/_to-navigation-document.ts +0 -13
- package/src/component/widget/navigation/_function/to-navigation-data/_to-navigation-group.ts +0 -13
- package/src/component/widget/navigation/_function/to-navigation-data/to-navigation-data.ts +0 -18
- package/src/component/widget/navigation/_function/to-navigation-json.ts +0 -8
- package/src/component/widget/navigation/_function/to-toc-groups.ts +0 -38
- package/src/component/widget/navigation/_type/doc-branch.ts +0 -11
- package/src/component/widget/navigation/_type/doc-group.ts +0 -24
- package/src/component/widget/navigation/_type/navigation-data.ts +0 -75
- package/src/component/widget/navigation/_type/navigation-focus-snapshot.ts +0 -17
- package/src/component/widget/navigation/_type/toc-group.ts +0 -39
- package/src/component/widget/navigation/_wg-navigation-nav-list.css +0 -48
- package/src/component/widget/navigation/_wg-navigation-nav-list.tsx +0 -61
- package/src/component/widget/navigation/_wg-navigation-nav.css +0 -21
- package/src/component/widget/navigation/_wg-navigation-nav.tsx +0 -33
- package/src/component/widget/navigation/_wg-navigation-scroll.tsx +0 -117
- package/src/component/widget/navigation/_wg-navigation-toc.css +0 -91
- package/src/component/widget/navigation/_wg-navigation-toc.tsx +0 -114
- package/src/component/widget/navigation/wg-navigation.css +0 -282
- package/src/component/widget/navigation/wg-navigation.tsx +0 -420
- package/src/component/widget/prose/_constant/heading-highlight.ts +0 -4
- package/src/component/widget/prose/_constant/section.ts +0 -7
- package/src/component/widget/prose/_constant/status.ts +0 -9
- package/src/component/widget/prose/_function/has-block-content.ts +0 -30
- package/src/component/widget/prose/_function/rehype-head.ts +0 -31
- package/src/component/widget/prose/_function/rehype-sections/_to-section-headings.ts +0 -20
- package/src/component/widget/prose/_function/rehype-sections/rehype-sections.ts +0 -21
- package/src/component/widget/prose/_function/rehype-tables.ts +0 -22
- package/src/component/widget/prose/_function/remark-blocks.ts +0 -112
- package/src/component/widget/prose/_function/remark-flow.ts +0 -26
- package/src/component/widget/prose/_function/remark-links.ts +0 -33
- package/src/component/widget/prose/_function/remark-parts.ts +0 -26
- package/src/component/widget/prose/_function/remark-status.ts +0 -59
- package/src/component/widget/prose/_function/remark-swatch.ts +0 -16
- package/src/component/widget/prose/_function/remark-unknown-directives.ts +0 -25
- package/src/component/widget/prose/wg-prose.css +0 -484
- package/src/component/widget/prose/wg-prose.tsx +0 -43
- package/src/component/widget/shell/wg-shell.css +0 -63
- package/src/component/widget/shell/wg-shell.tsx +0 -88
- package/src/component/widget/shell-controls/wg-shell-controls.css +0 -4
- package/src/component/widget/shell-controls/wg-shell-controls.tsx +0 -170
- package/src/constant/asset.ts +0 -63
- package/src/constant/copy.ts +0 -133
- package/src/constant/doc.ts +0 -13
- package/src/constant/font.ts +0 -34
- package/src/constant/site.ts +0 -24
- package/src/constant/status.ts +0 -15
- package/src/entry/browser/browser.tsx +0 -22
- package/src/entry/cli/_constant/cli.ts +0 -19
- package/src/entry/cli/_constant/crawling.ts +0 -14
- package/src/entry/cli/_constant/snippet.ts +0 -21
- package/src/entry/cli/_function/create-app.tsx +0 -57
- package/src/entry/cli/_function/create-dev-app.ts +0 -87
- package/src/entry/cli/_function/create-processor/_rehype-assets.ts +0 -70
- package/src/entry/cli/_function/create-processor/_rehype-heading-ids.ts +0 -34
- package/src/entry/cli/_function/create-processor/_remark-report.ts +0 -15
- package/src/entry/cli/_function/create-processor/_remark-snippets.ts +0 -117
- package/src/entry/cli/_function/create-processor/_shiki-transformers.ts +0 -91
- package/src/entry/cli/_function/create-processor/create-processor.ts +0 -64
- package/src/entry/cli/_function/read-docs.ts +0 -128
- package/src/entry/cli/_function/read-home.ts +0 -32
- package/src/entry/cli/_function/to-crawling-app.ts +0 -42
- package/src/entry/cli/cli.test.ts +0 -804
- package/src/entry/cli/cli.ts +0 -206
- package/src/env.d.ts +0 -6
- package/src/page/doc/pg-doc.tsx +0 -35
- package/src/page/home/pg-home.tsx +0 -40
- package/src/store/navigation/create-navigation-stores.test.ts +0 -74
- package/src/store/navigation/create-navigation-stores.ts +0 -59
- package/src/store/navigation/navigation-state.ts +0 -36
- package/src/store/navigation/navigation-storage.ts +0 -9
- package/src/store/navigation/to-navigation-scroll-state.ts +0 -42
- package/src/style/base.css +0 -28
- package/src/style/token.css +0 -188
- package/src/type/doc-content.ts +0 -15
- package/src/type/doc-data.ts +0 -38
- package/src/type/doc-file-data.ts +0 -21
- package/src/type/doc-heading.ts +0 -17
- package/src/type/doc-outline.ts +0 -16
- package/src/type/doc-section.ts +0 -9
- package/src/type/doc.ts +0 -20
- package/src/type/group-path.ts +0 -7
- package/src/type/site-assets.ts +0 -25
- package/src/type/site-config.ts +0 -73
- package/src/type/vfile-data.d.ts +0 -10
- package/src/util/async/to-change-queue.test.ts +0 -58
- package/src/util/async/to-change-queue.ts +0 -43
- package/src/util/dom/find-hash-target.ts +0 -18
- package/src/util/dom/reveal-hash-target.ts +0 -15
- package/src/util/error/to-error-message.ts +0 -10
- package/src/util/file/to-asset-path.ts +0 -26
- package/src/util/font/to-font-css.test.ts +0 -41
- package/src/util/font/to-font-css.ts +0 -28
- 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("&", "&").replaceAll("<", "<").replaceAll(">", ">")}</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 };
|