for-humanity 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +1 -1
  3. package/README.md +4 -3
  4. package/dist/browser.js +9 -9
  5. package/dist/cli.css +25 -310
  6. package/dist/cli.js +177 -321
  7. package/package.json +4 -3
  8. package/src/asset/font/{jetbrains-mono/LICENSE → monoplex-kr/LICENSE.md} +61 -8
  9. package/src/asset/font/monoplex-kr/MonoplexKR-Regular.woff2 +0 -0
  10. package/src/asset/font/monoplex-kr/MonoplexKR-SemiBold.woff2 +0 -0
  11. package/src/asset/font/monoplex-kr/monoplex-kr.css +20 -0
  12. package/src/component/widget/navigation/_constant/navigation-data-schema.ts +0 -3
  13. package/src/component/widget/navigation/_function/to-navigation-data/_to-navigation-document.ts +2 -3
  14. package/src/component/widget/navigation/_function/to-navigation-data/_to-navigation-group.ts +3 -4
  15. package/src/component/widget/navigation/_function/to-navigation-data/to-navigation-data.ts +6 -14
  16. package/src/component/widget/navigation/_type/navigation-data.ts +0 -12
  17. package/src/component/widget/navigation/_type/navigation-focus-snapshot.ts +0 -4
  18. package/src/component/widget/navigation/_wg-navigation-nav-list.css +5 -136
  19. package/src/component/widget/navigation/_wg-navigation-nav-list.tsx +10 -55
  20. package/src/component/widget/navigation/_wg-navigation-nav.css +2 -8
  21. package/src/component/widget/navigation/_wg-navigation-nav.tsx +1 -11
  22. package/src/component/widget/navigation/_wg-navigation-toc.css +12 -207
  23. package/src/component/widget/navigation/_wg-navigation-toc.tsx +39 -80
  24. package/src/component/widget/navigation/wg-navigation.tsx +10 -69
  25. package/src/component/widget/prose/wg-prose.css +0 -3
  26. package/src/component/widget/shell/wg-shell.css +1 -1
  27. package/src/constant/copy.ts +5 -5
  28. package/src/constant/font.ts +2 -12
  29. package/src/entry/browser/browser.tsx +1 -1
  30. package/src/entry/cli/_constant/crawling.ts +14 -0
  31. package/src/entry/cli/_function/create-dev-app.ts +9 -1
  32. package/src/entry/cli/_function/read-docs.ts +20 -1
  33. package/src/entry/cli/_function/to-crawling-app.ts +42 -0
  34. package/src/entry/cli/cli.test.ts +112 -9
  35. package/src/entry/cli/cli.ts +27 -22
  36. package/src/store/navigation/create-navigation-stores.test.ts +10 -28
  37. package/src/store/navigation/create-navigation-stores.ts +4 -18
  38. package/src/store/navigation/navigation-state.ts +0 -10
  39. package/src/store/navigation/navigation-storage.ts +0 -5
  40. package/src/style/token.css +11 -14
  41. package/src/type/site-config.ts +10 -1
  42. package/src/util/font/to-font-css.test.ts +2 -2
  43. package/src/util/mermaid/render-flow.js +2 -2
  44. package/src/asset/font/jetbrains-mono/jetbrains-mono-latin-wght-normal.woff2 +0 -0
  45. package/src/asset/font/jetbrains-mono/jetbrains-mono.css +0 -11
  46. package/src/asset/font/pretendard/LICENSE.txt +0 -94
  47. package/src/asset/font/pretendard/pretendardvariable-dynamic-subset.css +0 -1008
  48. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.0.woff2 +0 -0
  49. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.1.woff2 +0 -0
  50. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.10.woff2 +0 -0
  51. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.11.woff2 +0 -0
  52. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.12.woff2 +0 -0
  53. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.13.woff2 +0 -0
  54. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.14.woff2 +0 -0
  55. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.15.woff2 +0 -0
  56. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.16.woff2 +0 -0
  57. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.17.woff2 +0 -0
  58. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.18.woff2 +0 -0
  59. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.19.woff2 +0 -0
  60. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.2.woff2 +0 -0
  61. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.20.woff2 +0 -0
  62. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.21.woff2 +0 -0
  63. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.22.woff2 +0 -0
  64. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.23.woff2 +0 -0
  65. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.24.woff2 +0 -0
  66. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.25.woff2 +0 -0
  67. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.26.woff2 +0 -0
  68. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.27.woff2 +0 -0
  69. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.28.woff2 +0 -0
  70. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.29.woff2 +0 -0
  71. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.3.woff2 +0 -0
  72. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.30.woff2 +0 -0
  73. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.31.woff2 +0 -0
  74. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.32.woff2 +0 -0
  75. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.33.woff2 +0 -0
  76. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.34.woff2 +0 -0
  77. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.35.woff2 +0 -0
  78. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.36.woff2 +0 -0
  79. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.37.woff2 +0 -0
  80. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.38.woff2 +0 -0
  81. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.39.woff2 +0 -0
  82. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.4.woff2 +0 -0
  83. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.40.woff2 +0 -0
  84. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.41.woff2 +0 -0
  85. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.42.woff2 +0 -0
  86. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.43.woff2 +0 -0
  87. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.44.woff2 +0 -0
  88. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.45.woff2 +0 -0
  89. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.46.woff2 +0 -0
  90. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.47.woff2 +0 -0
  91. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.48.woff2 +0 -0
  92. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.49.woff2 +0 -0
  93. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.5.woff2 +0 -0
  94. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.50.woff2 +0 -0
  95. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.51.woff2 +0 -0
  96. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.52.woff2 +0 -0
  97. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.53.woff2 +0 -0
  98. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.54.woff2 +0 -0
  99. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.55.woff2 +0 -0
  100. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.56.woff2 +0 -0
  101. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.57.woff2 +0 -0
  102. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.58.woff2 +0 -0
  103. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.59.woff2 +0 -0
  104. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.6.woff2 +0 -0
  105. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.60.woff2 +0 -0
  106. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.61.woff2 +0 -0
  107. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.62.woff2 +0 -0
  108. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.63.woff2 +0 -0
  109. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.64.woff2 +0 -0
  110. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.65.woff2 +0 -0
  111. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.66.woff2 +0 -0
  112. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.67.woff2 +0 -0
  113. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.68.woff2 +0 -0
  114. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.69.woff2 +0 -0
  115. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.7.woff2 +0 -0
  116. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.70.woff2 +0 -0
  117. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.71.woff2 +0 -0
  118. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.72.woff2 +0 -0
  119. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.73.woff2 +0 -0
  120. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.74.woff2 +0 -0
  121. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.75.woff2 +0 -0
  122. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.76.woff2 +0 -0
  123. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.77.woff2 +0 -0
  124. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.78.woff2 +0 -0
  125. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.79.woff2 +0 -0
  126. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.8.woff2 +0 -0
  127. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.80.woff2 +0 -0
  128. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.81.woff2 +0 -0
  129. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.82.woff2 +0 -0
  130. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.83.woff2 +0 -0
  131. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.84.woff2 +0 -0
  132. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.85.woff2 +0 -0
  133. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.86.woff2 +0 -0
  134. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.87.woff2 +0 -0
  135. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.88.woff2 +0 -0
  136. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.89.woff2 +0 -0
  137. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.9.woff2 +0 -0
  138. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.90.woff2 +0 -0
  139. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.91.woff2 +0 -0
  140. package/src/component/widget/navigation/_function/to-navigation-branch-id.ts +0 -6
  141. package/src/component/widget/navigation/_wg-navigation-branch-toggle.css +0 -28
  142. package/src/component/widget/navigation/_wg-navigation-branch-toggle.tsx +0 -48
  143. package/src/store/navigation/_constant/tree.ts +0 -6
  144. package/src/store/navigation/to-navigation-tree-state.ts +0 -19
  145. package/src/store/navigation/use-navigation-tree-store.ts +0 -12
@@ -4,25 +4,15 @@
4
4
  */
5
5
 
6
6
  /**
7
- * 본문 글꼴 CSS · 패키지 기준 경로 · dynamic subset 92개
7
+ * 본문·코드 글꼴 CSS · 패키지 기준 경로 · Regular·SemiBold 두 파일
8
8
  */
9
- export const font_sans_css = "src/asset/font/pretendard/pretendardvariable-dynamic-subset.css";
10
-
11
- /**
12
- * 코드 글꼴 CSS · 라틴 subset · 한글은 본문 글꼴
13
- */
14
- export const font_mono_css = "src/asset/font/jetbrains-mono/jetbrains-mono.css";
9
+ export const font_mono_css = "src/asset/font/monoplex-kr/monoplex-kr.css";
15
10
 
16
11
  /**
17
12
  * 사이트 이름 글꼴 CSS · Architects Daughter Regular · 라틴 subset
18
13
  */
19
14
  export const font_brand_css = "src/asset/font/architects-daughter/architects-daughter.css";
20
15
 
21
- /**
22
- * 탐색·라틴 문자에 필요한 본문 조각 · 92개 전체 선요청 방지
23
- */
24
- export const font_sans_preload_file = "PretendardVariable.subset.91.woff2";
25
-
26
16
  /**
27
17
  * 늦게 받은 글꼴로 이미 보이는 대체 글꼴을 교체하지 않는 표시 정책
28
18
  */
@@ -13,7 +13,7 @@ if (root !== null && payload !== null) {
13
13
  try {
14
14
  const value: unknown = JSON.parse(payload.textContent);
15
15
  const data = navigationDataSchema.parse(value);
16
- const stores = createNavigationStores({local: () => localStorage, session: () => sessionStorage});
16
+ const stores = createNavigationStores({session: () => sessionStorage});
17
17
  createRoot(root).render(<WgShellControls data={data} stores={stores} reload={root.hasAttribute("data-shell-reload")} />);
18
18
  } catch (error) {
19
19
  // 초기화 오류에서도 서버의 링크·본문과 네이티브 스크롤 유지
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 크롤러 안내 파일의 사이트 루트 경로
3
+ */
4
+ export const crawling_robots_path = "/robots.txt";
5
+
6
+ /**
7
+ * 공개 페이지 목록의 사이트 루트 경로
8
+ */
9
+ export const crawling_sitemap_path = "/sitemap.xml";
10
+
11
+ /**
12
+ * 공개 크롤러 파일과 정적 출력이 충돌하는 문서의 첫 경로 조각
13
+ */
14
+ export const crawling_reserved_roots = [crawling_robots_path.slice(1), crawling_sitemap_path.slice(1)];
@@ -5,6 +5,7 @@ import {type Context, Hono} from "hono";
5
5
  import {streamSSE} from "hono/streaming";
6
6
  import {asset_content_type_default, asset_content_types, asset_dir, asset_favicon_path, asset_reload_path} from "@/constant/asset";
7
7
  import {font_cache_control} from "@/constant/font";
8
+ import {crawling_robots_path, crawling_sitemap_path} from "@/entry/cli/_constant/crawling";
8
9
 
9
10
  /**
10
11
  * 개발 서버 입력 계약
@@ -14,6 +15,10 @@ export interface DevOptions {
14
15
  * 정적 자원 외 요청을 처리할 페이지 앱
15
16
  */
16
17
  pages: Hono;
18
+ /**
19
+ * 공개 URL을 설정한 프로젝트의 크롤러 파일 앱
20
+ */
21
+ crawling?: Hono;
17
22
  /**
18
23
  * 자원 URL → 파일 경로 · 정적 빌드와 동일 목록
19
24
  */
@@ -25,7 +30,7 @@ export interface DevOptions {
25
30
  }
26
31
 
27
32
  /**
28
- * 개발 전용 자원·SSE 라우트 · 나머지 요청은 페이지 앱에 위임
33
+ * 개발 전용 자원·SSE와 선택적 크롤러 응답 · 나머지 요청은 페이지 앱에 위임
29
34
  * 정적 빌드의 페이지 라우트와 분리
30
35
  */
31
36
  export const createDevApp = (options: DevOptions) => {
@@ -73,6 +78,9 @@ export const createDevApp = (options: DevOptions) => {
73
78
  );
74
79
  app.get(`/${asset_dir}/*`, handleAsset);
75
80
  app.get(asset_favicon_path, handleAsset);
81
+ if (options.crawling !== undefined) {
82
+ app.on("GET", [crawling_robots_path, crawling_sitemap_path], (c) => options.crawling?.fetch(c.req.raw, c.env) ?? c.notFound());
83
+ }
76
84
  app.all("/*", (c) => options.pages.fetch(c.req.raw, c.env));
77
85
 
78
86
  return app;
@@ -16,12 +16,30 @@ import type {Doc} from "@/type/doc";
16
16
  import {docDataSchema} from "@/type/doc-data";
17
17
  import type {DocFileData} from "@/type/doc-file-data";
18
18
 
19
+ /**
20
+ * 문서 폴더를 읽고 명령별 출력 경로와 함께 검증하는 입력
21
+ */
22
+ export interface ReadDocsParams {
23
+ /**
24
+ * CLI에 넘긴 문서 폴더의 절대 경로
25
+ */
26
+ root: string;
27
+ /**
28
+ * 설정·자원 목록을 연결한 Markdown 처리기
29
+ */
30
+ processor: Processor;
31
+ /**
32
+ * 명령이 생성할 파일과 충돌하는 첫 경로 조각 · 생략 시 추가 예약 없음
33
+ */
34
+ reserved?: readonly string[];
35
+ }
36
+
19
37
  /**
20
38
  * Markdown 수집과 렌더링 · 루트 README는 홈, AGENTS·CLAUDE는 작성 지침으로 분리
21
39
  * 문서 목록에서 루트 안내 파일·node_modules·dist 제외
22
40
  * 머리말·문서 식별자 중복·플러그인 오류 시 실패
23
41
  */
24
- export const readDocs = async (options: {root: string; processor: Processor}): Promise<Doc[]> => {
42
+ export const readDocs = async (options: ReadDocsParams): Promise<Doc[]> => {
25
43
  const paths = (await readdir(options.root, {recursive: true})).filter(
26
44
  (path) => /\.mdx?$/i.test(path) && !/^(readme|agents|claude)\.md$/i.test(path) && !path.split(sep).includes("node_modules") && !path.startsWith(`dist${sep}`),
27
45
  );
@@ -39,6 +57,7 @@ export const readDocs = async (options: {root: string; processor: Processor}): P
39
57
  segments.some((segment) => segment === "" || segment === "." || segment === ".." || segment === "index.html") ||
40
58
  /[#?%*:\\]/.test(id) ||
41
59
  /[\p{Cc}]/u.test(id) ||
60
+ options.reserved?.includes(segments[0]) === true ||
42
61
  segments[0] === asset_dir ||
43
62
  segments[0] === asset_favicon_path.slice(1)
44
63
  ) {
@@ -0,0 +1,42 @@
1
+ import {Hono} from "hono";
2
+ import {html} from "hono/html";
3
+ import {crawling_robots_path, crawling_sitemap_path} from "@/entry/cli/_constant/crawling";
4
+ import type {AppOptions} from "@/entry/cli/_function/create-app";
5
+ import type {SiteConfig} from "@/type/site-config";
6
+
7
+ /**
8
+ * URL을 명시한 공개 프로젝트의 크롤러 응답 입력
9
+ */
10
+ export interface CrawlingParams {
11
+ /**
12
+ * 설정 검증에서 루트 origin으로 정규화한 공개 HTTP(S) URL
13
+ */
14
+ url: NonNullable<SiteConfig["url"]>;
15
+ /**
16
+ * dev 파일 변경 시 교체되는 현재 문서 목록 · 본문은 사이트맵에 포함하지 않음
17
+ */
18
+ store: AppOptions["store"];
19
+ }
20
+
21
+ /**
22
+ * dev와 정적 빌드에서 같은 크롤러 파일 생성 · 정책·수정 시각은 추정하지 않음
23
+ */
24
+ export const toCrawlingApp = (params: CrawlingParams) => {
25
+ const app = new Hono();
26
+
27
+ app.get(crawling_robots_path, (c) => c.text(`User-agent: *\nAllow: /\n\nSitemap: ${params.url}${crawling_sitemap_path}\n`));
28
+ app.get(
29
+ crawling_sitemap_path,
30
+ (_c) =>
31
+ new Response(
32
+ html`<?xml version="1.0" encoding="UTF-8"?>
33
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
34
+ <url><loc>${params.url}/</loc></url>
35
+ ${params.store.docs.map((doc) => html`<url><loc>${params.url}/${doc.id.split("/").map(encodeURIComponent).join("/")}/</loc></url>`)}
36
+ </urlset>`.toString(),
37
+ {headers: {"Content-Type": "application/xml; charset=utf-8"}},
38
+ ),
39
+ );
40
+
41
+ return app;
42
+ };
@@ -1,8 +1,10 @@
1
1
  import assert from "node:assert/strict";
2
- import {mkdir, mkdtemp, rm, symlink, writeFile} from "node:fs/promises";
2
+ import {EventEmitter} from "node:events";
3
+ import {mkdir, mkdtemp, readFile, rm, symlink, writeFile} from "node:fs/promises";
3
4
  import {tmpdir} from "node:os";
4
5
  import {join} from "node:path";
5
6
  import {test} from "node:test";
7
+ import {toSSG} from "hono/ssg";
6
8
  import {navigationDataSchema} from "@/component/widget/navigation/_constant/navigation-data-schema";
7
9
  import {toDocGroups} from "@/component/widget/navigation/_function/to-doc-groups/to-doc-groups";
8
10
  import {toNavigationData} from "@/component/widget/navigation/_function/to-navigation-data/to-navigation-data";
@@ -10,9 +12,11 @@ import {toNavigationJson} from "@/component/widget/navigation/_function/to-navig
10
12
  import {WgNavigation} from "@/component/widget/navigation/wg-navigation";
11
13
  import {asset_style_path} from "@/constant/asset";
12
14
  import {createApp} from "@/entry/cli/_function/create-app";
15
+ import {createDevApp} from "@/entry/cli/_function/create-dev-app";
13
16
  import {createProcessor} from "@/entry/cli/_function/create-processor/create-processor";
14
17
  import {readDocs} from "@/entry/cli/_function/read-docs";
15
18
  import {readHome} from "@/entry/cli/_function/read-home";
19
+ import {toCrawlingApp} from "@/entry/cli/_function/to-crawling-app";
16
20
  import {docDataSchema} from "@/type/doc-data";
17
21
  import {siteConfigSchema} from "@/type/site-config";
18
22
 
@@ -54,6 +58,107 @@ test("page resource URLs use the supplied content fingerprints instead of fixed
54
58
  assert.doesNotMatch(html, /(?:href|src)="\/_fh\/(?:style\.css|client\.js)"/);
55
59
  });
56
60
 
61
+ test("public site URL is opt-in and normalizes an HTTP(S) root URL", () => {
62
+ assert.deepEqual(siteConfigSchema.parse({url: "https://docs.example.org/"}), {...siteConfigSchema.parse({}), url: "https://docs.example.org"});
63
+ assert.deepEqual(siteConfigSchema.parse({url: "http://localhost:4321"}), {...siteConfigSchema.parse({}), url: "http://localhost:4321"});
64
+ assert.equal("url" in siteConfigSchema.parse({}), false);
65
+ });
66
+
67
+ test("public site URL rejects unsupported protocols, credentials and non-root URLs", () => {
68
+ for (const url of [
69
+ "",
70
+ "/",
71
+ "file:///docs",
72
+ "ftp://docs.example.org",
73
+ "https://user:password@docs.example.org",
74
+ "https://docs.example.org/docs/",
75
+ "https://docs.example.org/?q=guide",
76
+ "https://docs.example.org/#guide",
77
+ ]) {
78
+ assert.equal(siteConfigSchema.safeParse({url}).success, false, url);
79
+ }
80
+ });
81
+
82
+ test("crawler files are absent when the public site URL is omitted", async () => {
83
+ const site = siteConfigSchema.parse({});
84
+ const assets = {fontCss: "", preload: [], reload: false, browserScript: "", stylePath: asset_style_path};
85
+ const app = createDevApp({pages: createApp({site, store: {docs: []}, assets}), files: new Map(), reload: new EventEmitter()});
86
+
87
+ assert.equal((await app.request("/robots.txt")).status, 404);
88
+ assert.equal((await app.request("/sitemap.xml")).status, 404);
89
+ });
90
+
91
+ test("crawler files list home and encoded nested document URLs with the current document store", async (t) => {
92
+ const root = await mkdtemp(join(tmpdir(), "for-humanity-crawling-"));
93
+ t.after(() => rm(root, {recursive: true, force: true}));
94
+ await mkdir(join(root, "guide"));
95
+ await writeFile(join(root, "guide.md"), "---\nname: Start\nlabel: Start\ngroup: Guide\n---\n\nStart here.\n");
96
+ await writeFile(join(root, "guide/한국어 & tools.md"), "---\nname: Guide\nlabel: Guide\ngroup: Guide\n---\n\n## Read\n\nText.\n");
97
+ const site = siteConfigSchema.parse({url: "https://docs.example.org/"});
98
+ const docs = await readDocs({root, processor: createProcessor({site, root})});
99
+ const store = {docs};
100
+ const assets = {fontCss: "", preload: [], reload: false, browserScript: "", stylePath: asset_style_path};
101
+ assert.ok(site.url);
102
+ const app = createDevApp({pages: createApp({site, store, assets}), crawling: toCrawlingApp({url: site.url, store}), files: new Map(), reload: new EventEmitter()});
103
+ const robots = await app.request("/robots.txt");
104
+ const sitemap = await app.request("/sitemap.xml");
105
+
106
+ assert.equal((await app.request("/guide/")).status, 200, "crawler routes must preserve ordinary document routes");
107
+ assert.equal((await app.request(`/guide/${encodeURIComponent("한국어 & tools")}/`)).status, 200, "crawler routes must preserve nested document routes");
108
+ assert.equal(robots.status, 200);
109
+ assert.match(String(robots.headers.get("content-type")), /^text\/plain/);
110
+ assert.equal(await robots.text(), "User-agent: *\nAllow: /\n\nSitemap: https://docs.example.org/sitemap.xml\n");
111
+ assert.equal(sitemap.status, 200);
112
+ assert.match(String(sitemap.headers.get("content-type")), /^application\/xml/);
113
+ const xml = await sitemap.text();
114
+ assert.match(xml, /^<\?xml version="1\.0" encoding="UTF-8"\?>/);
115
+ assert.match(xml, /<urlset xmlns="http:\/\/www\.sitemaps\.org\/schemas\/sitemap\/0\.9">/);
116
+ assert.match(xml, /<loc>https:\/\/docs\.example\.org\/<\/loc>/);
117
+ assert.ok(xml.includes(`<loc>https://docs.example.org/guide/${encodeURIComponent("한국어 & tools")}/</loc>`));
118
+ assert.equal([...xml.matchAll(/<loc>/g)].length, 3);
119
+ assert.doesNotMatch(xml, /<lastmod>|<changefreq>|<priority>|robots\.txt|sitemap\.xml|_fh|#read/);
120
+
121
+ store.docs = [];
122
+ assert.equal([...(await (await app.request("/sitemap.xml")).text()).matchAll(/<loc>/g)].length, 1);
123
+ });
124
+
125
+ test("SSG writes crawler files and preserves ordinary and nested document pages", async (t) => {
126
+ const root = await mkdtemp(join(tmpdir(), "for-humanity-crawling-build-"));
127
+ t.after(() => rm(root, {recursive: true, force: true}));
128
+ await mkdir(join(root, "guide"));
129
+ await writeFile(join(root, "guide.md"), "---\nname: Start\nlabel: Start\ngroup: Guide\n---\n\nStart here.\n");
130
+ await writeFile(join(root, "guide/details.md"), "---\nname: Details\nlabel: Details\ngroup: Guide\n---\n\nNested details.\n");
131
+ const site = siteConfigSchema.parse({url: "https://docs.example.org"});
132
+ const docs = await readDocs({root, processor: createProcessor({site, root})});
133
+ const assets = {fontCss: "", preload: [], reload: false, browserScript: "", stylePath: asset_style_path};
134
+ const app = createApp({site, store: {docs}, assets});
135
+ const result = await toSSG(app, {writeFile, mkdir}, {dir: root});
136
+ assert.ok(site.url);
137
+ const crawlerResult = await toSSG(toCrawlingApp({url: site.url, store: {docs}}), {writeFile, mkdir}, {dir: root});
138
+
139
+ assert.equal(result.success, true);
140
+ assert.equal(crawlerResult.success, true);
141
+ assert.equal(result.files.length + crawlerResult.files.length, 5, "home, both documents and both crawler files must be emitted");
142
+ assert.match(await readFile(join(root, "guide/index.html"), "utf8"), /Start here\./);
143
+ assert.match(await readFile(join(root, "guide/details/index.html"), "utf8"), /Nested details\./);
144
+ assert.match(await readFile(join(root, "robots.txt"), "utf8"), /Sitemap: https:\/\/docs\.example\.org\/sitemap\.xml/);
145
+ assert.match(await readFile(join(root, "sitemap.xml"), "utf8"), /<loc>https:\/\/docs\.example\.org\/<\/loc>/);
146
+ });
147
+
148
+ test("crawler filenames reject conflicting document roots only when reserved", async (t) => {
149
+ const root = await mkdtemp(join(tmpdir(), "for-humanity-crawling-paths-"));
150
+ t.after(() => rm(root, {recursive: true, force: true}));
151
+ await mkdir(join(root, "sitemap.xml"));
152
+ await writeFile(join(root, "Robots.txt.md"), "---\nname: Robots\nlabel: Robots\ngroup: Guide\n---\n\nText.\n");
153
+ await writeFile(join(root, "sitemap.xml/guide.md"), "---\nname: Sitemap\nlabel: Sitemap\ngroup: Guide\n---\n\nText.\n");
154
+ const site = siteConfigSchema.parse({});
155
+ const processor = createProcessor({site, root});
156
+ const params = {root, processor, reserved: ["robots.txt", "sitemap.xml"]};
157
+
158
+ await assert.rejects(readDocs(params), /문서 id에 사용할 수 없는 경로/);
159
+ assert.equal((await readDocs({root, processor})).length, 2);
160
+ });
161
+
57
162
  test("navigation initialization runs after its DOM and before the document body", async () => {
58
163
  const site = siteConfigSchema.parse({});
59
164
  const browserScript = "window.navigationFixture = true;";
@@ -209,12 +314,11 @@ test("nested groups follow metadata paths and keep sibling branches and document
209
314
  const html = toNavigationHtml({site, docs, current: "authoring"});
210
315
 
211
316
  assert.match(html, /href="\/authoring\/"[^>]*aria-current="page"/);
212
- assert.match(html, /Atlas<\/span><button\b[^>]*aria-expanded="true"[^>]*>−<\/button><ul\b/);
213
- assert.match(html, /Research<\/span><button\b[^>]*aria-expanded="true"[^>]*>−<\/button><ul\b/);
317
+ assert.match(html, /Atlas<\/span><ul\b/);
318
+ assert.match(html, /Research<\/span><ul\b/);
214
319
  assert.equal([...html.matchAll(/href="\/authoring\/"/g)].length, 1);
215
320
  assert.equal([...html.matchAll(/>Research<\/span>/g)].length, 2);
216
- assert.equal([...html.matchAll(/wg_navigationNavList__item--current/g)].length, 3);
217
- assert.doesNotMatch(toNavigationHtml({site, docs}), /aria-current="page"|wg_navigationNavList__item--current/);
321
+ assert.doesNotMatch(toNavigationHtml({site, docs}), /aria-current="page"/);
218
322
  assert.ok(html.indexOf('href="/parts/"') < html.indexOf('href="/authoring/"'));
219
323
  assert.doesNotMatch(html, /<details\b/);
220
324
  });
@@ -265,17 +369,16 @@ test("parent documents contain ordered descendants with independent URLs and one
265
369
  assert.deepEqual(toDocGroups({docs: docs.toReversed(), navigation: site.navigation}), groups);
266
370
 
267
371
  const html = toNavigationHtml({site, docs, current: "details"});
268
- assert.match(html, /href="\/settings\/"[^>]*>Settings<\/a><button\b[^>]*aria-label="Collapse Settings"[^>]*>−<\/button><ul\b[^>]*aria-label="Settings"/);
269
- assert.match(html, /href="\/guide\/site\/"[^>]*>Site<\/a><button\b[^>]*aria-label="Collapse Site"[^>]*>−<\/button><ul\b[^>]*aria-label="Site"/);
372
+ assert.match(html, /href="\/settings\/"[^>]*>Settings<\/a><ul\b[^>]*aria-label="Settings"/);
373
+ assert.match(html, /href="\/guide\/site\/"[^>]*>Site<\/a><ul\b[^>]*aria-label="Site"/);
270
374
  assert.match(html, /href="\/details\/"[^>]*aria-current="page"/);
271
375
  assert.equal([...html.matchAll(/aria-current="page"/g)].length, 1);
272
376
  assert.equal([...html.matchAll(/wg_navigationNavList__link--active/g)].length, 1);
273
- assert.equal([...html.matchAll(/wg_navigationNavList__item--current/g)].length, 5);
274
377
  for (const doc of docs) {
275
378
  assert.equal(html.split(`href="/${doc.id}/"`).length - 1, 1);
276
379
  }
277
380
  assert.ok(html.indexOf('href="/themes/"') < html.indexOf('href="/guide/site/"'));
278
- assert.doesNotMatch(toNavigationHtml({site, docs}), /aria-current="page"|wg_navigationNavList__item--current/);
381
+ assert.doesNotMatch(toNavigationHtml({site, docs}), /aria-current="page"/);
279
382
 
280
383
  const app = createApp({
281
384
  site,
@@ -7,7 +7,7 @@
7
7
  import {EventEmitter} from "node:events";
8
8
  import {existsSync, readFileSync, watch} from "node:fs";
9
9
  import {copyFile, mkdir, rm, writeFile} from "node:fs/promises";
10
- import {basename, dirname, extname, join, relative, resolve, sep} from "node:path";
10
+ import {dirname, extname, join, relative, resolve, sep} from "node:path";
11
11
  import {fileURLToPath, pathToFileURL} from "node:url";
12
12
  import {serve} from "@hono/node-server";
13
13
  import {serveStatic} from "@hono/node-server/serve-static";
@@ -15,15 +15,17 @@ import {debounce} from "es-toolkit";
15
15
  import {Hono} from "hono";
16
16
  import {toSSG} from "hono/ssg";
17
17
  import {asset_favicon_path, asset_font_dir, asset_media_extensions, asset_style_path} from "@/constant/asset";
18
- import {copy_error_config, copy_error_font_preload, copy_error_prefix, copy_error_unknown_command} from "@/constant/copy";
19
- import {font_brand_css, font_cache_control, font_mono_css, font_sans_css, font_sans_preload_file} from "@/constant/font";
18
+ import {copy_error_config, copy_error_prefix, copy_error_unknown_command} from "@/constant/copy";
19
+ import {font_brand_css, font_cache_control, font_mono_css} from "@/constant/font";
20
20
  import {site_config_absent} from "@/constant/site";
21
21
  import {cli_config_file_name, cli_default_command, cli_default_docs_dir, cli_dev_port, cli_reload_delay_ms} from "@/entry/cli/_constant/cli";
22
+ import {crawling_reserved_roots} from "@/entry/cli/_constant/crawling";
22
23
  import {createApp} from "@/entry/cli/_function/create-app";
23
24
  import {createDevApp} from "@/entry/cli/_function/create-dev-app";
24
25
  import {createProcessor} from "@/entry/cli/_function/create-processor/create-processor";
25
26
  import {readDocs} from "@/entry/cli/_function/read-docs";
26
27
  import {readHome} from "@/entry/cli/_function/read-home";
28
+ import {toCrawlingApp} from "@/entry/cli/_function/to-crawling-app";
27
29
  import {siteConfigSchema} from "@/type/site-config";
28
30
  import {toChangeQueue} from "@/util/async/to-change-queue";
29
31
  import {toErrorMessage} from "@/util/error/to-error-message";
@@ -75,33 +77,25 @@ const main = async () => {
75
77
 
76
78
  const siteConfig = parsedConfig.data;
77
79
  // 내장 @font-face와 파일 수집 · font-family는 token.css 소유
78
- const sans = toFontCss({css: join(kitRoot, font_sans_css), fontDir: asset_font_dir});
79
80
  const mono = toFontCss({css: join(kitRoot, font_mono_css), fontDir: asset_font_dir});
80
81
  const brand = toFontCss({css: join(kitRoot, font_brand_css), fontDir: asset_font_dir});
81
- const sansPreload = [...sans.files].find((entry) => basename(entry[1]) === font_sans_preload_file);
82
82
 
83
- if (sansPreload === undefined) {
84
- throw new Error(`${copy_error_font_preload}: ${font_sans_preload_file}`);
85
- }
86
-
87
- const fontCss = [sans.css, mono.css, brand.css].join("\n");
83
+ const fontCss = [mono.css, brand.css].join("\n");
88
84
  const stylePath = toAssetPath({path: asset_style_path, file: join(kitRoot, "dist/cli.css")});
89
85
  const browserScript = readFileSync(join(kitRoot, "dist/browser.js"), "utf8");
90
- const kitFiles = new Map([
91
- [stylePath, join(kitRoot, "dist/cli.css")],
92
- [asset_favicon_path, join(kitRoot, "src/asset/favicon.svg")],
93
- ...sans.files,
94
- ...mono.files,
95
- ...brand.files,
96
- ]);
86
+ const kitFiles = new Map([[stylePath, join(kitRoot, "dist/cli.css")], [asset_favicon_path, join(kitRoot, "src/asset/favicon.svg")], ...mono.files, ...brand.files]);
97
87
  const files = new Map(kitFiles);
98
88
  const processor = createProcessor({site: siteConfig, root: docsRoot, files});
99
- const [docs, home] = await Promise.all([readDocs({root: docsRoot, processor}), readHome({root: docsRoot, processor})]);
89
+ const [docs, home] = await Promise.all([
90
+ readDocs({root: docsRoot, processor, reserved: siteConfig.url === undefined ? undefined : crawling_reserved_roots}),
91
+ readHome({root: docsRoot, processor}),
92
+ ]);
100
93
  const store = {docs, home};
94
+ const crawling = siteConfig.url === undefined ? undefined : toCrawlingApp({url: siteConfig.url, store});
101
95
  const app = createApp({
102
96
  site: siteConfig,
103
97
  store,
104
- assets: {stylePath, browserScript, fontCss, preload: [...brand.files.keys(), sansPreload[0], ...mono.files.keys()], reload: command === "dev"},
98
+ assets: {stylePath, browserScript, fontCss, preload: [...brand.files.keys(), ...mono.files.keys()], reload: command === "dev"},
105
99
  });
106
100
 
107
101
  if (command === "build") {
@@ -114,12 +108,20 @@ const main = async () => {
114
108
  throw result.error;
115
109
  }
116
110
 
111
+ const crawlerResult = crawling === undefined ? undefined : await toSSG(crawling, {writeFile, mkdir}, {dir: outDir});
112
+
113
+ if (crawlerResult?.success === false) {
114
+ throw crawlerResult.error;
115
+ }
116
+
117
117
  for (const [url, file] of files) {
118
118
  await mkdir(dirname(join(outDir, url)), {recursive: true});
119
119
  await copyFile(file, join(outDir, url));
120
120
  }
121
121
 
122
- console.log(`${result.files.length} pages, ${files.size} assets → ${relative(process.cwd(), outDir)}`);
122
+ console.log(
123
+ `${crawlerResult === undefined ? result.files.length : result.files.length + crawlerResult.files.length} files, ${files.size} assets → ${relative(process.cwd(), outDir)}`,
124
+ );
123
125
 
124
126
  return;
125
127
  }
@@ -127,7 +129,7 @@ const main = async () => {
127
129
  const reload = new EventEmitter();
128
130
  let generation = 0;
129
131
 
130
- serve({fetch: createDevApp({pages: app, files, reload}).fetch, port: cli_dev_port}, (info) => console.log(`http://localhost:${info.port}/`));
132
+ serve({fetch: createDevApp({pages: app, crawling, files, reload}).fetch, port: cli_dev_port}, (info) => console.log(`http://localhost:${info.port}/`));
131
133
 
132
134
  /**
133
135
  * 갱신은 하나씩 실행 · 연속 저장 합치기 · 최신 변경만 적용
@@ -139,7 +141,10 @@ const main = async () => {
139
141
  const currentGeneration = generation;
140
142
  const nextFiles = new Map(kitFiles);
141
143
  const nextProcessor = createProcessor({site: siteConfig, root: docsRoot, files: nextFiles});
142
- const [docs, home] = await Promise.all([readDocs({root: docsRoot, processor: nextProcessor}), readHome({root: docsRoot, processor: nextProcessor})]);
144
+ const [docs, home] = await Promise.all([
145
+ readDocs({root: docsRoot, processor: nextProcessor, reserved: siteConfig.url === undefined ? undefined : crawling_reserved_roots}),
146
+ readHome({root: docsRoot, processor: nextProcessor}),
147
+ ]);
143
148
 
144
149
  if (currentGeneration !== generation) {
145
150
  return;
@@ -1,35 +1,25 @@
1
1
  import assert from "node:assert/strict";
2
2
  import {test} from "node:test";
3
3
  import {createNavigationStores} from "@/store/navigation/create-navigation-stores";
4
- import {navigation_scroll_storage_key, navigation_tree_storage_key} from "@/store/navigation/navigation-storage";
4
+ import {navigation_scroll_storage_key} from "@/store/navigation/navigation-storage";
5
5
 
6
6
  const fixture = () => {
7
- const local = new Map<string, string>();
8
7
  const session = new Map<string, string>();
9
- const storage = (values: Map<string, string>) => ({
8
+ const storage = {
10
9
  getItem: (key: string) => {
11
- const value = values.get(key);
10
+ const value = session.get(key);
12
11
  return value === undefined ? null : value;
13
12
  },
14
13
  setItem: (key: string, value: string) => {
15
- values.set(key, value);
14
+ session.set(key, value);
16
15
  },
17
16
  removeItem: (key: string) => {
18
- values.delete(key);
17
+ session.delete(key);
19
18
  },
20
- });
21
- return {local, session, options: {local: () => storage(local), session: () => storage(session)}};
19
+ };
20
+ return {session, options: {session: () => storage}};
22
21
  };
23
22
 
24
- test("starts with every branch expanded and persists explicit choices across pages", () => {
25
- const setup = fixture();
26
- const stores = createNavigationStores(setup.options);
27
- assert.deepEqual(stores.tree.getState(), {collapsed: {}});
28
- stores.tree.setState({collapsed: {'group:["Projects"]': true, "outline:one:heading:setup": false}});
29
- assert.deepEqual(createNavigationStores(setup.options).tree.getState(), stores.tree.getState());
30
- assert.equal(setup.session.has(navigation_tree_storage_key), false);
31
- });
32
-
33
23
  test("retains independent wide, desktop and drawer scroll positions within a tab", () => {
34
24
  const setup = fixture();
35
25
  const stores = createNavigationStores(setup.options);
@@ -41,23 +31,19 @@ test("retains independent wide, desktop and drawer scroll positions within a tab
41
31
  },
42
32
  });
43
33
  assert.deepEqual(createNavigationStores(setup.options).scroll.getState(), stores.scroll.getState());
44
- assert.equal(setup.local.has(navigation_scroll_storage_key), false);
45
34
  });
46
35
 
47
36
  test("rejects malformed and unknown-version persistence without losing defaults", () => {
48
- for (const value of ["{", "null", '{"state":null,"version":1}', '{"state":{"collapsed":{"group:a":true}},"version":999}']) {
37
+ for (const value of ["{", "null", '{"state":null,"version":1}', '{"state":{"positions":{}},"version":999}']) {
49
38
  const setup = fixture();
50
- setup.local.set(navigation_tree_storage_key, value);
51
39
  setup.session.set(navigation_scroll_storage_key, value);
52
40
  const stores = createNavigationStores(setup.options);
53
- assert.deepEqual(stores.tree.getState(), {collapsed: {}});
54
41
  assert.deepEqual(stores.scroll.getState(), {positions: {}});
55
42
  }
56
43
  });
57
44
 
58
- test("narrows persisted branch flags and rejects invalid scroll coordinates", () => {
45
+ test("rejects invalid scroll coordinates", () => {
59
46
  const setup = fixture();
60
- setup.local.set(navigation_tree_storage_key, JSON.stringify({version: 1, state: {collapsed: {good: true, bad: "true"}, unexpected: true}}));
61
47
  setup.session.set(
62
48
  navigation_scroll_storage_key,
63
49
  JSON.stringify({
@@ -72,7 +58,6 @@ test("narrows persisted branch flags and rejects invalid scroll coordinates", ()
72
58
  }),
73
59
  );
74
60
  const stores = createNavigationStores(setup.options);
75
- assert.deepEqual(stores.tree.getState(), {collapsed: {good: true}});
76
61
  assert.deepEqual(stores.scroll.getState().positions.wide, {navigation: "docs", pathname: "/one/", documents: 100, outline: 20});
77
62
  assert.equal(stores.scroll.getState().positions.desktop, undefined);
78
63
  assert.equal(stores.scroll.getState().positions.drawer, undefined);
@@ -82,10 +67,7 @@ test("keeps in-memory interaction working when storage reads and writes are bloc
82
67
  const blocked = () => {
83
68
  throw new Error("Storage blocked");
84
69
  };
85
- const stores = createNavigationStores({local: blocked, session: blocked});
86
- stores.tree.setState({collapsed: {"document:one": true}});
87
- stores.tree.persist.rehydrate();
88
- assert.equal(stores.tree.getState().collapsed["document:one"], true);
70
+ const stores = createNavigationStores({session: blocked});
89
71
  stores.scroll.setState({positions: {desktop: {navigation: "docs", pathname: "/", documents: 100, outline: 200}}});
90
72
  stores.scroll.persist.rehydrate();
91
73
  assert.equal(stores.scroll.getState().positions.desktop?.documents, 100);
@@ -1,18 +1,13 @@
1
1
  import {createJSONStorage, persist, type StateStorage} from "zustand/middleware";
2
2
  import {createStore} from "zustand/vanilla";
3
- import type {NavigationScrollState, NavigationTreeState} from "@/store/navigation/navigation-state";
4
- import {navigation_scroll_storage_key, navigation_storage_version, navigation_tree_storage_key} from "@/store/navigation/navigation-storage";
3
+ import type {NavigationScrollState} from "@/store/navigation/navigation-state";
4
+ import {navigation_scroll_storage_key, navigation_storage_version} from "@/store/navigation/navigation-storage";
5
5
  import {toNavigationScrollState} from "@/store/navigation/to-navigation-scroll-state";
6
- import {toNavigationTreeState} from "@/store/navigation/to-navigation-tree-state";
7
6
 
8
7
  /**
9
8
  * 브라우저 저장소를 늦게 읽는 경계 · 차단 환경과 테스트의 메모리 저장소 지원
10
9
  */
11
10
  interface NavigationStorage {
12
- /**
13
- * 브라우저 재실행 후에도 유지할 접힘 선택
14
- */
15
- local: () => StateStorage;
16
11
  /**
17
12
  * 같은 탭 안에서만 복원할 스크롤 위치
18
13
  */
@@ -25,9 +20,9 @@ interface NavigationStorage {
25
20
  */
26
21
  export const createNavigationStores = (options: NavigationStorage) => {
27
22
  /**
28
- * 저장소 접근이 막혀도 persist 갱신이 버튼·스크롤 동작을 중단하지 않도록 격리
23
+ * 저장소 접근이 막혀도 persist 갱신이 스크롤 동작을 중단하지 않도록 격리
29
24
  */
30
- const toSafeStorage = (getStorage: NavigationStorage["local"]): StateStorage => {
25
+ const toSafeStorage = (getStorage: NavigationStorage["session"]): StateStorage => {
31
26
  return {
32
27
  getItem: (key) => {
33
28
  // persist의 읽기 오류 경로로 보내 기존 메모리 상태를 유지 · null은 실제 저장소 초기화에만 사용
@@ -51,15 +46,6 @@ export const createNavigationStores = (options: NavigationStorage) => {
51
46
  };
52
47
 
53
48
  return {
54
- tree: createStore<NavigationTreeState>()(
55
- persist(() => ({collapsed: {}}), {
56
- name: navigation_tree_storage_key,
57
- version: navigation_storage_version,
58
- storage: createJSONStorage(() => toSafeStorage(options.local)),
59
- merge: (persisted) => toNavigationTreeState(persisted),
60
- migrate: () => ({collapsed: {}}),
61
- }),
62
- ),
63
49
  scroll: createStore<NavigationScrollState>()(
64
50
  persist(() => ({positions: {}}), {
65
51
  name: navigation_scroll_storage_key,
@@ -3,16 +3,6 @@
3
3
  */
4
4
  export type NavigationLayout = "wide" | "desktop" | "drawer";
5
5
 
6
- /**
7
- * 사용자가 명시적으로 바꾼 가지 선택 · 없는 키는 펼침
8
- */
9
- export interface NavigationTreeState {
10
- /**
11
- * 문서 ID·그룹 전체 경로·페이지별 heading으로 구분한 접힘 상태
12
- */
13
- collapsed: Record<string, boolean>;
14
- }
15
-
16
6
  /**
17
7
  * 한 화면 배치의 마지막 탐색 위치 · 목록 변경과 다른 페이지의 목차는 재사용하지 않음
18
8
  */
@@ -1,8 +1,3 @@
1
- /**
2
- * 문서·현재 페이지 목차의 접힘 선택 · 기본은 전체 펼침
3
- */
4
- export const navigation_tree_storage_key = "for-humanity:navigation-tree";
5
-
6
1
  /**
7
2
  * 저장 형태가 바뀌면 기존 자료를 기본 상태로 돌리는 persist 버전
8
3
  */