for-humanity 0.13.0 → 0.14.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.
@@ -1,4 +1,4 @@
1
- import { S as copy_nav_toc_label, T as copy_warn_unknown_status, _ as copy_home_missing, a as copy_error_doc_id_clash, b as copy_nav_open, c as copy_error_doc_parent_missing, g as copy_error_unknown_directive, h as copy_error_snippet, i as copy_error_doc_id, l as copy_error_frontmatter, o as copy_error_doc_parent_cycle, r as copy_error_directive_syntax, s as copy_error_doc_parent_group, t as copy_error_asset, u as copy_error_home_clash, v as copy_nav_aria_label, w as copy_warn_broken_link, x as copy_nav_title, y as copy_nav_close } from "./copy-C1uzvEnr.js";
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
2
  import { t as logger } from "./logger-Dv1i9xSV.js";
3
3
  import { t as groupPathSchema } from "./group-path-XAThmzvC.js";
4
4
  import { t as toChangeQueue } from "./to-change-queue-DbEnZGNK.js";
@@ -21,10 +21,12 @@ import { SKIP, visit } from "unist-util-visit";
21
21
  import { createHash } from "node:crypto";
22
22
  import Slugger from "github-slugger";
23
23
  import { toString } from "hast-util-to-string";
24
+ import { dropRightWhile, dropWhile, minBy, takeWhile, uniqBy } from "es-toolkit/array";
24
25
  import { transformerMetaHighlight, transformerNotationDiff, transformerNotationErrorLevel, transformerNotationHighlight, transformerNotationWordHighlight } from "@shikijs/transformers";
25
26
  import { toString as toString$1 } from "mdast-util-to-string";
26
27
  import { escape, escapeRegExp } from "es-toolkit/string";
27
28
  import { findAndReplace } from "mdast-util-find-and-replace";
29
+ import { isEqual } from "es-toolkit/predicate";
28
30
  import { VFile } from "vfile";
29
31
  import { parse } from "yaml";
30
32
  import { Hono } from "hono";
@@ -33,7 +35,6 @@ import { ssgParams } from "hono/ssg";
33
35
  import clsx from "clsx";
34
36
  import { jsx, jsxs } from "hono/jsx/jsx-runtime";
35
37
  import { createHead, escapeHtml } from "unhead/server";
36
- import { takeWhile } from "es-toolkit/array";
37
38
  import { Fragment, useEffect, useLayoutEffect, useRef, useState } from "hono/jsx";
38
39
  import { flushSync } from "hono/jsx/dom";
39
40
  //#region src/app/_constant/crawling.ts
@@ -105,6 +106,23 @@ var asset_media_extensions = /* @__PURE__ */ new Set([
105
106
  ".zip"
106
107
  ]);
107
108
  //#endregion
109
+ //#region src/util/file/assert-content-path.ts
110
+ /**
111
+ * 파일 경로와 symlink 대상의 공개 범위 검사
112
+ */
113
+ var assertContentPath = (params) => {
114
+ for (const path of [resolve(params.path), ...existsSync(params.path) ? [realpathSync(params.path)] : []]) {
115
+ const location = relative(params.scope.root, path);
116
+ const internal = relative(params.scope.privateRoot, path);
117
+ if (location === ".." || location.startsWith(`..${sep}`) || isAbsolute(location) || location.split(sep).some((part) => [
118
+ "dist",
119
+ ".git",
120
+ "node_modules"
121
+ ].includes(part))) throw new Error(`문서 범위 밖의 참조: ${params.path}`);
122
+ if (!params.scope.includePrivate && internal !== ".." && !internal.startsWith(`..${sep}`) && !isAbsolute(internal)) throw new Error(`공개 문서에서 private 자료를 참조할 수 없습니다: ${params.path}`);
123
+ }
124
+ };
125
+ //#endregion
108
126
  //#region src/util/file/to-asset-path.ts
109
127
  /**
110
128
  * 파일명에 내용 지문 추가. 같은 내용은 같은 URL, 변경한 내용은 새 캐시 키 사용
@@ -136,6 +154,14 @@ var rehypeAssets = (options) => (tree, file) => {
136
154
  return;
137
155
  }
138
156
  const target = resolve(dirname(file.path), path);
157
+ if (options.scope !== void 0) try {
158
+ assertContentPath({
159
+ scope: options.scope,
160
+ path: target
161
+ });
162
+ } catch (error) {
163
+ file.fail(error instanceof Error ? error.message : String(error), node);
164
+ }
139
165
  const location = relative(options.root, target);
140
166
  if (location === ".." || location.startsWith(`..${sep}`) || isAbsolute(location)) file.fail(`${copy_error_asset}: 문서 폴더 밖 · ${url}`, node);
141
167
  if (!existsSync(target)) file.fail(`${copy_error_asset}: 파일이 없다 · ${url}`, node);
@@ -151,6 +177,69 @@ var rehypeAssets = (options) => (tree, file) => {
151
177
  });
152
178
  };
153
179
  //#endregion
180
+ //#region src/util/file/to-content-id.ts
181
+ /**
182
+ * 폴더 이름과 독립적인 공개 URL, 내부 문서의 private 접두사
183
+ */
184
+ var toContentId = (params) => {
185
+ if (relative(params.scope.root, params.path).toLowerCase() === "readme.md") return "";
186
+ for (const [root, prefix] of [[params.scope.publicRoot, ""], [params.scope.privateRoot, "private/"]]) {
187
+ const location = relative(root, params.path);
188
+ if (location === ".." || location.startsWith(`..${sep}`) || isAbsolute(location)) continue;
189
+ const id = location.replace(/\.mdx?$/i, "").split(sep).join("/").toLowerCase();
190
+ if (prefix === "" && id.split("/")[0] === "private") throw new Error(`private URL은 내부 문서 전용입니다: ${params.path}`);
191
+ return `${prefix}${id}`;
192
+ }
193
+ throw new Error(`공개 또는 내부 문서 폴더에 없는 Markdown 참조: ${params.path}`);
194
+ };
195
+ //#endregion
196
+ //#region src/app/_function/create-processor/_rehype-content-links.ts
197
+ /**
198
+ * 원시 HTML의 내부 참조 차단과 Markdown 링크의 사이트 URL 변환
199
+ */
200
+ var rehypeContentLinks = (options) => (tree, file) => {
201
+ const scope = options.scope;
202
+ if (scope === void 0) return;
203
+ visit(tree, "element", (node) => {
204
+ for (const property of [
205
+ "href",
206
+ "src",
207
+ "poster",
208
+ "data",
209
+ "srcSet"
210
+ ]) {
211
+ const value = node.properties[property];
212
+ if (typeof value !== "string") continue;
213
+ const urls = property === "srcSet" ? value.split(",").map((entry) => entry.trim().split(/\s+/)[0]) : [value];
214
+ for (const url of urls) {
215
+ if (/^(?:[a-z][a-z\d+.-]*:|\/\/|#)/i.test(url)) continue;
216
+ const path = decodeURIComponent(url.split(/[?#]/)[0]);
217
+ try {
218
+ if (path.startsWith("/")) {
219
+ const pathname = new URL(path, "https://content.invalid").pathname.toLowerCase();
220
+ if (!scope.includePrivate && (pathname === "/private" || pathname.startsWith("/private/"))) throw new Error(`공개 문서에서 private URL을 참조할 수 없습니다: ${url}`);
221
+ continue;
222
+ }
223
+ const target = resolve(dirname(file.path), path);
224
+ assertContentPath({
225
+ scope,
226
+ path: target
227
+ });
228
+ if (property === "href" && /\.mdx?$/i.test(path)) {
229
+ const id = toContentId({
230
+ scope,
231
+ path: target
232
+ });
233
+ node.properties[property] = `${id === "" ? "/" : `/${id}/`}${url.slice(url.split(/[?#]/)[0].length)}`;
234
+ }
235
+ } catch (error) {
236
+ file.fail(error instanceof Error ? error.message : String(error), node);
237
+ }
238
+ }
239
+ }
240
+ });
241
+ };
242
+ //#endregion
154
243
  //#region src/app/_function/create-processor/_rehype-heading-ids.ts
155
244
  /**
156
245
  * 제목 id와 목차 수집. 같은 제목은 -1, -2 접미사
@@ -212,11 +301,12 @@ var snippet_language_by_extension = {
212
301
  ".yml": "yaml"
213
302
  };
214
303
  //#endregion
215
- //#region src/app/_function/create-processor/_remark-snippets.ts
304
+ //#region src/app/_function/create-processor/_to-snippet-body.ts
216
305
  /**
217
306
  * region 지정 시 해당 범위만 추출, 미지정 시 전체 사용
218
307
  *
219
308
  * - region 경계 줄과 공통 들여쓰기 제거
309
+ * - 빈 region은 빈 문자열, 없는 region은 undefined 반환
220
310
  */
221
311
  var toSnippetBody = (source, region) => {
222
312
  const lines = source.replace(/\r\n/g, "\n").split("\n");
@@ -235,12 +325,14 @@ var toSnippetBody = (source, region) => {
235
325
  if (inside) kept.push(line);
236
326
  }
237
327
  if (!found) return;
238
- while (kept.length > 0 && kept[0]?.trim() === "") kept.shift();
239
- while (kept.length > 0 && kept.at(-1)?.trim() === "") kept.pop();
240
- const indents = kept.filter((line) => line.trim() !== "").map((line) => line.length - line.trimStart().length);
241
- const indent = indents.length === 0 ? 0 : Math.min(...indents);
242
- return kept.map((line) => line.slice(Math.min(indent, line.length - line.trimStart().length))).join("\n");
328
+ const content = dropRightWhile(dropWhile(kept, (line) => line.trim() === ""), (line) => line.trim() === "");
329
+ const leastIndentedLine = minBy(content.filter((line) => line.trim() !== ""), (line) => line.length - line.trimStart().length);
330
+ if (leastIndentedLine === void 0) return "";
331
+ const indent = leastIndentedLine.length - leastIndentedLine.trimStart().length;
332
+ return content.map((line) => line.slice(Math.min(indent, line.length - line.trimStart().length))).join("\n");
243
333
  };
334
+ //#endregion
335
+ //#region src/app/_function/create-processor/_remark-snippets.ts
244
336
  /**
245
337
  * 문서 상대 경로의 코드 스니펫 삽입
246
338
  *
@@ -255,6 +347,14 @@ var remarkSnippets = (options) => (tree, file) => {
255
347
  if (match === null) return;
256
348
  const [, path = "", region, meta] = match;
257
349
  const target = resolve(dirname(file.path), path);
350
+ if (options.scope !== void 0) try {
351
+ assertContentPath({
352
+ scope: options.scope,
353
+ path: target
354
+ });
355
+ } catch (error) {
356
+ file.fail(error instanceof Error ? error.message : String(error), node);
357
+ }
258
358
  const location = relative(options.root, target);
259
359
  if (location === ".." || location.startsWith(`..${sep}`) || isAbsolute(location)) file.fail(`${copy_error_snippet}: 문서 폴더 밖 · ${path}`, node);
260
360
  if (!existsSync(target)) file.fail(`${copy_error_snippet}: 파일이 없다 · ${path}`, node);
@@ -286,49 +386,43 @@ var shikiTransformers = [
286
386
  transformerNotationWordHighlight({ matchAlgorithm: "v3" }),
287
387
  transformerMetaHighlight(),
288
388
  {
289
- name: "for-humanity:line-numbers",
290
- pre(node) {
291
- const meta = this.options.meta?.__raw;
292
- if (typeof meta === "string" && /\bline-numbers\b/.test(meta)) this.addClassToHast(node, "wg_prose__lineNumbers");
293
- }
294
- },
295
- {
296
- name: "for-humanity:code-tone",
297
- pre(node) {
298
- const meta = this.options.meta?.__raw;
299
- if (typeof meta !== "string") return;
300
- for (const word of ["bad", "good"]) if (new RegExp(`\\b${word}\\b`).test(meta)) this.addClassToHast(node, `wg_prose__code--${word}`);
301
- }
302
- },
303
- {
304
- name: "for-humanity:code-title",
389
+ name: "for-humanity:code-metadata",
305
390
  root(root) {
306
- const meta = this.options.meta?.__raw;
307
- const title = typeof meta === "string" ? /\[([^\]]+)\]/.exec(meta)?.[1] : void 0;
391
+ this.pre.properties.style = void 0;
392
+ this.addClassToHast(this.pre, "wg_prose__code");
393
+ if (this.options.meta?.__raw === void 0) return;
394
+ if (/\bline-numbers\b/.test(this.options.meta.__raw)) this.addClassToHast(this.pre, "wg_prose__lineNumbers");
395
+ const isBad = /\bbad\b/.test(this.options.meta.__raw);
396
+ const isGood = /\bgood\b/.test(this.options.meta.__raw);
397
+ if (isBad) this.addClassToHast(this.pre, "wg_prose__code--bad");
398
+ if (isGood) this.addClassToHast(this.pre, "wg_prose__code--good");
399
+ const title = /\[([^\]]+)\]/.exec(this.options.meta.__raw)?.[1];
308
400
  if (title === void 0) return;
309
- const tones = typeof meta === "string" ? ["bad", "good"].filter((word) => new RegExp(`\\b${word}\\b`).test(meta)) : [];
310
- const caption = {
401
+ root.children = [{
311
402
  type: "element",
312
- tagName: "figcaption",
313
- properties: { className: ["wg_prose__codeFileTitle"] },
403
+ tagName: "figure",
404
+ properties: { className: ["wg_prose__codeFile"] },
314
405
  children: [{
315
- type: "text",
316
- value: title
317
- }, {
318
406
  type: "element",
319
- tagName: "span",
320
- properties: { className: ["wg_prose__codeFileLang"] },
407
+ tagName: "figcaption",
408
+ properties: { className: [
409
+ "wg_prose__codeFileTitle",
410
+ ...isBad ? ["wg_prose__codeFileTitle--bad"] : [],
411
+ ...isGood ? ["wg_prose__codeFileTitle--good"] : []
412
+ ] },
321
413
  children: [{
322
414
  type: "text",
323
- value: this.options.lang
415
+ value: title
416
+ }, {
417
+ type: "element",
418
+ tagName: "span",
419
+ properties: { className: ["wg_prose__codeFileLang"] },
420
+ children: [{
421
+ type: "text",
422
+ value: this.options.lang
423
+ }]
324
424
  }]
325
- }]
326
- };
327
- root.children = [{
328
- type: "element",
329
- tagName: "figure",
330
- properties: { className: ["wg_prose__codeFile", ...tones.map((tone) => `wg_prose__codeFile--${tone}`)] },
331
- children: [caption, ...root.children.filter((child) => child.type !== "doctype")]
425
+ }, ...root.children.filter((child) => child.type !== "doctype")]
332
426
  }];
333
427
  }
334
428
  }
@@ -363,25 +457,25 @@ var section_heading_depth = {
363
457
  * name 제목과 첫 절 앞 본문의 공통 머리. README는 원문의 h1 사용
364
458
  */
365
459
  var rehypeHead = () => (tree, file) => {
366
- const frontmatter = file.data.fh?.frontmatter;
367
460
  const firstSection = tree.children.findIndex((node) => node.type === "element" && node.tagName === `h${section_heading_depth.section}`);
368
461
  const leadEnd = firstSection === -1 ? tree.children.length : firstSection;
369
- if (frontmatter === void 0 && leadEnd === 0) return;
370
- tree.children = [{
462
+ const head = {
371
463
  type: "element",
372
464
  tagName: "header",
373
465
  properties: { className: ["wg_prose__head"] },
374
- children: [...tree.children.slice(0, leadEnd).filter((node) => node.type !== "doctype")]
375
- }, ...tree.children.slice(leadEnd)];
376
- if (frontmatter !== void 0 && tree.children[0].type === "element") tree.children[0].children.unshift({
466
+ children: tree.children.slice(0, leadEnd).filter((node) => node.type !== "doctype")
467
+ };
468
+ if (file.data.fh?.frontmatter !== void 0) head.children.unshift({
377
469
  type: "element",
378
470
  tagName: "h1",
379
- properties: {},
471
+ properties: { className: ["wg_prose__title"] },
380
472
  children: [{
381
473
  type: "text",
382
- value: frontmatter.name
474
+ value: file.data.fh.frontmatter.name
383
475
  }]
384
476
  });
477
+ else if (leadEnd === 0) return;
478
+ tree.children = [head, ...tree.children.slice(leadEnd)];
385
479
  };
386
480
  //#endregion
387
481
  //#region src/component/widget/prose/_function/rehype-sections/_to-section-headings.ts
@@ -407,7 +501,9 @@ var toSectionHeadings = (node) => {
407
501
  */
408
502
  var rehypeSections = () => (tree, file) => {
409
503
  if (file.data.fh === void 0) return;
410
- file.data.fh.sections = toSectionHeadings(tree).map((heading) => ({ part: typeof heading.properties.dataPart === "string" ? heading.properties.dataPart : void 0 }));
504
+ file.data.fh.sections = toSectionHeadings(tree).map((heading) => {
505
+ return { part: typeof heading.properties.dataPart === "string" ? heading.properties.dataPart : void 0 };
506
+ });
411
507
  };
412
508
  //#endregion
413
509
  //#region src/component/widget/prose/_function/rehype-tables.ts
@@ -513,6 +609,24 @@ var remarkLinks = (options) => (tree, file) => {
513
609
  if (node.type !== "link" && node.type !== "definition") return;
514
610
  const match = /^(?![a-z]+:|\/|#)([^#?]+)(\.mdx?)([?#].*|)$/i.exec(node.url);
515
611
  if (match) {
612
+ if (options.scope !== void 0) {
613
+ const target = resolve(dirname(file.path), decodeURIComponent(`${match[1]}${match[2]}`));
614
+ try {
615
+ assertContentPath({
616
+ scope: options.scope,
617
+ path: target
618
+ });
619
+ const id = toContentId({
620
+ scope: options.scope,
621
+ path: target
622
+ });
623
+ if (!existsSync(target)) file.message(`${copy_warn_broken_link}: ${node.url}`, node);
624
+ node.url = `${id === "" ? "/" : `/${id}/`}${match[3]}`;
625
+ } catch (error) {
626
+ file.fail(error instanceof Error ? error.message : String(error), node);
627
+ }
628
+ return;
629
+ }
516
630
  if (!existsSync(resolve(dirname(file.path), `${match[1]}${match[2]}`))) file.message(`${copy_warn_broken_link}: ${node.url}`, node);
517
631
  const target = posix.join(here, `${match[1]}${match[2]}`).toLowerCase();
518
632
  node.url = target === "readme.md" ? `/${match[3]}` : `/${target.slice(0, -match[2].length)}/${match[3]}`;
@@ -626,21 +740,29 @@ var remarkUnknownDirectives = () => (tree, file) => {
626
740
  *
627
741
  * - 코드와 문서 머리 생성 후 제목 ID와 가름 수집
628
742
  * - 플러그인 HTML 조각은 마지막 rehype-raw에서 해석
629
- * - Shiki inline style: token.css의 --app-code-* 참조
743
+ * - 코드 상자의 색상은 prose CSS 소유, Shiki 토큰 색상은 --app-code-* 참조
630
744
  */
631
745
  var createProcessor = (options) => {
632
- return unified().use(remarkParse).use(remarkFrontmatter, ["yaml"]).use(remarkGfm).use(remarkSnippets, { root: options.root }).use(remarkSmartypants).use(remarkDirective).use(remarkBlocks).use(remarkParts).use(remarkMermaid, {
746
+ const files = options.files ?? /* @__PURE__ */ new Map();
747
+ return unified().use(remarkParse).use(remarkFrontmatter, ["yaml"]).use(remarkGfm).use(remarkSnippets, {
748
+ root: options.root,
749
+ scope: options.scope
750
+ }).use(remarkSmartypants).use(remarkDirective).use(remarkBlocks).use(remarkParts).use(remarkMermaid, {
633
751
  assets: options.mermaid,
634
- files: options.files ?? /* @__PURE__ */ new Map()
635
- }).use(remarkStatus, { status: options.site.status }).use(remarkSwatch).use(remarkLinks, { root: options.root }).use(remarkUnknownDirectives).use(remarkReport, { root: options.root }).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeShiki, {
752
+ files
753
+ }).use(remarkStatus, { status: options.site.status }).use(remarkSwatch).use(remarkLinks, {
754
+ root: options.root,
755
+ scope: options.scope
756
+ }).use(remarkUnknownDirectives).use(remarkReport, { root: options.root }).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeShiki, {
636
757
  theme: createCssVariablesTheme({
637
758
  name: "for-humanity",
638
759
  variablePrefix: "--app-code-"
639
760
  }),
640
761
  transformers: shikiTransformers
641
- }).use(rehypeHead).use(rehypeHeadingIds).use(rehypeSections).use(rehypeTables).use(rehypeRaw).use(rehypeAssets, {
762
+ }).use(rehypeHead).use(rehypeHeadingIds).use(rehypeSections).use(rehypeTables).use(rehypeRaw).use(rehypeContentLinks, { scope: options.scope }).use(rehypeAssets, {
642
763
  root: options.root,
643
- files: options.files ?? /* @__PURE__ */ new Map()
764
+ files,
765
+ scope: options.scope
644
766
  }).use(rehypeStringify, { allowDangerousHtml: true });
645
767
  };
646
768
  //#endregion
@@ -659,6 +781,32 @@ var loadMermaidAssets = (kitRoot) => {
659
781
  };
660
782
  };
661
783
  //#endregion
784
+ //#region src/app/_function/read-content-scope.ts
785
+ /**
786
+ * 공개 폴더 필수, 내부 폴더 선택, 범위 폴더의 symlink 별칭 금지
787
+ */
788
+ var readContentScope = (params) => {
789
+ if (!existsSync(params.root) || !statSync(params.root).isDirectory()) throw new Error(`content 문서 루트가 없습니다: ${params.root}`);
790
+ const root = realpathSync(params.root);
791
+ const publicRoot = join(root, params.content.public);
792
+ const privateRoot = join(root, params.content.private);
793
+ for (const path of [publicRoot, privateRoot]) {
794
+ if (!existsSync(path)) {
795
+ if (path === privateRoot) continue;
796
+ throw new Error(`공개 문서 폴더가 없습니다: ${path}. content/public 구조 또는 content 설정을 확인하세요`);
797
+ }
798
+ const actual = realpathSync(path);
799
+ const location = relative(root, actual);
800
+ if (!statSync(path).isDirectory() || actual !== path || location === ".." || location.startsWith(`..${sep}`) || isAbsolute(location)) throw new Error(`문서 폴더는 content 내부의 실제 폴더여야 합니다: ${path}`);
801
+ }
802
+ return {
803
+ root,
804
+ publicRoot,
805
+ privateRoot,
806
+ includePrivate: params.includePrivate
807
+ };
808
+ };
809
+ //#endregion
662
810
  //#region src/constant/doc.ts
663
811
  /**
664
812
  * 기존 frontmatter type의 호환 값. 모두 같은 Markdown 렌더링
@@ -707,15 +855,23 @@ var docDataSchema = z.object({
707
855
  //#endregion
708
856
  //#region src/app/_function/read-docs.ts
709
857
  /**
710
- * Markdown 수집과 렌더링. 루트 README는 홈, AGENTS, CLAUDE는 작성 지침으로 분리
858
+ * 공개 범위의 Markdown 수집과 렌더링, 범위 미지정 시 독립 Markdown 처리
711
859
  *
712
- * - 문서 목록에서 루트 안내 파일, node_modules, dist 제외
860
+ * - 사이트 실행에서 공개·내부 문서만 수집, content 루트 안내 파일 제외
713
861
  * - 머리말, 문서 식별자 중복, 플러그인 오류 시 실패
714
862
  */
715
863
  var readDocs = async (options) => {
716
- const paths = (await readdir(options.root, { recursive: true })).filter((path) => /\.mdx?$/i.test(path) && !/^(readme|agents|claude)\.md$/i.test(path) && !path.split(sep).includes("node_modules") && !path.startsWith(`dist${sep}`));
864
+ const roots = options.scope === void 0 ? [options.root] : [options.scope.publicRoot, ...options.scope.includePrivate && existsSync(options.scope.privateRoot) ? [options.scope.privateRoot] : []];
865
+ const paths = (await Promise.all(roots.map(async (root) => (await readdir(root, { recursive: true })).map((path) => relative(options.root, join(root, path)))))).flat().filter((path) => /\.mdx?$/i.test(path) && !/^(readme|agents|claude)\.md$/i.test(path) && !path.split(sep).includes("node_modules") && !path.startsWith(`dist${sep}`));
717
866
  const docs = await Promise.all(paths.map(async (path) => {
718
- const id = path.replace(/\.mdx?$/i, "").split(sep).join("/").toLowerCase();
867
+ if (options.scope !== void 0) assertContentPath({
868
+ scope: options.scope,
869
+ path: join(options.root, path)
870
+ });
871
+ const id = options.scope === void 0 ? path.replace(/\.mdx?$/i, "").split(sep).join("/").toLowerCase() : toContentId({
872
+ scope: options.scope,
873
+ path: join(options.root, path)
874
+ });
719
875
  const segments = id.split("/");
720
876
  if (segments.some((segment) => segment === "" || segment === "." || segment === ".." || segment === "index.html") || /[#?%*:\\]/.test(id) || /[\p{Cc}]/u.test(id) || options.reserved?.includes(segments[0]) === true || segments[0] === "_fh" || segments[0] === "/favicon.svg".slice(1)) throw new Error(`${copy_error_doc_id}: ${path}`);
721
877
  const source = await readFile(join(options.root, path), "utf8");
@@ -727,9 +883,14 @@ var readDocs = async (options) => {
727
883
  const frontmatter = tree.children[0];
728
884
  const parsed = docDataSchema.safeParse(frontmatter?.type === "yaml" ? parse(frontmatter.value) : {});
729
885
  if (!parsed.success) throw new Error(`${copy_error_frontmatter}: ${path}: ${parsed.error.issues.map((issue) => `${issue.path.join(".")} ${issue.message}`).join(", ")}`);
886
+ const parentPath = parsed.data.parent !== void 0 && /^\.\.?\//.test(parsed.data.parent) ? resolve(dirname(file.path), parsed.data.parent) : void 0;
887
+ const parent = parsed.data.parent !== void 0 && parentPath !== void 0 ? posix.join(posix.dirname(id), parsed.data.parent) : parsed.data.parent;
730
888
  const data = {
731
889
  ...parsed.data,
732
- parent: parsed.data.parent !== void 0 && /^\.\.?\//.test(parsed.data.parent) ? posix.join(posix.dirname(id), parsed.data.parent) : parsed.data.parent
890
+ parent: options.scope !== void 0 && parentPath !== void 0 ? toContentId({
891
+ scope: options.scope,
892
+ path: parentPath
893
+ }) : parent
733
894
  };
734
895
  const fh = {
735
896
  frontmatter: data,
@@ -759,7 +920,7 @@ var readDocs = async (options) => {
759
920
  const parent = docsById.get(parentId);
760
921
  if (parent === void 0) throw new Error(`${copy_error_doc_parent_missing}: ${doc.id} → ${parentId}`);
761
922
  if (ancestors.has(parent.id)) throw new Error(`${copy_error_doc_parent_cycle}: ${[...ancestors, parent.id].join(" → ")}`);
762
- if (JSON.stringify(parent.data.group) !== JSON.stringify(doc.data.group)) throw new Error(`${copy_error_doc_parent_group}: ${doc.id} → ${parent.id}`);
923
+ if (!isEqual(parent.data.group, doc.data.group)) throw new Error(`${copy_error_doc_parent_group}: ${doc.id} → ${parent.id}`);
763
924
  ancestors.add(parent.id);
764
925
  parentId = parent.data.parent;
765
926
  }
@@ -782,6 +943,10 @@ var readHome = async (options) => {
782
943
  if (paths.length > 1) throw new Error(`${copy_error_home_clash}: ${paths.join(", ")}`);
783
944
  if (paths.length === 0) return;
784
945
  const path = join(options.root, paths[0]);
946
+ if (options.scope !== void 0) assertContentPath({
947
+ scope: options.scope,
948
+ path
949
+ });
785
950
  const fh = {
786
951
  headings: [],
787
952
  sections: []
@@ -808,11 +973,13 @@ var readHome = async (options) => {
808
973
  var toCrawlingApp = (params) => {
809
974
  const app = new Hono();
810
975
  app.get(crawling_robots_path, (c) => c.text(`User-agent: *\nAllow: /\n\nSitemap: ${params.url}${crawling_sitemap_path}\n`));
811
- app.get(crawling_sitemap_path, (_c) => new Response(html`<?xml version="1.0" encoding="UTF-8"?>
976
+ app.get(crawling_sitemap_path, (_c) => {
977
+ return new Response(html`<?xml version="1.0" encoding="UTF-8"?>
812
978
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
813
979
  <url><loc>${params.url}/</loc></url>
814
980
  ${params.store.docs.map((doc) => html`<url><loc>${params.url}/${doc.id.split("/").map(encodeURIComponent).join("/")}/</loc></url>`)}
815
- </urlset>`.toString(), { headers: { "Content-Type": "application/xml; charset=utf-8" } }));
981
+ </urlset>`.toString(), { headers: { "Content-Type": "application/xml; charset=utf-8" } });
982
+ });
816
983
  return app;
817
984
  };
818
985
  //#endregion
@@ -863,6 +1030,14 @@ var toHttpApp = (options) => {
863
1030
  return app;
864
1031
  };
865
1032
  //#endregion
1033
+ //#region src/app/_function/to-page-app/_to-html.ts
1034
+ /**
1035
+ * Hono JSX 페이지의 HTML 문서 선언, 서버와 정적 출력의 공통 직렬화
1036
+ */
1037
+ var toHtml = (page) => {
1038
+ return html`<!DOCTYPE html>${page}`;
1039
+ };
1040
+ //#endregion
866
1041
  //#region src/component/widget/prose/wg-prose.tsx
867
1042
  var WgProse = (props) => {
868
1043
  if (props.html !== void 0) return /* @__PURE__ */ jsx("article", {
@@ -873,7 +1048,13 @@ var WgProse = (props) => {
873
1048
  className: clsx("wg_prose__root"),
874
1049
  children: props.head !== void 0 && /* @__PURE__ */ jsxs("header", {
875
1050
  className: clsx("wg_prose__head"),
876
- children: [/* @__PURE__ */ jsx("h1", { children: props.head.title }), props.head.lead !== void 0 && /* @__PURE__ */ jsx("p", { children: props.head.lead })]
1051
+ children: [/* @__PURE__ */ jsx("h1", {
1052
+ className: clsx("wg_prose__title"),
1053
+ children: props.head.title
1054
+ }), props.head.lead !== void 0 && /* @__PURE__ */ jsx("p", {
1055
+ className: clsx("wg_prose__lead"),
1056
+ children: props.head.lead
1057
+ })]
877
1058
  })
878
1059
  });
879
1060
  };
@@ -931,10 +1112,13 @@ var toDocGroupBranches = (options) => {
931
1112
  /**
932
1113
  * 메타데이터 경로로 읽는 순서를 가진 문서 계층 구성
933
1114
  *
934
- * - navigation의 첫 지정을 조상에도 적용. 미지정 형제 묶음은 이름순
1115
+ * - 같은 조상의 중복 지정은 최초 순서 유지
1116
+ * - 미지정 형제 묶음은 이름순
935
1117
  */
936
1118
  var toDocGroups = (options) => {
937
- const positions = new Map(options.navigation.flatMap((path, index) => path.map((_name, depth) => [JSON.stringify(path.slice(0, depth + 1)), index])).toReversed());
1119
+ const positions = new Map(uniqBy(options.navigation.flatMap((path, index) => {
1120
+ return path.map((_name, depth) => [JSON.stringify(path.slice(0, depth + 1)), index]);
1121
+ }), (entry) => entry[0]));
938
1122
  const children = Map.groupBy(options.docs, (doc) => doc.data.parent);
939
1123
  return toDocGroupBranches({
940
1124
  docs: options.docs,
@@ -992,15 +1176,23 @@ var toTocGroups = (outline) => {
992
1176
  heading,
993
1177
  section: outline.sections[index]
994
1178
  }));
995
- const sections = entries.flatMap((entry, index) => entry.heading.depth === section_heading_depth.section ? [{
996
- heading: entry.heading,
997
- part: entry.section.part,
998
- subs: takeWhile(entries.slice(index + 1), (sub) => sub.heading.depth !== section_heading_depth.section).map((sub) => ({ heading: sub.heading }))
999
- }] : []);
1000
- return sections.flatMap((section, index) => index === 0 || section.part !== void 0 ? [{
1001
- part: section.part,
1002
- sections: [section, ...takeWhile(sections.slice(index + 1), (next) => next.part === void 0)]
1003
- }] : []);
1179
+ const sections = entries.flatMap((entry, index) => {
1180
+ if (entry.heading.depth !== section_heading_depth.section) return [];
1181
+ return [{
1182
+ heading: entry.heading,
1183
+ part: entry.section.part,
1184
+ subs: takeWhile(entries.slice(index + 1), (sub) => sub.heading.depth !== section_heading_depth.section).map((sub) => {
1185
+ return { heading: sub.heading };
1186
+ })
1187
+ }];
1188
+ });
1189
+ return sections.flatMap((section, index) => {
1190
+ if (index > 0 && section.part === void 0) return [];
1191
+ return [{
1192
+ part: section.part,
1193
+ sections: [section, ...takeWhile(sections.slice(index + 1), (next) => next.part === void 0)]
1194
+ }];
1195
+ });
1004
1196
  };
1005
1197
  //#endregion
1006
1198
  //#region src/component/widget/navigation/_function/to-navigation-data/to-navigation-data.ts
@@ -1366,30 +1558,6 @@ var navigation_mobile_query = "(width < 1024px)";
1366
1558
  */
1367
1559
  var navigation_wide_query = "(width >= 1360px)";
1368
1560
  //#endregion
1369
- //#region src/component/widget/navigation/_function/restore-navigation-scroll.ts
1370
- /**
1371
- * 탐색 진입점에서 첫 paint 전 복원. 기존 배치별 저장 자료도 한 번 수용
1372
- */
1373
- var restoreNavigationScroll = (options) => {
1374
- const navigation = document.querySelector("[data-navigation]");
1375
- const documents = document.querySelector("[data-navigation-scroll=\"documents\"]");
1376
- const outline = document.querySelector("[data-navigation-scroll=\"outline\"]");
1377
- if (!navigation || !documents || !outline) return;
1378
- navigation.dataset.navigationLayout = options.layout;
1379
- try {
1380
- const value = sessionStorage.getItem(options.key);
1381
- const stored = value === null ? void 0 : JSON.parse(value);
1382
- const state = typeof stored === "object" && stored !== null && "version" in stored && stored.version === options.version && "state" in stored ? options.toState(stored.state) : { positions: {} };
1383
- const legacy = sessionStorage.getItem(`${options.key}:${options.layout === "drawer" ? "drawer" : "desktop"}`);
1384
- const previous = state.positions[options.layout] === void 0 && legacy !== null ? options.toState({ positions: { [options.layout]: JSON.parse(legacy) } }).positions[options.layout] : void 0;
1385
- const position = state.positions[options.layout] ?? previous;
1386
- const signature = [...navigation.querySelectorAll(".wg_navigationNav__root a[href]")].map((link) => `${link.pathname}:${link.textContent}`).join("\n");
1387
- if (position === void 0 || position.navigation !== signature) return;
1388
- documents.scrollTop = position.documents;
1389
- if (position.pathname === location.pathname) outline.scrollTop = position.outline;
1390
- } catch {}
1391
- };
1392
- //#endregion
1393
1561
  //#region src/component/widget/navigation/_wg-navigation-nav-list.tsx
1394
1562
  var WgNavigationNavList = (props) => {
1395
1563
  const entries = [...props.group.docs.map((doc) => ({
@@ -1404,24 +1572,26 @@ var WgNavigationNavList = (props) => {
1404
1572
  return /* @__PURE__ */ jsx("ul", {
1405
1573
  className: clsx("wg_navigationNavList__root"),
1406
1574
  "aria-label": props.group.name,
1407
- children: entries.map((entry) => /* @__PURE__ */ jsxs("li", {
1408
- className: clsx("wg_navigationNavList__item"),
1409
- children: [entry.kind === "document" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("a", {
1410
- className: clsx("wg_navigationNavList__link", { "wg_navigationNavList__link--active": entry.doc.active }),
1411
- href: `/${entry.doc.id}/`,
1412
- title: entry.doc.label,
1413
- "aria-current": entry.doc.active ? "page" : void 0,
1414
- children: entry.doc.name
1415
- }), entry.doc.children.length > 0 && /* @__PURE__ */ jsx(WgNavigationNavList, { group: {
1416
- name: entry.doc.name,
1417
- path: props.group.path,
1418
- docs: entry.doc.children,
1419
- groups: []
1420
- } })] }), entry.kind === "group" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1421
- className: clsx("wg_navigationNavList__branch"),
1422
- children: entry.group.name
1423
- }), /* @__PURE__ */ jsx(WgNavigationNavList, { group: entry.group })] })]
1424
- }, entry.key))
1575
+ children: entries.map((entry) => {
1576
+ return /* @__PURE__ */ jsxs("li", {
1577
+ className: clsx("wg_navigationNavList__item"),
1578
+ children: [entry.kind === "document" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("a", {
1579
+ className: clsx("wg_navigationNavList__link", { "wg_navigationNavList__link--active": entry.doc.active }),
1580
+ href: `/${entry.doc.id}/`,
1581
+ title: entry.doc.label,
1582
+ "aria-current": entry.doc.active ? "page" : void 0,
1583
+ children: entry.doc.name
1584
+ }), entry.doc.children.length > 0 && /* @__PURE__ */ jsx(WgNavigationNavList, { group: {
1585
+ name: entry.doc.name,
1586
+ path: props.group.path,
1587
+ docs: entry.doc.children,
1588
+ groups: []
1589
+ } })] }), entry.kind === "group" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1590
+ className: clsx("wg_navigationNavList__branch"),
1591
+ children: entry.group.name
1592
+ }), /* @__PURE__ */ jsx(WgNavigationNavList, { group: entry.group })] })]
1593
+ }, entry.key);
1594
+ })
1425
1595
  });
1426
1596
  };
1427
1597
  //#endregion
@@ -1432,14 +1602,16 @@ var WgNavigationNav = (props) => {
1432
1602
  "aria-label": copy_nav_aria_label,
1433
1603
  children: /* @__PURE__ */ jsx("div", {
1434
1604
  className: clsx("wg_navigationNav__docs"),
1435
- children: props.groups.map((group) => /* @__PURE__ */ jsxs("section", {
1436
- className: clsx("wg_navigationNav__docGroup"),
1437
- "aria-label": group.name,
1438
- children: [/* @__PURE__ */ jsx("div", {
1439
- className: clsx("wg_navigationNav__group"),
1440
- children: group.name
1441
- }), /* @__PURE__ */ jsx(WgNavigationNavList, { group })]
1442
- }, JSON.stringify(group.path)))
1605
+ children: props.groups.map((group) => {
1606
+ return /* @__PURE__ */ jsxs("section", {
1607
+ className: clsx("wg_navigationNav__docGroup"),
1608
+ "aria-label": group.name,
1609
+ children: [/* @__PURE__ */ jsx("div", {
1610
+ className: clsx("wg_navigationNav__group"),
1611
+ children: group.name
1612
+ }), /* @__PURE__ */ jsx(WgNavigationNavList, { group })]
1613
+ }, JSON.stringify(group.path));
1614
+ })
1443
1615
  })
1444
1616
  });
1445
1617
  };
@@ -1579,72 +1751,46 @@ var WgNavigationToc = (props) => {
1579
1751
  children: [/* @__PURE__ */ jsx("div", {
1580
1752
  className: clsx("wg_navigationToc__label"),
1581
1753
  children: copy_nav_toc_label
1582
- }), props.groups.map((group) => /* @__PURE__ */ jsxs("div", {
1583
- className: clsx("wg_navigationToc__groupSection", { "wg_navigationToc__groupSection--named": group.part !== void 0 }),
1584
- children: [group.part !== void 0 && /* @__PURE__ */ jsx("div", {
1585
- className: clsx("wg_navigationToc__group"),
1586
- children: group.part
1587
- }), /* @__PURE__ */ jsx("ul", {
1588
- className: clsx("wg_navigationToc__list"),
1589
- children: group.sections.map((section) => /* @__PURE__ */ jsxs("li", {
1590
- className: clsx("wg_navigationToc__item"),
1591
- children: [/* @__PURE__ */ jsx("a", {
1592
- className: clsx("wg_navigationToc__link", { "wg_navigationToc__link--active": section.heading.slug === currentSlug }),
1593
- href: `#${section.heading.slug}`,
1594
- "data-toc-link": "",
1595
- "aria-current": section.heading.slug === currentSlug ? "location" : void 0,
1596
- children: section.heading.text
1597
- }), section.subs.length > 0 && /* @__PURE__ */ jsx("ul", {
1598
- className: clsx("wg_navigationToc__sub"),
1599
- children: section.subs.map((sub) => /* @__PURE__ */ jsx("li", {
1600
- className: clsx("wg_navigationToc__subItem"),
1601
- children: /* @__PURE__ */ jsx("a", {
1602
- className: clsx("wg_navigationToc__subLink", { "wg_navigationToc__subLink--active": sub.heading.slug === currentSlug }),
1603
- href: `#${sub.heading.slug}`,
1604
- "data-toc-sub-link": "",
1605
- "aria-current": sub.heading.slug === currentSlug ? "location" : void 0,
1606
- children: sub.heading.text
1607
- })
1608
- }, sub.heading.slug))
1609
- })]
1610
- }, section.heading.slug))
1611
- })]
1612
- }, group.sections[0].heading.slug))]
1754
+ }), props.groups.map((group) => {
1755
+ return /* @__PURE__ */ jsxs("div", {
1756
+ className: clsx("wg_navigationToc__groupSection", { "wg_navigationToc__groupSection--named": group.part !== void 0 }),
1757
+ children: [group.part !== void 0 && /* @__PURE__ */ jsx("div", {
1758
+ className: clsx("wg_navigationToc__group"),
1759
+ children: group.part
1760
+ }), /* @__PURE__ */ jsx("ul", {
1761
+ className: clsx("wg_navigationToc__list"),
1762
+ children: group.sections.map((section) => {
1763
+ return /* @__PURE__ */ jsxs("li", {
1764
+ className: clsx("wg_navigationToc__item"),
1765
+ children: [/* @__PURE__ */ jsx("a", {
1766
+ className: clsx("wg_navigationToc__link", { "wg_navigationToc__link--active": section.heading.slug === currentSlug }),
1767
+ href: `#${section.heading.slug}`,
1768
+ "data-toc-link": "",
1769
+ "aria-current": section.heading.slug === currentSlug ? "location" : void 0,
1770
+ children: section.heading.text
1771
+ }), section.subs.length > 0 && /* @__PURE__ */ jsx("ul", {
1772
+ className: clsx("wg_navigationToc__sub"),
1773
+ children: section.subs.map((sub) => {
1774
+ return /* @__PURE__ */ jsx("li", {
1775
+ className: clsx("wg_navigationToc__subItem"),
1776
+ children: /* @__PURE__ */ jsx("a", {
1777
+ className: clsx("wg_navigationToc__subLink", { "wg_navigationToc__subLink--active": sub.heading.slug === currentSlug }),
1778
+ href: `#${sub.heading.slug}`,
1779
+ "data-toc-sub-link": "",
1780
+ "aria-current": sub.heading.slug === currentSlug ? "location" : void 0,
1781
+ children: sub.heading.text
1782
+ })
1783
+ }, sub.heading.slug);
1784
+ })
1785
+ })]
1786
+ }, section.heading.slug);
1787
+ })
1788
+ })]
1789
+ }, group.sections[0].heading.slug);
1790
+ })]
1613
1791
  });
1614
1792
  };
1615
1793
  //#endregion
1616
- //#region src/store/navigation/navigation-storage.ts
1617
- /**
1618
- * 같은 탭에서 문서 이동 후 탐색 위치 복원. 데스크톱과 드로어는 별도 저장
1619
- */
1620
- var navigation_scroll_storage_key = "for-humanity:navigation-scroll";
1621
- //#endregion
1622
- //#region src/store/navigation/to-navigation-scroll-state.ts
1623
- /**
1624
- * 알 수 없는 저장 형태, 음수, 무한 좌표 제외. 초기 HTML과 Zustand에 같은 검증 적용
1625
- */
1626
- var toNavigationScrollState = (value) => {
1627
- /**
1628
- * 한 배치의 완전한 좌표만 복원 대상으로 수용
1629
- */
1630
- const toPosition = (position) => {
1631
- if (typeof position !== "object" || position === null || !("navigation" in position) || typeof position.navigation !== "string" || !("pathname" in position) || typeof position.pathname !== "string" || !("documents" in position) || typeof position.documents !== "number" || !Number.isFinite(position.documents) || position.documents < 0 || !("outline" in position) || typeof position.outline !== "number" || !Number.isFinite(position.outline) || position.outline < 0) return;
1632
- return {
1633
- navigation: position.navigation,
1634
- pathname: position.pathname,
1635
- documents: position.documents,
1636
- outline: position.outline
1637
- };
1638
- };
1639
- if (typeof value !== "object" || value === null || !("positions" in value) || typeof value.positions !== "object" || value.positions === null) return { positions: {} };
1640
- const positions = {
1641
- wide: toPosition("wide" in value.positions ? value.positions.wide : void 0),
1642
- desktop: toPosition("desktop" in value.positions ? value.positions.desktop : void 0),
1643
- drawer: toPosition("drawer" in value.positions ? value.positions.drawer : void 0)
1644
- };
1645
- return { positions: Object.fromEntries(Object.entries(positions).filter((entry) => entry[1] !== void 0)) };
1646
- };
1647
- //#endregion
1648
1794
  //#region src/util/dom/reveal-hash-target.ts
1649
1795
  /**
1650
1796
  * 접힌 내용의 hash 대상과 상위 Details 공개
@@ -1678,6 +1824,7 @@ var WgNavigation = (props) => {
1678
1824
  const layoutRef = useRef(layout);
1679
1825
  const focusRef = useRef(null);
1680
1826
  const restorePendingRef = useRef(true);
1827
+ const navigationSignature = JSON.stringify(props.data.groups, (key, value) => key === "active" ? void 0 : value);
1681
1828
  /**
1682
1829
  * 저장된 배치와 현재 CSS 조건을 구분. 조건 변경 직후 잘린 좌표를 이전 배치에 저장하지 않음
1683
1830
  */
@@ -1699,31 +1846,23 @@ var WgNavigation = (props) => {
1699
1846
  return layoutRef.current === "drawer" ? drawerDocumentsRef.current : documentsRef.current;
1700
1847
  };
1701
1848
  /**
1702
- * 모든 링크의 URL, 표시명으로 사이트 공통 문서 목록의 동일성 판정
1703
- */
1704
- const getNavigationSignature = () => {
1705
- const navigation = getNavigationPort();
1706
- if (navigation === null) return void 0;
1707
- return [...navigation.querySelectorAll(".wg_navigationNav__root a[href]")].map((link) => `${link.pathname}:${link.textContent}`).join("\n");
1708
- };
1709
- /**
1710
1849
  * 닫기, 이동보다 먼저 실제 좌표 보존. 숨긴 모달과 아직 복원하지 않은 배치는 저장하지 않음
1711
1850
  */
1712
1851
  const saveScroll = () => {
1713
1852
  const documents = getDocumentsPort();
1714
- if (props.stores === void 0 || documents === null || outlineRef.current === null || restorePendingRef.current || layoutRef.current !== getNavigationLayout() || layoutRef.current === "drawer" && dialogRef.current?.open !== true) return;
1715
- const signature = getNavigationSignature();
1716
- if (signature === void 0) return;
1717
- const navigationPosition = {
1718
- navigation: signature,
1719
- pathname: location.pathname,
1720
- documents: documents.scrollTop,
1721
- outline: outlineRef.current.scrollTop
1722
- };
1723
- props.stores.scroll.setState((state) => ({ positions: {
1724
- ...state.positions,
1725
- [layoutRef.current]: navigationPosition
1726
- } }));
1853
+ const outline = outlineRef.current;
1854
+ if (props.stores === void 0 || documents === null || outline === null || restorePendingRef.current || layoutRef.current !== getNavigationLayout() || layoutRef.current === "drawer" && dialogRef.current?.open !== true) return;
1855
+ props.stores.scroll.setState((state) => {
1856
+ return { positions: {
1857
+ ...state.positions,
1858
+ [layoutRef.current]: {
1859
+ navigation: navigationSignature,
1860
+ pathname: location.pathname,
1861
+ documents: documents.scrollTop,
1862
+ outline: outline.scrollTop
1863
+ }
1864
+ } };
1865
+ });
1727
1866
  };
1728
1867
  /**
1729
1868
  * 연결된 영역에서 첫 paint 전 복원. 문서는 페이지 간 공유하고 목차는 같은 경로에서만 재사용
@@ -1731,25 +1870,16 @@ var WgNavigation = (props) => {
1731
1870
  const restoreScroll = () => {
1732
1871
  layoutRef.current = getNavigationLayout();
1733
1872
  const documents = getDocumentsPort();
1734
- const navigation = getNavigationPort();
1735
- if (props.stores === void 0 || documents === null || outlineRef.current === null || navigation === null) return;
1873
+ if (props.stores === void 0 || documents === null || outlineRef.current === null) return;
1736
1874
  if (layoutRef.current === "drawer" && dialogRef.current?.open !== true) return;
1737
- const signature = getNavigationSignature();
1738
- if (signature === void 0) return;
1739
1875
  const position = props.stores.scroll.getState().positions[layoutRef.current];
1740
- documents.scrollTop = 0;
1741
- outlineRef.current.scrollTop = 0;
1742
- if (position === void 0) restoreNavigationScroll({
1743
- key: navigation_scroll_storage_key,
1744
- version: 1,
1745
- layout: layoutRef.current,
1746
- toState: toNavigationScrollState
1747
- });
1748
- else if (position.navigation === signature) {
1876
+ if (position?.navigation === navigationSignature) {
1749
1877
  documents.scrollTop = position.documents;
1750
- if (position.pathname === location.pathname) outlineRef.current.scrollTop = position.outline;
1878
+ outlineRef.current.scrollTop = position.pathname === location.pathname ? position.outline : 0;
1879
+ } else {
1880
+ documents.scrollTop = 0;
1881
+ outlineRef.current.scrollTop = 0;
1751
1882
  }
1752
- navigation.dataset.navigationLayout = layoutRef.current;
1753
1883
  restorePendingRef.current = false;
1754
1884
  saveScroll();
1755
1885
  documents.dispatchEvent(new Event("scroll"));
@@ -1834,12 +1964,12 @@ var WgNavigation = (props) => {
1834
1964
  flushSync(() => setLayout(nextLayout));
1835
1965
  };
1836
1966
  /**
1837
- * BFCache 복귀 시 저장 좌표로 동기 렌더 후 복원
1967
+ * BFCache 복귀 시 persist의 최신 저장 좌표를 다시 읽어 복원
1838
1968
  */
1839
1969
  const handlePageShow = (event) => {
1840
1970
  if (!event.persisted || props.stores === void 0) return;
1841
1971
  restorePendingRef.current = true;
1842
- flushSync(() => props.stores?.scroll.persist.rehydrate());
1972
+ props.stores.scroll.persist.rehydrate();
1843
1973
  handleLayoutChange();
1844
1974
  restoreScroll();
1845
1975
  };
@@ -1921,6 +2051,7 @@ var WgNavigation = (props) => {
1921
2051
  ref: props.stores === void 0 ? void 0 : navigationRef,
1922
2052
  className: clsx("wg_navigation__navigation"),
1923
2053
  "data-navigation": "",
2054
+ "data-navigation-layout": layout,
1924
2055
  children: /* @__PURE__ */ jsx(WgNavigationScroll, {
1925
2056
  portRef: documentsRef,
1926
2057
  kind: "documents",
@@ -1983,6 +2114,7 @@ var WgNavigation = (props) => {
1983
2114
  ref: props.stores === void 0 ? void 0 : drawerNavigationRef,
1984
2115
  className: clsx("wg_navigation__navigation"),
1985
2116
  "data-navigation": "",
2117
+ "data-navigation-layout": layout,
1986
2118
  children: /* @__PURE__ */ jsx(WgNavigationScroll, {
1987
2119
  portRef: drawerDocumentsRef,
1988
2120
  kind: "documents",
@@ -2224,25 +2356,19 @@ var PgDoc = (props) => {
2224
2356
  //#endregion
2225
2357
  //#region src/page/home/pg-home.tsx
2226
2358
  var PgHome = (props) => {
2227
- return /* @__PURE__ */ jsx(WgShell, {
2359
+ return /* @__PURE__ */ jsxs(WgShell, {
2228
2360
  site: props.site,
2229
2361
  docs: props.docs,
2230
2362
  assets: props.assets,
2231
2363
  outline: props.home?.outline,
2232
- children: props.home === void 0 ? /* @__PURE__ */ jsx(WgProse, { head: {
2364
+ children: [props.home !== void 0 && /* @__PURE__ */ jsx(WgProse, { html: props.home.html }), props.home === void 0 && /* @__PURE__ */ jsx(WgProse, { head: {
2233
2365
  title: props.site.title,
2234
- lead: copy_home_missing
2235
- } }) : /* @__PURE__ */ jsx(WgProse, { html: props.home.html })
2366
+ lead: "문서 폴더에 README.md를 추가하면 이곳에 홈 화면으로 표시됩니다."
2367
+ } })]
2236
2368
  });
2237
2369
  };
2238
2370
  //#endregion
2239
- //#region src/app/_function/to-page-app.tsx
2240
- /**
2241
- * Hono JSX 페이지에 HTML 문서 선언 추가. 서버와 정적 출력에서 같은 렌더러 사용
2242
- */
2243
- var toHtml = (page) => {
2244
- return html`<!DOCTYPE html>${page}`;
2245
- };
2371
+ //#region src/app/_function/to-page-app/to-page-app.tsx
2246
2372
  /**
2247
2373
  * 첫 화면 /. 문서 /:slug{.+}/. 하위 폴더 id 지원
2248
2374
  *
@@ -2278,14 +2404,16 @@ var toPageApp = (options) => {
2278
2404
  var toFontCss = (options) => {
2279
2405
  const files = /* @__PURE__ */ new Map();
2280
2406
  return {
2281
- css: [...readFileSync(options.css, "utf8").matchAll(/@font-face\s*\{[^}]*\}/g)].map((match) => match[0].replace(/font-display:\s*[^;]+;/g, `font-display: ${font_display_strategy};`).replace(/url\(([^)]+)\)/g, (_match, relativeUrl) => {
2282
- const file = join(dirname(options.css), relativeUrl);
2283
- const extension = extname(file);
2284
- const hash = createHash("sha256").update(readFileSync(file)).digest("hex").slice(0, 12);
2285
- const url = `${options.fontDir}/${basename(file, extension)}.${hash}${extension}`;
2286
- files.set(url, file);
2287
- return `url(${url})`;
2288
- })).join("\n"),
2407
+ css: [...readFileSync(options.css, "utf8").matchAll(/@font-face\s*\{[^}]*\}/g)].map((match) => {
2408
+ return match[0].replace(/font-display:\s*[^;]+;/g, `font-display: ${font_display_strategy};`).replace(/url\(([^)]+)\)/g, (_match, relativeUrl) => {
2409
+ const file = join(dirname(options.css), relativeUrl);
2410
+ const extension = extname(file);
2411
+ const hash = createHash("sha256").update(readFileSync(file)).digest("hex").slice(0, 12);
2412
+ const url = `${options.fontDir}/${basename(file, extension)}.${hash}${extension}`;
2413
+ files.set(url, file);
2414
+ return `url(${url})`;
2415
+ });
2416
+ }).join("\n"),
2289
2417
  files
2290
2418
  };
2291
2419
  };
@@ -2321,18 +2449,26 @@ var loadSite = async (params) => {
2321
2449
  ...brand.files
2322
2450
  ]);
2323
2451
  if (params.stylePaths === void 0) files.set(stylePaths[0], join(params.kitRoot, "dist/cli.css"));
2452
+ const scope = readContentScope({
2453
+ root: params.root,
2454
+ content: params.site.content,
2455
+ includePrivate: params.includePrivate === true
2456
+ });
2324
2457
  const processor = createProcessor({
2325
2458
  site: params.site,
2326
- root: params.root,
2459
+ root: scope.root,
2327
2460
  files,
2328
- mermaid: loadMermaidAssets(params.kitRoot)
2461
+ mermaid: loadMermaidAssets(params.kitRoot),
2462
+ scope
2329
2463
  });
2330
2464
  const [docs, home] = await Promise.all([readDocs({
2331
- root: params.root,
2465
+ root: scope.root,
2466
+ scope,
2332
2467
  processor,
2333
2468
  reserved: params.site.url === void 0 ? void 0 : crawling_reserved_roots
2334
2469
  }), readHome({
2335
- root: params.root,
2470
+ root: scope.root,
2471
+ scope,
2336
2472
  processor
2337
2473
  })]);
2338
2474
  const store = {