for-humanity 0.13.1 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { S as copy_nav_toc_label, T as copy_warn_unknown_status, a as copy_error_doc_id_clash, b as copy_nav_open, c as copy_error_doc_parent_missing, g as copy_error_unknown_directive, h as copy_error_snippet, i as copy_error_doc_id, l as copy_error_frontmatter, o as copy_error_doc_parent_cycle, r as copy_error_directive_syntax, s as copy_error_doc_parent_group, t as copy_error_asset, u as copy_error_home_clash, v as copy_nav_aria_label, w as copy_warn_broken_link, x as copy_nav_title, y as copy_nav_close } from "./copy-C1uzvEnr.js";
2
- import { t as logger } from "./logger-Dv1i9xSV.js";
2
+ import { t as logger } from "./logger-CLH67K3L.js";
3
3
  import { t as groupPathSchema } from "./group-path-XAThmzvC.js";
4
4
  import { t as toChangeQueue } from "./to-change-queue-DbEnZGNK.js";
5
5
  import { readFile, readdir } from "node:fs/promises";
@@ -21,7 +21,7 @@ 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
+ import { dropRightWhile, dropWhile, minBy, takeWhile, uniqBy, zip } from "es-toolkit/array";
25
25
  import { transformerMetaHighlight, transformerNotationDiff, transformerNotationErrorLevel, transformerNotationHighlight, transformerNotationWordHighlight } from "@shikijs/transformers";
26
26
  import { toString as toString$1 } from "mdast-util-to-string";
27
27
  import { escape, escapeRegExp } from "es-toolkit/string";
@@ -106,6 +106,23 @@ var asset_media_extensions = /* @__PURE__ */ new Set([
106
106
  ".zip"
107
107
  ]);
108
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
109
126
  //#region src/util/file/to-asset-path.ts
110
127
  /**
111
128
  * 파일명에 내용 지문 추가. 같은 내용은 같은 URL, 변경한 내용은 새 캐시 키 사용
@@ -137,6 +154,14 @@ var rehypeAssets = (options) => (tree, file) => {
137
154
  return;
138
155
  }
139
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
+ }
140
165
  const location = relative(options.root, target);
141
166
  if (location === ".." || location.startsWith(`..${sep}`) || isAbsolute(location)) file.fail(`${copy_error_asset}: 문서 폴더 밖 · ${url}`, node);
142
167
  if (!existsSync(target)) file.fail(`${copy_error_asset}: 파일이 없다 · ${url}`, node);
@@ -152,6 +177,68 @@ var rehypeAssets = (options) => (tree, file) => {
152
177
  });
153
178
  };
154
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
+ for (const url of property === "srcSet" ? value.split(",").map((entry) => entry.trim().split(/\s+/)[0]) : [value]) {
214
+ if (/^(?:[a-z][a-z\d+.-]*:|\/\/|#)/i.test(url)) continue;
215
+ const path = decodeURIComponent(url.split(/[?#]/)[0]);
216
+ try {
217
+ if (path.startsWith("/")) {
218
+ const pathname = new URL(path, "https://content.invalid").pathname.toLowerCase();
219
+ if (!scope.includePrivate && (pathname === "/private" || pathname.startsWith("/private/"))) throw new Error(`공개 문서에서 private URL을 참조할 수 없습니다: ${url}`);
220
+ continue;
221
+ }
222
+ const target = resolve(dirname(file.path), path);
223
+ assertContentPath({
224
+ scope,
225
+ path: target
226
+ });
227
+ if (property === "href" && /\.mdx?$/i.test(path)) {
228
+ const id = toContentId({
229
+ scope,
230
+ path: target
231
+ });
232
+ node.properties[property] = `${id === "" ? "/" : `/${id}/`}${url.slice(url.split(/[?#]/)[0].length)}`;
233
+ }
234
+ } catch (error) {
235
+ file.fail(error instanceof Error ? error.message : String(error), node);
236
+ }
237
+ }
238
+ }
239
+ });
240
+ };
241
+ //#endregion
155
242
  //#region src/app/_function/create-processor/_rehype-heading-ids.ts
156
243
  /**
157
244
  * 제목 id와 목차 수집. 같은 제목은 -1, -2 접미사
@@ -174,7 +261,7 @@ var rehypeHeadingIds = () => (tree, file) => {
174
261
  node.children = [{
175
262
  type: "element",
176
263
  tagName: "span",
177
- properties: { className: ["wg_prose__headingText"] },
264
+ properties: { className: ["wg_article__headingText"] },
178
265
  children: node.children
179
266
  }];
180
267
  });
@@ -221,11 +308,10 @@ var snippet_language_by_extension = {
221
308
  * - 빈 region은 빈 문자열, 없는 region은 undefined 반환
222
309
  */
223
310
  var toSnippetBody = (source, region) => {
224
- const lines = source.replace(/\r\n/g, "\n").split("\n");
225
311
  let inside = region === void 0;
226
312
  let found = region === void 0;
227
313
  const kept = [];
228
- for (const line of lines) {
314
+ for (const line of source.replace(/\r\n/g, "\n").split("\n")) {
229
315
  const marker = snippet_region_pattern.exec(line);
230
316
  if (marker !== null) {
231
317
  if (region !== void 0 && marker[2] === region) {
@@ -259,6 +345,14 @@ var remarkSnippets = (options) => (tree, file) => {
259
345
  if (match === null) return;
260
346
  const [, path = "", region, meta] = match;
261
347
  const target = resolve(dirname(file.path), path);
348
+ if (options.scope !== void 0) try {
349
+ assertContentPath({
350
+ scope: options.scope,
351
+ path: target
352
+ });
353
+ } catch (error) {
354
+ file.fail(error instanceof Error ? error.message : String(error), node);
355
+ }
262
356
  const location = relative(options.root, target);
263
357
  if (location === ".." || location.startsWith(`..${sep}`) || isAbsolute(location)) file.fail(`${copy_error_snippet}: 문서 폴더 밖 · ${path}`, node);
264
358
  if (!existsSync(target)) file.fail(`${copy_error_snippet}: 파일이 없다 · ${path}`, node);
@@ -271,10 +365,9 @@ var remarkSnippets = (options) => (tree, file) => {
271
365
  return;
272
366
  }
273
367
  const extension = extname(actual).toLowerCase();
274
- const lang = snippet_language_by_extension[extension] === void 0 ? extension.slice(1) : snippet_language_by_extension[extension];
275
368
  parent.children.splice(index, 1, {
276
369
  type: "code",
277
- lang,
370
+ lang: snippet_language_by_extension[extension] === void 0 ? extension.slice(1) : snippet_language_by_extension[extension],
278
371
  meta: meta === void 0 ? null : meta,
279
372
  value: body
280
373
  });
@@ -293,26 +386,26 @@ var shikiTransformers = [
293
386
  name: "for-humanity:code-metadata",
294
387
  root(root) {
295
388
  this.pre.properties.style = void 0;
296
- this.addClassToHast(this.pre, "wg_prose__code");
389
+ this.addClassToHast(this.pre, "wg_article__code");
297
390
  if (this.options.meta?.__raw === void 0) return;
298
- if (/\bline-numbers\b/.test(this.options.meta.__raw)) this.addClassToHast(this.pre, "wg_prose__lineNumbers");
391
+ if (/\bline-numbers\b/.test(this.options.meta.__raw)) this.addClassToHast(this.pre, "wg_article__lineNumbers");
299
392
  const isBad = /\bbad\b/.test(this.options.meta.__raw);
300
393
  const isGood = /\bgood\b/.test(this.options.meta.__raw);
301
- if (isBad) this.addClassToHast(this.pre, "wg_prose__code--bad");
302
- if (isGood) this.addClassToHast(this.pre, "wg_prose__code--good");
394
+ if (isBad) this.addClassToHast(this.pre, "wg_article__code--bad");
395
+ if (isGood) this.addClassToHast(this.pre, "wg_article__code--good");
303
396
  const title = /\[([^\]]+)\]/.exec(this.options.meta.__raw)?.[1];
304
397
  if (title === void 0) return;
305
398
  root.children = [{
306
399
  type: "element",
307
400
  tagName: "figure",
308
- properties: { className: ["wg_prose__codeFile"] },
401
+ properties: { className: ["wg_article__codeFile"] },
309
402
  children: [{
310
403
  type: "element",
311
404
  tagName: "figcaption",
312
405
  properties: { className: [
313
- "wg_prose__codeFileTitle",
314
- ...isBad ? ["wg_prose__codeFileTitle--bad"] : [],
315
- ...isGood ? ["wg_prose__codeFileTitle--good"] : []
406
+ "wg_article__codeFileTitle",
407
+ ...isBad ? ["wg_article__codeFileTitle--bad"] : [],
408
+ ...isGood ? ["wg_article__codeFileTitle--good"] : []
316
409
  ] },
317
410
  children: [{
318
411
  type: "text",
@@ -320,7 +413,7 @@ var shikiTransformers = [
320
413
  }, {
321
414
  type: "element",
322
415
  tagName: "span",
323
- properties: { className: ["wg_prose__codeFileLang"] },
416
+ properties: { className: ["wg_article__codeFileLang"] },
324
417
  children: [{
325
418
  type: "text",
326
419
  value: this.options.lang
@@ -332,22 +425,7 @@ var shikiTransformers = [
332
425
  }
333
426
  ];
334
427
  //#endregion
335
- //#region src/component/widget/mermaid/_function/remark-mermaid.ts
336
- /**
337
- * Mermaid 원문과 브라우저 렌더 영역. JavaScript 비활성 환경에서도 원문 유지
338
- */
339
- var remarkMermaid = (options) => (tree) => {
340
- visit(tree, "code", (node, index, parent) => {
341
- if (node.lang !== "mermaid" || parent === void 0 || index === void 0) return;
342
- if (options.assets !== void 0) for (const [url, path] of options.assets.files) options.files.set(url, path);
343
- parent.children[index] = {
344
- type: "html",
345
- value: `<figure class="wg_mermaid__root" data-mermaid${options.assets === void 0 ? "" : ` data-mermaid-module="${options.assets.entry}"`}><div class="wg_mermaid__result" hidden></div><pre class="wg_mermaid__source"><code data-mermaid-source>${node.value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")}</code></pre><p class="wg_mermaid__error" role="status" hidden></p></figure>`
346
- };
347
- });
348
- };
349
- //#endregion
350
- //#region src/component/widget/prose/_constant/section.ts
428
+ //#region src/component/widget/article/_constant/section.ts
351
429
  /**
352
430
  * 목차 대상 제목 깊이. h2 절, h3 소제목
353
431
  */
@@ -356,7 +434,7 @@ var section_heading_depth = {
356
434
  sub: 3
357
435
  };
358
436
  //#endregion
359
- //#region src/component/widget/prose/_function/rehype-head.ts
437
+ //#region src/component/widget/article/_function/rehype-head.ts
360
438
  /**
361
439
  * name 제목과 첫 절 앞 본문의 공통 머리. README는 원문의 h1 사용
362
440
  */
@@ -366,13 +444,13 @@ var rehypeHead = () => (tree, file) => {
366
444
  const head = {
367
445
  type: "element",
368
446
  tagName: "header",
369
- properties: { className: ["wg_prose__head"] },
447
+ properties: { className: ["wg_article__head"] },
370
448
  children: tree.children.slice(0, leadEnd).filter((node) => node.type !== "doctype")
371
449
  };
372
450
  if (file.data.fh?.frontmatter !== void 0) head.children.unshift({
373
451
  type: "element",
374
452
  tagName: "h1",
375
- properties: { className: ["wg_prose__title"] },
453
+ properties: { className: ["wg_article__title"] },
376
454
  children: [{
377
455
  type: "text",
378
456
  value: file.data.fh.frontmatter.name
@@ -382,7 +460,7 @@ var rehypeHead = () => (tree, file) => {
382
460
  tree.children = [head, ...tree.children.slice(leadEnd)];
383
461
  };
384
462
  //#endregion
385
- //#region src/component/widget/prose/_function/rehype-sections/_to-section-headings.ts
463
+ //#region src/component/widget/article/_function/rehype-sections/_to-section-headings.ts
386
464
  /**
387
465
  * 목차 대상 h2, h3. 중첩 블록까지 문서 순서 수집
388
466
  *
@@ -396,7 +474,7 @@ var toSectionHeadings = (node) => {
396
474
  });
397
475
  };
398
476
  //#endregion
399
- //#region src/component/widget/prose/_function/rehype-sections/rehype-sections.ts
477
+ //#region src/component/widget/article/_function/rehype-sections/rehype-sections.ts
400
478
  /**
401
479
  * h2, h3의 가름 메타데이터 수집. 작성한 제목은 그대로 유지
402
480
  *
@@ -410,7 +488,7 @@ var rehypeSections = () => (tree, file) => {
410
488
  });
411
489
  };
412
490
  //#endregion
413
- //#region src/component/widget/prose/_function/rehype-tables.ts
491
+ //#region src/component/widget/article/_function/rehype-tables.ts
414
492
  /**
415
493
  * 넓은 표의 내부 가로 스크롤 래퍼
416
494
  */
@@ -420,14 +498,14 @@ var rehypeTables = () => (tree) => {
420
498
  parent.children[index] = {
421
499
  type: "element",
422
500
  tagName: "div",
423
- properties: { className: ["wg_prose__table"] },
501
+ properties: { className: ["wg_article__table"] },
424
502
  children: [node]
425
503
  };
426
504
  return [SKIP, index + 1];
427
505
  });
428
506
  };
429
507
  //#endregion
430
- //#region src/component/widget/prose/_function/has-block-content.ts
508
+ //#region src/component/widget/article/_function/has-block-content.ts
431
509
  /**
432
510
  * 부품 본문의 내용 판정. 참조 정의, 주석, 빈 텍스트와 빈 구조 제외
433
511
  *
@@ -442,7 +520,7 @@ var hasBlockContent = (node) => {
442
520
  return true;
443
521
  };
444
522
  //#endregion
445
- //#region src/component/widget/prose/_function/remark-blocks.ts
523
+ //#region src/component/widget/article/_function/remark-blocks.ts
446
524
  /**
447
525
  * Note, Details의 제목, 본문, 속성, 닫힘 계약
448
526
  *
@@ -462,7 +540,6 @@ var remarkBlocks = () => (tree, file) => {
462
540
  if (!node.children.slice(1).some(hasBlockContent)) file.fail(`${copy_error_directive_syntax}: ${node.name} · 본문 필수`, node);
463
541
  const unsupported = node.attributes ? Object.keys(node.attributes).find((key) => node.name !== "details" || key !== "open") : void 0;
464
542
  if (unsupported !== void 0) file.fail(`${copy_error_directive_syntax}: ${node.name} · 미지원 속성 ${unsupported}`, node);
465
- const open = node.attributes?.open;
466
543
  const attributeSource = source.slice(label.position?.end.offset, node.position?.end.offset).split(/\r?\n/, 1)[0].trim();
467
544
  if (attributeSource !== "" && (node.name !== "details" || !/^\{[ \t]*open[ \t]*\}$/.test(attributeSource))) file.fail(`${copy_error_directive_syntax}: ${node.name} · 허용 속성은 Details의 {open}, 나머지는 생략`, node);
468
545
  const original = source.slice(node.position?.start.offset, node.position?.end.offset);
@@ -485,23 +562,23 @@ var remarkBlocks = () => (tree, file) => {
485
562
  label.data = {
486
563
  ...label.data,
487
564
  hName: node.name === "note" ? "strong" : "summary",
488
- hProperties: { className: [node.name === "note" ? "wg_prose__noteTitle" : "wg_prose__detailsSummary"] }
565
+ hProperties: { className: [node.name === "note" ? "wg_article__noteTitle" : "wg_article__detailsSummary"] }
489
566
  };
490
567
  node.data = {
491
568
  ...node.data,
492
569
  hName: node.name === "note" ? "aside" : "details",
493
570
  hProperties: node.name === "note" ? {
494
- className: ["wg_prose__note"],
571
+ className: ["wg_article__note"],
495
572
  ariaLabel: title
496
573
  } : {
497
- className: ["wg_prose__details"],
498
- open: open === ""
574
+ className: ["wg_article__details"],
575
+ open: node.attributes?.open === ""
499
576
  }
500
577
  };
501
578
  });
502
579
  };
503
580
  //#endregion
504
- //#region src/component/widget/prose/_function/remark-links.ts
581
+ //#region src/component/widget/article/_function/remark-links.ts
505
582
  /**
506
583
  * 상대 Markdown 링크의 사이트 URL 변환, query와 hash 원문 유지
507
584
  *
@@ -513,6 +590,24 @@ var remarkLinks = (options) => (tree, file) => {
513
590
  if (node.type !== "link" && node.type !== "definition") return;
514
591
  const match = /^(?![a-z]+:|\/|#)([^#?]+)(\.mdx?)([?#].*|)$/i.exec(node.url);
515
592
  if (match) {
593
+ if (options.scope !== void 0) {
594
+ const target = resolve(dirname(file.path), decodeURIComponent(`${match[1]}${match[2]}`));
595
+ try {
596
+ assertContentPath({
597
+ scope: options.scope,
598
+ path: target
599
+ });
600
+ const id = toContentId({
601
+ scope: options.scope,
602
+ path: target
603
+ });
604
+ if (!existsSync(target)) file.message(`${copy_warn_broken_link}: ${node.url}`, node);
605
+ node.url = `${id === "" ? "/" : `/${id}/`}${match[3]}`;
606
+ } catch (error) {
607
+ file.fail(error instanceof Error ? error.message : String(error), node);
608
+ }
609
+ return;
610
+ }
516
611
  if (!existsSync(resolve(dirname(file.path), `${match[1]}${match[2]}`))) file.message(`${copy_warn_broken_link}: ${node.url}`, node);
517
612
  const target = posix.join(here, `${match[1]}${match[2]}`).toLowerCase();
518
613
  node.url = target === "readme.md" ? `/${match[3]}` : `/${target.slice(0, -match[2].length)}/${match[3]}`;
@@ -520,7 +615,7 @@ var remarkLinks = (options) => (tree, file) => {
520
615
  });
521
616
  };
522
617
  //#endregion
523
- //#region src/component/widget/prose/_function/remark-parts.ts
618
+ //#region src/component/widget/article/_function/remark-parts.ts
524
619
  /**
525
620
  * ::part[이름]의 본문 가름과 다음 h2의 data-part
526
621
  *
@@ -532,7 +627,7 @@ var remarkParts = () => (tree) => {
532
627
  const section = tree.children.slice(index + 1).find((next) => next.type === "heading" && next.depth === section_heading_depth.section);
533
628
  node.data = {
534
629
  hName: "div",
535
- hProperties: { className: ["wg_prose__part"] }
630
+ hProperties: { className: ["wg_article__part"] }
536
631
  };
537
632
  if (section) section.data = {
538
633
  ...section.data,
@@ -544,7 +639,7 @@ var remarkParts = () => (tree) => {
544
639
  }
545
640
  };
546
641
  //#endregion
547
- //#region src/component/widget/prose/_constant/status.ts
642
+ //#region src/component/widget/article/_constant/status.ts
548
643
  /**
549
644
  * 상태 문구 뒤 날짜 패턴. YYYY-MM-DD
550
645
  */
@@ -554,7 +649,7 @@ var status_date_pattern = String.raw`\d{4}-\d{2}-\d{2}`;
554
649
  */
555
650
  var status_candidate_pattern = new RegExp(String.raw`\([^()\n]*${status_date_pattern}\)`, "g");
556
651
  //#endregion
557
- //#region src/component/widget/prose/_function/remark-status.ts
652
+ //#region src/component/widget/article/_function/remark-status.ts
558
653
  /**
559
654
  * 설정 문구의 상태 표지 변환. 괄호 제거. 제목, 링크 제외
560
655
  *
@@ -576,7 +671,7 @@ var remarkStatus = (options) => {
576
671
  if (text === void 0 || item === void 0) return false;
577
672
  return {
578
673
  type: "html",
579
- value: `<span class="wg_prose__pill wg_prose__pill--${item.kind}">${escape(text)}</span>`
674
+ value: `<span class="wg_article__pill wg_article__pill--${item.kind}">${escape(text)}</span>`
580
675
  };
581
676
  }], { ignore });
582
677
  findAndReplace(tree, [status_candidate_pattern, (match, info) => {
@@ -586,22 +681,22 @@ var remarkStatus = (options) => {
586
681
  };
587
682
  };
588
683
  //#endregion
589
- //#region src/component/widget/prose/_function/remark-swatch.ts
684
+ //#region src/component/widget/article/_function/remark-swatch.ts
590
685
  /**
591
686
  * 인라인 hex 색 값의 칩 표시
592
687
  *
593
- * - 문서별 색은 --wg-prose-chip-color로 전달. 고정 스타일시트 값 불가
688
+ * - 문서별 색은 --wg-article-chip-color로 전달. 고정 스타일시트 값 불가
594
689
  */
595
690
  var remarkSwatch = () => (tree) => {
596
691
  visit(tree, "inlineCode", (node) => {
597
692
  if (/^#[0-9a-f]{3,8}$/i.test(node.value.trim())) node.data = { hProperties: {
598
- className: ["wg_prose__color"],
599
- style: `--wg-prose-chip-color: ${node.value.trim()}`
693
+ className: ["wg_article__color"],
694
+ style: `--wg-article-chip-color: ${node.value.trim()}`
600
695
  } };
601
696
  });
602
697
  };
603
698
  //#endregion
604
- //#region src/component/widget/prose/_function/remark-unknown-directives.ts
699
+ //#region src/component/widget/article/_function/remark-unknown-directives.ts
605
700
  /**
606
701
  * hName 없는 블록 지시문은 작성 오류
607
702
  *
@@ -620,28 +715,50 @@ var remarkUnknownDirectives = () => (tree, file) => {
620
715
  });
621
716
  };
622
717
  //#endregion
718
+ //#region src/component/widget/mermaid/_function/remark-mermaid.ts
719
+ /**
720
+ * Mermaid 원문과 브라우저 렌더 영역. JavaScript 비활성 환경에서도 원문 유지
721
+ */
722
+ var remarkMermaid = (options) => (tree) => {
723
+ visit(tree, "code", (node, index, parent) => {
724
+ if (node.lang !== "mermaid" || parent === void 0 || index === void 0) return;
725
+ if (options.assets !== void 0) for (const [url, path] of options.assets.files) options.files.set(url, path);
726
+ parent.children[index] = {
727
+ type: "html",
728
+ value: `<figure class="wg_mermaid__root" data-mermaid${options.assets === void 0 ? "" : ` data-mermaid-module="${options.assets.entry}"`}><div class="wg_mermaid__result" hidden></div><pre class="wg_mermaid__source"><code data-mermaid-source>${node.value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;")}</code></pre><p class="wg_mermaid__error" role="status" hidden></p></figure>`
729
+ };
730
+ });
731
+ };
732
+ //#endregion
623
733
  //#region src/app/_function/create-processor/create-processor.ts
624
734
  /**
625
735
  * Markdown 변환과 문서 메타데이터 수집
626
736
  *
627
737
  * - 코드와 문서 머리 생성 후 제목 ID와 가름 수집
628
738
  * - 플러그인 HTML 조각은 마지막 rehype-raw에서 해석
629
- * - 코드 상자의 색상은 prose CSS 소유, Shiki 토큰 색상은 --app-code-* 참조
739
+ * - 코드 상자의 색상은 article CSS 소유, Shiki 토큰 색상은 --app-code-* 참조
630
740
  */
631
741
  var createProcessor = (options) => {
632
742
  const files = options.files ?? /* @__PURE__ */ new Map();
633
- return unified().use(remarkParse).use(remarkFrontmatter, ["yaml"]).use(remarkGfm).use(remarkSnippets, { root: options.root }).use(remarkSmartypants).use(remarkDirective).use(remarkBlocks).use(remarkParts).use(remarkMermaid, {
743
+ return unified().use(remarkParse).use(remarkFrontmatter, ["yaml"]).use(remarkGfm).use(remarkSnippets, {
744
+ root: options.root,
745
+ scope: options.scope
746
+ }).use(remarkSmartypants).use(remarkDirective).use(remarkBlocks).use(remarkParts).use(remarkMermaid, {
634
747
  assets: options.mermaid,
635
748
  files
636
- }).use(remarkStatus, { status: options.site.status }).use(remarkSwatch).use(remarkLinks, { root: options.root }).use(remarkUnknownDirectives).use(remarkReport, { root: options.root }).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeShiki, {
749
+ }).use(remarkStatus, { status: options.site.status }).use(remarkSwatch).use(remarkLinks, {
750
+ root: options.root,
751
+ scope: options.scope
752
+ }).use(remarkUnknownDirectives).use(remarkReport, { root: options.root }).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeShiki, {
637
753
  theme: createCssVariablesTheme({
638
754
  name: "for-humanity",
639
755
  variablePrefix: "--app-code-"
640
756
  }),
641
757
  transformers: shikiTransformers
642
- }).use(rehypeHead).use(rehypeHeadingIds).use(rehypeSections).use(rehypeTables).use(rehypeRaw).use(rehypeAssets, {
758
+ }).use(rehypeHead).use(rehypeHeadingIds).use(rehypeSections).use(rehypeTables).use(rehypeRaw).use(rehypeContentLinks, { scope: options.scope }).use(rehypeAssets, {
643
759
  root: options.root,
644
- files
760
+ files,
761
+ scope: options.scope
645
762
  }).use(rehypeStringify, { allowDangerousHtml: true });
646
763
  };
647
764
  //#endregion
@@ -708,15 +825,23 @@ var docDataSchema = z.object({
708
825
  //#endregion
709
826
  //#region src/app/_function/read-docs.ts
710
827
  /**
711
- * Markdown 수집과 렌더링. 루트 README는 홈, AGENTS, CLAUDE는 작성 지침으로 분리
828
+ * 공개 범위의 Markdown 수집과 렌더링, 범위 미지정 시 독립 Markdown 처리
712
829
  *
713
- * - 문서 목록에서 루트 안내 파일, node_modules, dist 제외
830
+ * - 사이트 실행에서 공개와 내부 문서만 수집, content 루트 안내 파일 제외
714
831
  * - 머리말, 문서 식별자 중복, 플러그인 오류 시 실패
715
832
  */
716
833
  var readDocs = async (options) => {
717
- const paths = (await readdir(options.root, { recursive: true })).filter((path) => /\.mdx?$/i.test(path) && !/^(readme|agents|claude)\.md$/i.test(path) && !path.split(sep).includes("node_modules") && !path.startsWith(`dist${sep}`));
834
+ const roots = options.scope === void 0 ? [options.root] : [options.scope.publicRoot, ...options.scope.includePrivate && existsSync(options.scope.privateRoot) ? [options.scope.privateRoot] : []];
835
+ 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}`));
718
836
  const docs = await Promise.all(paths.map(async (path) => {
719
- const id = path.replace(/\.mdx?$/i, "").split(sep).join("/").toLowerCase();
837
+ if (options.scope !== void 0) assertContentPath({
838
+ scope: options.scope,
839
+ path: join(options.root, path)
840
+ });
841
+ const id = options.scope === void 0 ? path.replace(/\.mdx?$/i, "").split(sep).join("/").toLowerCase() : toContentId({
842
+ scope: options.scope,
843
+ path: join(options.root, path)
844
+ });
720
845
  const segments = id.split("/");
721
846
  if (segments.some((segment) => segment === "" || segment === "." || segment === ".." || segment === "index.html") || /[#?%*:\\]/.test(id) || /[\p{Cc}]/u.test(id) || options.reserved?.includes(segments[0]) === true || segments[0] === "_fh" || segments[0] === "/favicon.svg".slice(1)) throw new Error(`${copy_error_doc_id}: ${path}`);
722
847
  const source = await readFile(join(options.root, path), "utf8");
@@ -728,9 +853,14 @@ var readDocs = async (options) => {
728
853
  const frontmatter = tree.children[0];
729
854
  const parsed = docDataSchema.safeParse(frontmatter?.type === "yaml" ? parse(frontmatter.value) : {});
730
855
  if (!parsed.success) throw new Error(`${copy_error_frontmatter}: ${path}: ${parsed.error.issues.map((issue) => `${issue.path.join(".")} ${issue.message}`).join(", ")}`);
856
+ const parentPath = parsed.data.parent !== void 0 && /^\.\.?\//.test(parsed.data.parent) ? resolve(dirname(file.path), parsed.data.parent) : void 0;
857
+ const parent = parsed.data.parent !== void 0 && parentPath !== void 0 ? posix.join(posix.dirname(id), parsed.data.parent) : parsed.data.parent;
731
858
  const data = {
732
859
  ...parsed.data,
733
- parent: parsed.data.parent !== void 0 && /^\.\.?\//.test(parsed.data.parent) ? posix.join(posix.dirname(id), parsed.data.parent) : parsed.data.parent
860
+ parent: options.scope !== void 0 && parentPath !== void 0 ? toContentId({
861
+ scope: options.scope,
862
+ path: parentPath
863
+ }) : parent
734
864
  };
735
865
  const fh = {
736
866
  frontmatter: data,
@@ -783,6 +913,10 @@ var readHome = async (options) => {
783
913
  if (paths.length > 1) throw new Error(`${copy_error_home_clash}: ${paths.join(", ")}`);
784
914
  if (paths.length === 0) return;
785
915
  const path = join(options.root, paths[0]);
916
+ if (options.scope !== void 0) assertContentPath({
917
+ scope: options.scope,
918
+ path
919
+ });
786
920
  const fh = {
787
921
  headings: [],
788
922
  sections: []
@@ -874,190 +1008,32 @@ var toHtml = (page) => {
874
1008
  return html`<!DOCTYPE html>${page}`;
875
1009
  };
876
1010
  //#endregion
877
- //#region src/component/widget/prose/wg-prose.tsx
878
- var WgProse = (props) => {
1011
+ //#region src/component/widget/article/wg-article.tsx
1012
+ var WgArticle = (props) => {
879
1013
  if (props.html !== void 0) return /* @__PURE__ */ jsx("article", {
880
- className: clsx("wg_prose__root"),
1014
+ className: clsx("wg_article__root"),
881
1015
  dangerouslySetInnerHTML: { __html: props.html }
882
1016
  });
883
1017
  return /* @__PURE__ */ jsx("article", {
884
- className: clsx("wg_prose__root"),
1018
+ className: clsx("wg_article__root"),
885
1019
  children: props.head !== void 0 && /* @__PURE__ */ jsxs("header", {
886
- className: clsx("wg_prose__head"),
1020
+ className: clsx("wg_article__head"),
887
1021
  children: [/* @__PURE__ */ jsx("h1", {
888
- className: clsx("wg_prose__title"),
1022
+ className: clsx("wg_article__title"),
889
1023
  children: props.head.title
890
1024
  }), props.head.lead !== void 0 && /* @__PURE__ */ jsx("p", {
891
- className: clsx("wg_prose__lead"),
1025
+ className: clsx("wg_article__lead"),
892
1026
  children: props.head.lead
893
1027
  })]
894
1028
  })
895
1029
  });
896
1030
  };
897
1031
  //#endregion
898
- //#region src/component/widget/navigation/_function/to-doc-groups/_to-doc-branches.ts
899
- /**
900
- * 검증된 부모 관계로 문서 가지를 재귀 구성. 형제 순서와 URL 유지
901
- */
902
- var toDocBranches = (options) => {
903
- return options.docs.toSorted((a, b) => a.data.order - b.data.order || a.data.name.localeCompare(b.data.name, "en") || a.id.localeCompare(b.id, "en")).map((doc) => {
904
- const children = options.children.get(doc.id);
905
- return {
906
- ...doc,
907
- children: children === void 0 ? void 0 : toDocBranches({
908
- docs: children,
909
- children: options.children
910
- })
911
- };
912
- });
913
- };
914
- //#endregion
915
- //#region src/component/widget/navigation/_function/to-doc-groups/_to-doc-group-branches.ts
916
- /**
917
- * 각 단계의 직접 문서와 하위 묶음을 분리. 고정된 깊이 제한 없이 구성
918
- */
919
- var toDocGroupBranches = (options) => {
920
- return [...Map.groupBy(options.docs.filter((doc) => doc.data.group.length > options.path.length), (doc) => doc.data.group[options.path.length])].map(([name, docs]) => {
921
- const path = [...options.path, name];
922
- return {
923
- name,
924
- path,
925
- docs,
926
- position: options.positions.get(JSON.stringify(path)) ?? options.unlistedPosition
927
- };
928
- }).toSorted((a, b) => a.position - b.position || a.name.localeCompare(b.name, "ko")).map((group) => {
929
- return {
930
- name: group.name,
931
- path: group.path,
932
- docs: toDocBranches({
933
- docs: group.docs.filter((doc) => doc.data.group.length === group.path.length && doc.data.parent === void 0),
934
- children: options.children
935
- }),
936
- groups: toDocGroupBranches({
937
- docs: group.docs,
938
- children: options.children,
939
- path: group.path,
940
- positions: options.positions,
941
- unlistedPosition: options.unlistedPosition
942
- })
943
- };
944
- });
945
- };
946
- //#endregion
947
- //#region src/component/widget/navigation/_function/to-doc-groups/to-doc-groups.ts
948
- /**
949
- * 메타데이터 경로로 읽는 순서를 가진 문서 계층 구성
950
- *
951
- * - 같은 조상의 중복 지정은 최초 순서 유지
952
- * - 미지정 형제 묶음은 이름순
953
- */
954
- var toDocGroups = (options) => {
955
- const positions = new Map(uniqBy(options.navigation.flatMap((path, index) => {
956
- return path.map((_name, depth) => [JSON.stringify(path.slice(0, depth + 1)), index]);
957
- }), (entry) => entry[0]));
958
- const children = Map.groupBy(options.docs, (doc) => doc.data.parent);
959
- return toDocGroupBranches({
960
- docs: options.docs,
961
- children,
962
- path: [],
963
- positions,
964
- unlistedPosition: options.navigation.length
965
- });
966
- };
967
- //#endregion
968
- //#region src/component/widget/navigation/_function/to-navigation-data/_to-navigation-document.ts
969
- /**
970
- * 본문 없는 문서 가지. 재귀에서도 현재 페이지 한 항목만 표시
971
- */
972
- var toNavigationDocument = (options) => {
973
- return {
974
- id: options.doc.id,
975
- name: options.doc.data.name,
976
- label: options.doc.data.label,
977
- active: options.doc.id === options.current,
978
- children: options.doc.children === void 0 ? [] : options.doc.children.map((doc) => toNavigationDocument({
979
- ...options,
980
- doc
981
- }))
982
- };
983
- };
984
- //#endregion
985
- //#region src/component/widget/navigation/_function/to-navigation-data/_to-navigation-group.ts
986
- /**
987
- * 전체 묶음 경로와 본문 없는 하위 가지
988
- */
989
- var toNavigationGroup = (options) => {
990
- return {
991
- name: options.group.name,
992
- path: options.group.path,
993
- docs: options.group.docs.map((doc) => toNavigationDocument({
994
- doc,
995
- current: options.current
996
- })),
997
- groups: options.group.groups.map((group) => toNavigationGroup({
998
- ...options,
999
- group
1000
- }))
1001
- };
1002
- };
1003
- //#endregion
1004
- //#region src/component/widget/navigation/_function/to-toc-groups.ts
1005
- /**
1006
- * h2별 h3 수집. 가름 시작 절에서 목차 묶음 분리
1007
- *
1008
- * - 첫 h2 앞의 h3은 목차 제외
1009
- */
1010
- var toTocGroups = (outline) => {
1011
- const entries = outline.headings.filter((heading) => heading.depth === section_heading_depth.section || heading.depth === section_heading_depth.sub).map((heading, index) => ({
1012
- heading,
1013
- section: outline.sections[index]
1014
- }));
1015
- const sections = entries.flatMap((entry, index) => {
1016
- if (entry.heading.depth !== section_heading_depth.section) return [];
1017
- return [{
1018
- heading: entry.heading,
1019
- part: entry.section.part,
1020
- subs: takeWhile(entries.slice(index + 1), (sub) => sub.heading.depth !== section_heading_depth.section).map((sub) => {
1021
- return { heading: sub.heading };
1022
- })
1023
- }];
1024
- });
1025
- return sections.flatMap((section, index) => {
1026
- if (index > 0 && section.part === void 0) return [];
1027
- return [{
1028
- part: section.part,
1029
- sections: [section, ...takeWhile(sections.slice(index + 1), (next) => next.part === void 0)]
1030
- }];
1031
- });
1032
- };
1033
- //#endregion
1034
- //#region src/component/widget/navigation/_function/to-navigation-data/to-navigation-data.ts
1032
+ //#region src/component/widget/article/_constant/heading-highlight.ts
1035
1033
  /**
1036
- * 현재 문서의 이름, 계층, 목차만 전송. 다른 문서 본문은 브라우저 탐색 입력에서 제외
1037
- */
1038
- var toNavigationData = (options) => {
1039
- return {
1040
- title: options.site.title,
1041
- groups: toDocGroups({
1042
- docs: options.docs,
1043
- navigation: options.site.navigation
1044
- }).map((group) => toNavigationGroup({
1045
- group,
1046
- current: options.current
1047
- })),
1048
- outline: options.outline === void 0 ? [] : toTocGroups(options.outline),
1049
- home: options.current === void 0,
1050
- cursorFace: options.site.cursorFace
1051
- };
1052
- };
1053
- //#endregion
1054
- //#region src/component/widget/navigation/_function/to-navigation-json.ts
1055
- /**
1056
- * 작성한 이름이 script 종료 태그나 JavaScript 줄 구분자로 해석되지 않는 JSON
1034
+ * 앵커 이동 후 제목 강조 유지 시간. 이후 CSS로 서서히 해제
1057
1035
  */
1058
- var toNavigationJson = (data) => {
1059
- return JSON.stringify(data).replaceAll("<", "\\u003c").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
1060
- };
1036
+ var heading_highlight_hold_ms = 3e3;
1061
1037
  //#endregion
1062
1038
  //#region src/component/widget/cursor-face/_constant/cursor-face.ts
1063
1039
  /**
@@ -1256,7 +1232,8 @@ var WgMermaid = (props) => {
1256
1232
  * 공유 툴팁 재사용, 테마나 접힘 변경에서 숨긴 안내의 다음 호버 복원
1257
1233
  */
1258
1234
  const handleTooltipShow = (event) => {
1259
- if (event.target instanceof Element && event.target.closest("[title]") !== null) for (const tooltip of tooltips) {
1235
+ if (!(event.target instanceof Element) || event.target.closest("[title]") === null) return;
1236
+ for (const tooltip of tooltips) {
1260
1237
  tooltip.hidden = false;
1261
1238
  const bounds = tooltip.getBoundingClientRect();
1262
1239
  tooltip.style.left = `${scrollX + Math.max(0, Math.min(bounds.left, document.documentElement.clientWidth - bounds.width))}px`;
@@ -1268,6 +1245,9 @@ var WgMermaid = (props) => {
1268
1245
  const handleTooltipHide = () => {
1269
1246
  for (const tooltip of tooltips) tooltip.hidden = true;
1270
1247
  };
1248
+ /**
1249
+ * 보이는 다이어그램만 현재 테마로 차례 렌더. 모듈 수신 실패는 모든 영역에 오류 안내
1250
+ */
1271
1251
  const render = toChangeQueue({
1272
1252
  run: async () => {
1273
1253
  const elements = [...document.querySelectorAll(".wg_mermaid__root[data-mermaid]")];
@@ -1297,10 +1277,9 @@ var WgMermaid = (props) => {
1297
1277
  library.default.initialize(mode === "dark" ? render_dark_config : render_config);
1298
1278
  const rendered = await library.default.render(`fh-mermaid-${crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(4)).join("-")}`, source.textContent, measurement);
1299
1279
  if (lifetime.signal.aborted || !element.isConnected || theme.matches !== (mode === "dark")) return;
1300
- const shadow = result.shadowRoot ?? result.attachShadow({ mode: "open" });
1301
1280
  const canvas = document.createElement("div");
1302
1281
  canvas.innerHTML = rendered.svg;
1303
- shadow.replaceChildren(canvas);
1282
+ (result.shadowRoot ?? result.attachShadow({ mode: "open" })).replaceChildren(canvas);
1304
1283
  rendered.bindFunctions?.(canvas);
1305
1284
  canvas.addEventListener("mouseover", handleTooltipShow, { signal: lifetime.signal });
1306
1285
  canvas.addEventListener("mouseout", handleTooltipHide, { signal: lifetime.signal });
@@ -1371,8 +1350,10 @@ var WgMermaid = (props) => {
1371
1350
  const source = element.querySelector(".wg_mermaid__source");
1372
1351
  const result = element.querySelector(".wg_mermaid__result");
1373
1352
  const error = element.querySelector(".wg_mermaid__error");
1374
- result?.shadowRoot?.replaceChildren();
1375
- if (result !== null) result.hidden = true;
1353
+ if (result !== null) {
1354
+ result.shadowRoot?.replaceChildren();
1355
+ result.hidden = true;
1356
+ }
1376
1357
  if (source !== null) source.hidden = false;
1377
1358
  if (error !== null) error.hidden = true;
1378
1359
  delete element.dataset.mermaidTheme;
@@ -1384,7 +1365,7 @@ var WgMermaid = (props) => {
1384
1365
  return null;
1385
1366
  };
1386
1367
  //#endregion
1387
- //#region src/component/widget/navigation/_constant/navigation.ts
1368
+ //#region src/component/widget/sidebar/_constant/navigation.ts
1388
1369
  /**
1389
1370
  * 모바일 탐색 전환. wg-shell.css의 폭 조건과 일치
1390
1371
  */
@@ -1394,66 +1375,65 @@ var navigation_mobile_query = "(width < 1024px)";
1394
1375
  */
1395
1376
  var navigation_wide_query = "(width >= 1360px)";
1396
1377
  //#endregion
1397
- //#region src/component/widget/navigation/_wg-navigation-nav-list.tsx
1398
- var WgNavigationNavList = (props) => {
1399
- const entries = [...props.group.docs.map((doc) => ({
1400
- kind: "document",
1401
- key: `doc:${doc.id}`,
1402
- doc
1403
- })), ...props.group.groups.map((group) => ({
1404
- kind: "group",
1405
- key: `group:${JSON.stringify(group.path)}`,
1406
- group
1407
- }))];
1378
+ //#region src/component/widget/sidebar/_wg-doc-list.tsx
1379
+ var WgDocList = (props) => {
1408
1380
  return /* @__PURE__ */ jsx("ul", {
1409
- className: clsx("wg_navigationNavList__root"),
1381
+ className: clsx("wg_docList__root"),
1410
1382
  "aria-label": props.group.name,
1411
- children: entries.map((entry) => {
1383
+ children: [...props.group.docs.map((doc) => ({
1384
+ kind: "document",
1385
+ key: `doc:${doc.id}`,
1386
+ doc
1387
+ })), ...props.group.groups.map((group) => ({
1388
+ kind: "group",
1389
+ key: `group:${JSON.stringify(group.path)}`,
1390
+ group
1391
+ }))].map((entry) => {
1412
1392
  return /* @__PURE__ */ jsxs("li", {
1413
- className: clsx("wg_navigationNavList__item"),
1393
+ className: clsx("wg_docList__item"),
1414
1394
  children: [entry.kind === "document" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("a", {
1415
- className: clsx("wg_navigationNavList__link", { "wg_navigationNavList__link--active": entry.doc.active }),
1395
+ className: clsx("wg_docList__link", { "wg_docList__link--active": entry.doc.active }),
1416
1396
  href: `/${entry.doc.id}/`,
1417
1397
  title: entry.doc.label,
1418
1398
  "aria-current": entry.doc.active ? "page" : void 0,
1419
1399
  children: entry.doc.name
1420
- }), entry.doc.children.length > 0 && /* @__PURE__ */ jsx(WgNavigationNavList, { group: {
1400
+ }), entry.doc.children.length > 0 && /* @__PURE__ */ jsx(WgDocList, { group: {
1421
1401
  name: entry.doc.name,
1422
1402
  path: props.group.path,
1423
1403
  docs: entry.doc.children,
1424
1404
  groups: []
1425
1405
  } })] }), entry.kind === "group" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1426
- className: clsx("wg_navigationNavList__branch"),
1406
+ className: clsx("wg_docList__branch"),
1427
1407
  children: entry.group.name
1428
- }), /* @__PURE__ */ jsx(WgNavigationNavList, { group: entry.group })] })]
1408
+ }), /* @__PURE__ */ jsx(WgDocList, { group: entry.group })] })]
1429
1409
  }, entry.key);
1430
1410
  })
1431
1411
  });
1432
1412
  };
1433
1413
  //#endregion
1434
- //#region src/component/widget/navigation/_wg-navigation-nav.tsx
1435
- var WgNavigationNav = (props) => {
1414
+ //#region src/component/widget/sidebar/_wg-doc-tree.tsx
1415
+ var WgDocTree = (props) => {
1436
1416
  return /* @__PURE__ */ jsx("nav", {
1437
- className: clsx("wg_navigationNav__root"),
1417
+ className: clsx("wg_docTree__root"),
1438
1418
  "aria-label": copy_nav_aria_label,
1439
1419
  children: /* @__PURE__ */ jsx("div", {
1440
- className: clsx("wg_navigationNav__docs"),
1420
+ className: clsx("wg_docTree__docs"),
1441
1421
  children: props.groups.map((group) => {
1442
1422
  return /* @__PURE__ */ jsxs("section", {
1443
- className: clsx("wg_navigationNav__docGroup"),
1423
+ className: clsx("wg_docTree__docGroup"),
1444
1424
  "aria-label": group.name,
1445
1425
  children: [/* @__PURE__ */ jsx("div", {
1446
- className: clsx("wg_navigationNav__group"),
1426
+ className: clsx("wg_docTree__group"),
1447
1427
  children: group.name
1448
- }), /* @__PURE__ */ jsx(WgNavigationNavList, { group })]
1428
+ }), /* @__PURE__ */ jsx(WgDocList, { group })]
1449
1429
  }, JSON.stringify(group.path));
1450
1430
  })
1451
1431
  })
1452
1432
  });
1453
1433
  };
1454
1434
  //#endregion
1455
- //#region src/component/widget/navigation/_wg-navigation-scroll.tsx
1456
- var WgNavigationScroll = (props) => {
1435
+ //#region src/component/widget/sidebar/_wg-sidebar-scroll.tsx
1436
+ var WgSidebarScroll = (props) => {
1457
1437
  const [overflow, setOverflow] = useState({
1458
1438
  above: false,
1459
1439
  below: false
@@ -1491,9 +1471,9 @@ var WgNavigationScroll = (props) => {
1491
1471
  useLayoutEffect(() => {
1492
1472
  if (!props.ready || props.portRef.current === null) return;
1493
1473
  const observer = new ResizeObserver(scheduleOverflow);
1474
+ let active = true;
1494
1475
  observer.observe(props.portRef.current);
1495
1476
  if (props.portRef.current.firstElementChild !== null) observer.observe(props.portRef.current.firstElementChild);
1496
- let active = true;
1497
1477
  addEventListener("pageshow", scheduleOverflow);
1498
1478
  if (props.portRef.current.isConnected) updateOverflow();
1499
1479
  else queueMicrotask(() => {
@@ -1515,30 +1495,79 @@ var WgNavigationScroll = (props) => {
1515
1495
  }, [props.children, props.ready]);
1516
1496
  return /* @__PURE__ */ jsx("div", {
1517
1497
  ref: props.ready ? props.portRef : void 0,
1518
- className: clsx("wg_navigation__scroll", props.kind === "documents" && "wg_navigation__documents", props.kind === "outline" && "wg_navigation__headings", props.ready && "wg_navigation__scroll--ready", props.ready && overflow.above && "wg_navigation__scroll--above", props.ready && overflow.below && "wg_navigation__scroll--below"),
1519
- "data-navigation-scroll": props.kind,
1498
+ className: clsx("wg_sidebar__scroll", props.kind === "documents" && "wg_sidebar__docsScroll", props.kind === "outline" && "wg_sidebar__tocScroll", props.ready && "wg_sidebar__scroll--ready", props.ready && (overflow.above || overflow.below) && "wg_sidebar__scroll--overflow", props.ready && overflow.above && "wg_sidebar__scroll--above", props.ready && overflow.below && "wg_sidebar__scroll--below"),
1499
+ "data-sidebar-scroll": props.kind,
1520
1500
  onClick: props.onClick,
1521
1501
  onScroll: handlePortScroll,
1522
1502
  children: props.children
1523
1503
  });
1524
1504
  };
1525
1505
  //#endregion
1526
- //#region src/util/dom/find-hash-target.ts
1506
+ //#region src/component/widget/article/_constant/anchor-shift.ts
1527
1507
  /**
1528
- * URL hash 대상. 한국어 제목의 percent encoding 해석
1508
+ * 끝 구간 제목의 추가 도착 여백. 목차가 실행 중 계산해 제목에 지정
1529
1509
  */
1530
- var findHashTarget = (hash) => {
1531
- if (!hash) return null;
1510
+ var anchor_shift_property = "--wg-article-anchor-shift";
1511
+ //#endregion
1512
+ //#region src/constant/scroll.ts
1513
+ /**
1514
+ * 사용자가 문서 스크롤을 직접 조작하는 입력. 초기 앵커 보정 중단과 목차 고정 해제 기준
1515
+ */
1516
+ var scroll_user_input_events = [
1517
+ "wheel",
1518
+ "touchmove",
1519
+ "keydown",
1520
+ "mousedown"
1521
+ ];
1522
+ //#endregion
1523
+ //#region src/util/dom/find-hash-link.ts
1524
+ /**
1525
+ * 현재 문서 안 hash 이동을 일으키는 기본 클릭의 링크
1526
+ *
1527
+ * - 보조키, 새 창, 다운로드, 다른 문서 링크는 없음
1528
+ */
1529
+ var findHashLink = (event) => {
1530
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return null;
1531
+ const link = event.target instanceof Element ? event.target.closest("a[href]") : null;
1532
+ return link instanceof HTMLAnchorElement && (link.target === "" || link.target === "_self") && !link.hasAttribute("download") && link.origin === location.origin && link.pathname === location.pathname && link.search === location.search && link.hash !== "" ? link : null;
1533
+ };
1534
+ //#endregion
1535
+ //#region src/util/dom/to-hash-id.ts
1536
+ /**
1537
+ * URL hash의 대상 id. 한국어 제목의 percent encoding 해석
1538
+ *
1539
+ * - 빈 hash와 잘못된 encoding은 대상 없음
1540
+ */
1541
+ var toHashId = (hash) => {
1542
+ if (!hash) return;
1532
1543
  try {
1533
- return document.getElementById(decodeURIComponent(hash.slice(1)));
1544
+ return decodeURIComponent(hash.slice(1));
1534
1545
  } catch (error) {
1535
- if (error instanceof URIError) return null;
1546
+ if (error instanceof URIError) return;
1536
1547
  throw error;
1537
1548
  }
1538
1549
  };
1539
1550
  //#endregion
1540
- //#region src/component/widget/navigation/_wg-navigation-toc.tsx
1541
- var WgNavigationToc = (props) => {
1551
+ //#region src/util/dom/find-hash-target.ts
1552
+ /**
1553
+ * URL hash 대상 요소. 대상 id가 없으면 없음
1554
+ */
1555
+ var findHashTarget = (hash) => {
1556
+ const id = toHashId(hash);
1557
+ return id === void 0 ? null : document.getElementById(id);
1558
+ };
1559
+ //#endregion
1560
+ //#region src/util/dom/is-restoring-navigation.ts
1561
+ /**
1562
+ * 브라우저가 이전 스크롤 위치를 복원하는 문서 진입. 새로고침, 뒤로가기, 앞으로가기
1563
+ */
1564
+ var isRestoringNavigation = () => {
1565
+ const navigation = performance.getEntriesByType("navigation")[0];
1566
+ return navigation instanceof PerformanceNavigationTiming && (navigation.type === "reload" || navigation.type === "back_forward");
1567
+ };
1568
+ //#endregion
1569
+ //#region src/component/widget/sidebar/_wg-toc.tsx
1570
+ var WgToc = (props) => {
1542
1571
  const [currentSlug, setCurrentSlug] = useState();
1543
1572
  /**
1544
1573
  * 연결마다 구독 설치, 초기 위치 갱신은 본문 파싱 이후 실행
@@ -1546,71 +1575,133 @@ var WgNavigationToc = (props) => {
1546
1575
  useLayoutEffect(() => {
1547
1576
  const headings = props.groups.flatMap((group) => group.sections).flatMap((section) => [section.heading, ...section.subs.map((sub) => sub.heading)]);
1548
1577
  const frame = { id: 0 };
1578
+ const lifetime = new AbortController();
1579
+ const lock = { slug: isRestoringNavigation() ? void 0 : toHashId(location.hash) };
1549
1580
  /**
1550
- * 접힌 본문 제목은 제외하고 읽는 선을 통과한 마지막 제목 선택
1581
+ * 연속 스크롤의 프레임당 1회 현재 제목 갱신
1582
+ *
1583
+ * - 접힌 본문 제목은 제외하고 읽는 선을 통과한 마지막 제목 선택
1584
+ * - 링크, hash로 이동한 제목은 사용자 스크롤 전까지 유지
1551
1585
  */
1552
- const updateCurrentHeading = () => {
1553
- frame.id = 0;
1554
- const visible = headings.flatMap((heading) => {
1586
+ const handleReadingPositionChange = () => {
1587
+ if (frame.id !== 0) return;
1588
+ frame.id = requestAnimationFrame(() => {
1589
+ frame.id = 0;
1590
+ const visible = headings.flatMap((heading) => {
1591
+ const target = findHashTarget(`#${heading.slug}`);
1592
+ return target?.checkVisibility() ? [{
1593
+ slug: heading.slug,
1594
+ top: target.getBoundingClientRect().top - Number.parseFloat(getComputedStyle(target).scrollMarginTop)
1595
+ }] : [];
1596
+ });
1597
+ setCurrentSlug(visible.find((heading) => heading.slug === lock.slug)?.slug ?? visible.findLast((heading) => heading.top <= 1)?.slug ?? visible[0]?.slug);
1598
+ });
1599
+ };
1600
+ /**
1601
+ * 문서 크기, 화면 높이 변경. 끝 구간 제목의 도착 위치를 목차 전환 위치와 맞춘 뒤 현재 제목 재계산
1602
+ *
1603
+ * - 읽는 선에 닿지 못하는 끝 절이 있으면 마지막 한 화면 구간의 도착 위치를 문서 끝까지 비례 압축
1604
+ * - 압축한 거리만큼 제목 위 여백 추가, 앵커 이동과 목차 판정이 같은 위치 사용
1605
+ */
1606
+ const handleLayoutChange = () => {
1607
+ for (const heading of headings) findHashTarget(`#${heading.slug}`)?.style.removeProperty(anchor_shift_property);
1608
+ const anchors = headings.flatMap((heading) => {
1555
1609
  const target = findHashTarget(`#${heading.slug}`);
1556
1610
  return target?.checkVisibility() ? [{
1557
- slug: heading.slug,
1558
- top: target.getBoundingClientRect().top - Number.parseFloat(getComputedStyle(target).scrollMarginTop)
1611
+ target,
1612
+ arrival: scrollY + target.getBoundingClientRect().top - Number.parseFloat(getComputedStyle(target).scrollMarginTop)
1559
1613
  }] : [];
1560
1614
  });
1561
- setCurrentSlug(visible.findLast((heading) => heading.top <= 1)?.slug ?? visible[0]?.slug);
1615
+ const last = anchors.at(-1);
1616
+ const scrollEnd = document.documentElement.scrollHeight - innerHeight;
1617
+ if (last !== void 0 && scrollEnd > 0 && last.arrival > scrollEnd) {
1618
+ const stretchFrom = Math.max(0, scrollEnd - innerHeight);
1619
+ const stretchScale = (scrollEnd - stretchFrom) / (last.arrival - stretchFrom);
1620
+ for (const anchor of anchors) if (anchor.arrival > stretchFrom) anchor.target.style.setProperty(anchor_shift_property, `${(anchor.arrival - stretchFrom) * (1 - stretchScale)}px`);
1621
+ }
1622
+ handleReadingPositionChange();
1562
1623
  };
1563
1624
  /**
1564
- * 연속 스크롤의 프레임당 1회 위치 조회
1625
+ * 같은 문서 hash 링크의 도착 제목 고정. 같은 hash 재클릭 포함
1565
1626
  */
1566
- const handleReadingPositionChange = () => {
1567
- if (frame.id === 0) frame.id = requestAnimationFrame(updateCurrentHeading);
1627
+ const handleHashLinkClick = (event) => {
1628
+ const link = findHashLink(event);
1629
+ if (link === null) return;
1630
+ lock.slug = toHashId(link.hash);
1631
+ handleReadingPositionChange();
1632
+ };
1633
+ /**
1634
+ * 뒤로가기, 주소 수정의 hash 변경 도착 제목 고정
1635
+ */
1636
+ const handleHashChange = () => {
1637
+ lock.slug = toHashId(location.hash);
1638
+ handleReadingPositionChange();
1568
1639
  };
1569
- addEventListener("scroll", handleReadingPositionChange, { passive: true });
1570
- addEventListener("resize", handleReadingPositionChange);
1571
- addEventListener("toggle", handleReadingPositionChange, true);
1572
- addEventListener("pageshow", handleReadingPositionChange);
1573
- document.addEventListener("DOMContentLoaded", handleReadingPositionChange);
1574
- if (document.readyState !== "loading") updateCurrentHeading();
1640
+ /**
1641
+ * 사용자 스크롤 시작. 도착 제목 고정 해제
1642
+ *
1643
+ * - 재계산은 실제 스크롤에서 실행, 링크를 누르는 동안 다른 제목 표시 방지
1644
+ */
1645
+ const handleUserScroll = () => {
1646
+ lock.slug = void 0;
1647
+ };
1648
+ const observer = new ResizeObserver(handleLayoutChange);
1649
+ observer.observe(document.body);
1650
+ addEventListener("scroll", handleReadingPositionChange, {
1651
+ passive: true,
1652
+ signal: lifetime.signal
1653
+ });
1654
+ addEventListener("resize", handleLayoutChange, { signal: lifetime.signal });
1655
+ addEventListener("toggle", handleReadingPositionChange, {
1656
+ capture: true,
1657
+ signal: lifetime.signal
1658
+ });
1659
+ addEventListener("pageshow", handleReadingPositionChange, { signal: lifetime.signal });
1660
+ addEventListener("hashchange", handleHashChange, { signal: lifetime.signal });
1661
+ document.addEventListener("DOMContentLoaded", handleLayoutChange, { signal: lifetime.signal });
1662
+ document.addEventListener("click", handleHashLinkClick, { signal: lifetime.signal });
1663
+ for (const type of scroll_user_input_events) addEventListener(type, handleUserScroll, {
1664
+ passive: true,
1665
+ signal: lifetime.signal
1666
+ });
1667
+ if (document.readyState !== "loading") handleLayoutChange();
1575
1668
  return () => {
1576
1669
  cancelAnimationFrame(frame.id);
1577
- removeEventListener("scroll", handleReadingPositionChange);
1578
- removeEventListener("resize", handleReadingPositionChange);
1579
- removeEventListener("toggle", handleReadingPositionChange, true);
1580
- removeEventListener("pageshow", handleReadingPositionChange);
1581
- document.removeEventListener("DOMContentLoaded", handleReadingPositionChange);
1670
+ lifetime.abort();
1671
+ observer.disconnect();
1672
+ for (const heading of headings) findHashTarget(`#${heading.slug}`)?.style.removeProperty(anchor_shift_property);
1582
1673
  };
1583
1674
  }, [props.groups]);
1584
1675
  return /* @__PURE__ */ jsxs("nav", {
1585
- className: clsx("wg_navigationToc__root"),
1676
+ className: clsx("wg_toc__root"),
1586
1677
  "aria-label": copy_nav_toc_label,
1587
1678
  children: [/* @__PURE__ */ jsx("div", {
1588
- className: clsx("wg_navigationToc__label"),
1679
+ className: clsx("wg_toc__label"),
1589
1680
  children: copy_nav_toc_label
1590
1681
  }), props.groups.map((group) => {
1591
1682
  return /* @__PURE__ */ jsxs("div", {
1592
- className: clsx("wg_navigationToc__groupSection", { "wg_navigationToc__groupSection--named": group.part !== void 0 }),
1683
+ className: clsx("wg_toc__groupSection", { "wg_toc__groupSection--named": group.part !== void 0 }),
1593
1684
  children: [group.part !== void 0 && /* @__PURE__ */ jsx("div", {
1594
- className: clsx("wg_navigationToc__group"),
1685
+ className: clsx("wg_toc__group"),
1595
1686
  children: group.part
1596
1687
  }), /* @__PURE__ */ jsx("ul", {
1597
- className: clsx("wg_navigationToc__list"),
1688
+ className: clsx("wg_toc__list"),
1598
1689
  children: group.sections.map((section) => {
1599
1690
  return /* @__PURE__ */ jsxs("li", {
1600
- className: clsx("wg_navigationToc__item"),
1691
+ className: clsx("wg_toc__item"),
1601
1692
  children: [/* @__PURE__ */ jsx("a", {
1602
- className: clsx("wg_navigationToc__link", { "wg_navigationToc__link--active": section.heading.slug === currentSlug }),
1693
+ className: clsx("wg_toc__link", { "wg_toc__link--active": section.heading.slug === currentSlug }),
1603
1694
  href: `#${section.heading.slug}`,
1604
1695
  "data-toc-link": "",
1605
1696
  "aria-current": section.heading.slug === currentSlug ? "location" : void 0,
1606
1697
  children: section.heading.text
1607
1698
  }), section.subs.length > 0 && /* @__PURE__ */ jsx("ul", {
1608
- className: clsx("wg_navigationToc__sub"),
1699
+ className: clsx("wg_toc__sub"),
1609
1700
  children: section.subs.map((sub) => {
1610
1701
  return /* @__PURE__ */ jsx("li", {
1611
- className: clsx("wg_navigationToc__subItem"),
1702
+ className: clsx("wg_toc__subItem"),
1612
1703
  children: /* @__PURE__ */ jsx("a", {
1613
- className: clsx("wg_navigationToc__subLink", { "wg_navigationToc__subLink--active": sub.heading.slug === currentSlug }),
1704
+ className: clsx("wg_toc__subLink", { "wg_toc__subLink--active": sub.heading.slug === currentSlug }),
1614
1705
  href: `#${sub.heading.slug}`,
1615
1706
  "data-toc-sub-link": "",
1616
1707
  "aria-current": sub.heading.slug === currentSlug ? "location" : void 0,
@@ -1639,8 +1730,8 @@ var revealHashTarget = (hash) => {
1639
1730
  return target;
1640
1731
  };
1641
1732
  //#endregion
1642
- //#region src/component/widget/navigation/wg-navigation.tsx
1643
- var WgNavigation = (props) => {
1733
+ //#region src/component/widget/sidebar/wg-sidebar.tsx
1734
+ var WgSidebar = (props) => {
1644
1735
  const [layout, setLayout] = useState(() => {
1645
1736
  if (props.stores === void 0) return "desktop";
1646
1737
  if (matchMedia("(width < 1024px)").matches) return "drawer";
@@ -1670,12 +1761,6 @@ var WgNavigation = (props) => {
1670
1761
  return wideRef.current.matches ? "wide" : "desktop";
1671
1762
  };
1672
1763
  /**
1673
- * 배치별 DOM 참조 분리, 이전 노드 정리 시 새 배치 참조 유지
1674
- */
1675
- const getNavigationPort = () => {
1676
- return layoutRef.current === "drawer" ? drawerNavigationRef.current : navigationRef.current;
1677
- };
1678
- /**
1679
1764
  * 배치별 DOM은 분리하되 좌표 저장과 복원은 현재 표시되는 한 영역을 소비
1680
1765
  */
1681
1766
  const getDocumentsPort = () => {
@@ -1790,8 +1875,8 @@ var WgNavigation = (props) => {
1790
1875
  const nextLayout = getNavigationLayout();
1791
1876
  if (nextLayout === layoutRef.current) return;
1792
1877
  if (document.activeElement instanceof HTMLElement) focusRef.current = {
1793
- navigation: document.activeElement.closest("[data-navigation]") !== null,
1794
- drawer: document.activeElement === openButtonRef.current || document.activeElement.closest("[data-navigation-drawer]") !== null,
1878
+ navigation: document.activeElement.closest("[data-sidebar-docs]") !== null,
1879
+ drawer: document.activeElement === openButtonRef.current || document.activeElement.closest("[data-sidebar-drawer]") !== null,
1795
1880
  href: document.activeElement.getAttribute("href")
1796
1881
  };
1797
1882
  if (nextLayout !== "drawer") closeNavigation();
@@ -1836,11 +1921,14 @@ var WgNavigation = (props) => {
1836
1921
  useLayoutEffect(() => {
1837
1922
  if (props.stores === void 0) return;
1838
1923
  let active = true;
1924
+ /**
1925
+ * 연결된 배치의 좌표 복원 후 전환 전 포커스를 같은 링크나 진입 버튼으로 이동
1926
+ */
1839
1927
  const restoreNavigation = () => {
1840
1928
  if (!active) return;
1841
1929
  restoreScroll();
1842
1930
  if (focusRef.current === null) return;
1843
- const navigation = getNavigationPort();
1931
+ const navigation = layoutRef.current === "drawer" ? drawerNavigationRef.current : navigationRef.current;
1844
1932
  if (layout === "drawer") {
1845
1933
  if (focusRef.current.navigation) openButtonRef.current?.focus({ preventScroll: true });
1846
1934
  } else if (focusRef.current.navigation && navigation !== null) {
@@ -1857,26 +1945,25 @@ var WgNavigation = (props) => {
1857
1945
  }, [layout, props.stores]);
1858
1946
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1859
1947
  /* @__PURE__ */ jsxs("div", {
1860
- className: clsx("wg_navigation__sidebar", props.stores !== void 0 && "wg_navigation__sidebar--navigationReady"),
1948
+ className: clsx("wg_sidebar__docsColumn", props.stores !== void 0 && "wg_sidebar__docsColumn--ready"),
1861
1949
  children: [/* @__PURE__ */ jsxs("div", {
1862
- className: clsx("wg_navigation__brandRow"),
1950
+ className: clsx("wg_sidebar__brandRow"),
1863
1951
  children: [/* @__PURE__ */ jsx("a", {
1864
1952
  ref: props.stores === void 0 ? void 0 : brandRef,
1865
- className: clsx("wg_navigation__brand"),
1953
+ className: clsx("wg_sidebar__brand"),
1866
1954
  href: "/",
1867
1955
  "aria-current": props.data.home ? "page" : void 0,
1868
1956
  children: props.data.title
1869
1957
  }), /* @__PURE__ */ jsx("button", {
1870
1958
  ref: props.stores === void 0 ? void 0 : openButtonRef,
1871
- className: clsx("wg_navigation__menuToggle", props.stores !== void 0 && layout === "drawer" && "wg_navigation__menuToggle--visible"),
1959
+ className: clsx("wg_sidebar__menuToggle", props.stores !== void 0 && layout === "drawer" && "wg_sidebar__menuToggle--visible"),
1872
1960
  type: "button",
1873
1961
  "aria-label": copy_nav_open,
1874
1962
  "aria-controls": "fh-navigation-drawer",
1875
1963
  "aria-expanded": String(drawerOpen),
1876
- "data-navigation-open": "",
1877
1964
  onClick: handleOpenButtonClick,
1878
1965
  children: /* @__PURE__ */ jsx("svg", {
1879
- className: clsx("wg_navigation__menuIcon"),
1966
+ className: clsx("wg_sidebar__menuIcon"),
1880
1967
  viewBox: "0 0 24 24",
1881
1968
  "aria-hidden": "true",
1882
1969
  focusable: "false",
@@ -1885,58 +1972,56 @@ var WgNavigation = (props) => {
1885
1972
  })]
1886
1973
  }), layout !== "drawer" && /* @__PURE__ */ jsx("div", {
1887
1974
  ref: props.stores === void 0 ? void 0 : navigationRef,
1888
- className: clsx("wg_navigation__navigation"),
1889
- "data-navigation": "",
1890
- "data-navigation-layout": layout,
1891
- children: /* @__PURE__ */ jsx(WgNavigationScroll, {
1975
+ className: clsx("wg_sidebar__docsPanel"),
1976
+ "data-sidebar-docs": "",
1977
+ "data-sidebar-layout": layout,
1978
+ children: /* @__PURE__ */ jsx(WgSidebarScroll, {
1892
1979
  portRef: documentsRef,
1893
1980
  kind: "documents",
1894
1981
  ready: props.stores !== void 0,
1895
1982
  onScroll: saveScroll,
1896
1983
  onClick: handleNavigationClick,
1897
- children: /* @__PURE__ */ jsx(WgNavigationNav, { groups: props.data.groups })
1984
+ children: /* @__PURE__ */ jsx(WgDocTree, { groups: props.data.groups })
1898
1985
  })
1899
1986
  })]
1900
1987
  }),
1901
1988
  /* @__PURE__ */ jsx("aside", {
1902
- className: clsx("wg_navigation__tocRail"),
1903
- "data-navigation-rail": "",
1989
+ className: clsx("wg_sidebar__tocColumn"),
1990
+ "data-sidebar-toc": "",
1904
1991
  children: /* @__PURE__ */ jsx("div", {
1905
- className: clsx("wg_navigation__outline"),
1906
- "data-navigation-outline": "",
1907
- children: /* @__PURE__ */ jsx(WgNavigationScroll, {
1992
+ className: clsx("wg_sidebar__tocPanel"),
1993
+ children: /* @__PURE__ */ jsx(WgSidebarScroll, {
1908
1994
  portRef: outlineRef,
1909
1995
  kind: "outline",
1910
1996
  ready: props.stores !== void 0,
1911
1997
  onScroll: saveScroll,
1912
1998
  onClick: handleNavigationClick,
1913
- children: props.data.outline.length > 0 && /* @__PURE__ */ jsx(WgNavigationToc, { groups: props.data.outline })
1999
+ children: props.data.outline.length > 0 && /* @__PURE__ */ jsx(WgToc, { groups: props.data.outline })
1914
2000
  })
1915
2001
  })
1916
2002
  }),
1917
2003
  /* @__PURE__ */ jsxs("dialog", {
1918
2004
  ref: props.stores === void 0 ? void 0 : dialogRef,
1919
- className: clsx("wg_navigation__drawer"),
2005
+ className: clsx("wg_sidebar__drawer"),
1920
2006
  id: "fh-navigation-drawer",
1921
2007
  "aria-labelledby": "fh-navigation-title",
1922
- "data-navigation-drawer": "",
2008
+ "data-sidebar-drawer": "",
1923
2009
  onClose: handleDrawerClose,
1924
2010
  onCancel: handleDrawerCancel,
1925
2011
  onClick: handleDrawerClick,
1926
2012
  children: [/* @__PURE__ */ jsxs("div", {
1927
- className: clsx("wg_navigation__drawerHeader"),
2013
+ className: clsx("wg_sidebar__drawerHeader"),
1928
2014
  children: [/* @__PURE__ */ jsx("h2", {
1929
- className: clsx("wg_navigation__drawerTitle"),
2015
+ className: clsx("wg_sidebar__drawerTitle"),
1930
2016
  id: "fh-navigation-title",
1931
2017
  children: copy_nav_title
1932
2018
  }), /* @__PURE__ */ jsx("button", {
1933
- className: clsx("wg_navigation__drawerClose"),
2019
+ className: clsx("wg_sidebar__drawerClose"),
1934
2020
  type: "button",
1935
2021
  "aria-label": copy_nav_close,
1936
- "data-navigation-close": "",
1937
2022
  onClick: handleCloseButtonClick,
1938
2023
  children: /* @__PURE__ */ jsx("svg", {
1939
- className: clsx("wg_navigation__menuIcon"),
2024
+ className: clsx("wg_sidebar__menuIcon"),
1940
2025
  viewBox: "0 0 24 24",
1941
2026
  "aria-hidden": "true",
1942
2027
  focusable: "false",
@@ -1944,20 +2029,19 @@ var WgNavigation = (props) => {
1944
2029
  })
1945
2030
  })]
1946
2031
  }), /* @__PURE__ */ jsx("div", {
1947
- className: clsx("wg_navigation__drawerContent"),
1948
- "data-navigation-content": "",
2032
+ className: clsx("wg_sidebar__drawerContent"),
1949
2033
  children: layout === "drawer" && /* @__PURE__ */ jsx("div", {
1950
2034
  ref: props.stores === void 0 ? void 0 : drawerNavigationRef,
1951
- className: clsx("wg_navigation__navigation"),
1952
- "data-navigation": "",
1953
- "data-navigation-layout": layout,
1954
- children: /* @__PURE__ */ jsx(WgNavigationScroll, {
2035
+ className: clsx("wg_sidebar__docsPanel"),
2036
+ "data-sidebar-docs": "",
2037
+ "data-sidebar-layout": layout,
2038
+ children: /* @__PURE__ */ jsx(WgSidebarScroll, {
1955
2039
  portRef: drawerDocumentsRef,
1956
2040
  kind: "documents",
1957
2041
  ready: props.stores !== void 0,
1958
2042
  onScroll: saveScroll,
1959
2043
  onClick: handleNavigationClick,
1960
- children: /* @__PURE__ */ jsx(WgNavigationNav, { groups: props.data.groups })
2044
+ children: /* @__PURE__ */ jsx(WgDocTree, { groups: props.data.groups })
1961
2045
  })
1962
2046
  })
1963
2047
  })]
@@ -1965,12 +2049,6 @@ var WgNavigation = (props) => {
1965
2049
  ] });
1966
2050
  };
1967
2051
  //#endregion
1968
- //#region src/component/widget/prose/_constant/heading-highlight.ts
1969
- /**
1970
- * 앵커 이동 후 제목 강조 유지 시간. 이후 CSS로 서서히 해제
1971
- */
1972
- var heading_highlight_hold_ms = 3e3;
1973
- //#endregion
1974
2052
  //#region src/component/widget/shell-controls/wg-shell-controls.tsx
1975
2053
  var WgShellControls = (props) => {
1976
2054
  const headingRef = useRef(null);
@@ -1986,7 +2064,7 @@ var WgShellControls = (props) => {
1986
2064
  timerRef.current = 0;
1987
2065
  arrivalRef.current?.abort();
1988
2066
  arrivalRef.current = null;
1989
- headingRef.current?.classList.remove("wg_prose__headingText--highlighted");
2067
+ headingRef.current?.classList.remove("wg_article__headingText--highlighted");
1990
2068
  headingRef.current = null;
1991
2069
  };
1992
2070
  /**
@@ -2004,9 +2082,9 @@ var WgShellControls = (props) => {
2004
2082
  */
2005
2083
  const highlightHeading = (target) => {
2006
2084
  clearHeadingHighlight();
2007
- headingRef.current = target === null ? null : target.querySelector(".wg_prose__headingText");
2085
+ headingRef.current = target === null ? null : target.querySelector(".wg_article__headingText");
2008
2086
  if (headingRef.current === null) return;
2009
- headingRef.current.classList.add("wg_prose__headingText--highlighted");
2087
+ headingRef.current.classList.add("wg_article__headingText--highlighted");
2010
2088
  timerRef.current = window.setTimeout(clearHeadingHighlight, heading_highlight_hold_ms);
2011
2089
  arrivalRef.current = new AbortController();
2012
2090
  document.addEventListener("scrollend", handleHighlightArrival, { signal: arrivalRef.current.signal });
@@ -2015,12 +2093,10 @@ var WgShellControls = (props) => {
2015
2093
  * 탐색 DOM이 다시 만들어져도 같은 hash의 접힌 Details 공개를 유지
2016
2094
  */
2017
2095
  const handleHashClick = (event) => {
2018
- if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
2019
- const link = event.target instanceof Element ? event.target.closest("a[href]") : null;
2020
- if (link instanceof HTMLAnchorElement && (link.target === "" || link.target === "_self") && !link.hasAttribute("download") && link.origin === location.origin && link.pathname === location.pathname && link.search === location.search && link.hash) {
2021
- const target = revealHashTarget(link.hash);
2022
- if (link.hash === location.hash) highlightHeading(target);
2023
- }
2096
+ const link = findHashLink(event);
2097
+ if (link === null) return;
2098
+ const target = revealHashTarget(link.hash);
2099
+ if (link.hash === location.hash) highlightHeading(target);
2024
2100
  };
2025
2101
  /**
2026
2102
  * hash 변경 시 접힌 대상을 공개하고 네이티브 스크롤, 강조 적용
@@ -2030,23 +2106,31 @@ var WgShellControls = (props) => {
2030
2106
  target?.scrollIntoView();
2031
2107
  highlightHeading(target);
2032
2108
  };
2109
+ /**
2110
+ * 첫 사용자 입력. 초기 앵커 보정 중단, 부드러운 절 이동 활성화
2111
+ */
2033
2112
  const handleUserScroll = () => {
2034
2113
  userScrollRef.current?.abort();
2114
+ document.documentElement.style.scrollBehavior = "smooth";
2035
2115
  };
2036
2116
  /**
2037
2117
  * 초기 폰트 로드 후 앵커 보정. 해제된 인스턴스와 사용자 이동에 개입하지 않음
2118
+ *
2119
+ * - 새로고침, 뒤로가기는 브라우저의 이전 위치 복원 사용
2120
+ * - 부드러운 절 이동은 보정 후 활성화, 복원 진입은 첫 사용자 입력까지 즉시 스크롤 유지
2038
2121
  */
2039
2122
  const handlePageLoad = async () => {
2040
2123
  const lifetime = lifetimeRef.current;
2041
2124
  const hash = location.hash;
2042
2125
  const target = revealHashTarget(hash);
2126
+ if (isRestoringNavigation()) return;
2043
2127
  await document.fonts.ready;
2044
2128
  if (lifetime === null || lifetime.signal.aborted) return;
2045
2129
  if (target !== null && !userScrollRef.current?.signal.aborted && hash === location.hash) {
2046
2130
  target.scrollIntoView({ behavior: "instant" });
2047
2131
  highlightHeading(target);
2048
2132
  }
2049
- document.documentElement.style.removeProperty("scroll-behavior");
2133
+ document.documentElement.style.scrollBehavior = "smooth";
2050
2134
  };
2051
2135
  /**
2052
2136
  * 셸은 본문 앞에서 마운트. 본문 파싱 이후에만 초기 접힘 대상 조회
@@ -2062,19 +2146,13 @@ var WgShellControls = (props) => {
2062
2146
  const lifetime = new AbortController();
2063
2147
  lifetimeRef.current = lifetime;
2064
2148
  userScrollRef.current = new AbortController();
2065
- document.documentElement.style.scrollBehavior = "auto";
2066
2149
  document.addEventListener("click", handleHashClick, { signal: lifetime.signal });
2067
2150
  addEventListener("hashchange", handleHashChange, { signal: lifetime.signal });
2068
2151
  addEventListener("load", handlePageLoad, {
2069
2152
  once: true,
2070
2153
  signal: lifetime.signal
2071
2154
  });
2072
- for (const type of [
2073
- "wheel",
2074
- "touchmove",
2075
- "keydown",
2076
- "mousedown"
2077
- ]) addEventListener(type, handleUserScroll, {
2155
+ for (const type of scroll_user_input_events) addEventListener(type, handleUserScroll, {
2078
2156
  once: true,
2079
2157
  passive: true,
2080
2158
  signal: lifetime.signal
@@ -2095,8 +2173,8 @@ var WgShellControls = (props) => {
2095
2173
  }, []);
2096
2174
  return /* @__PURE__ */ jsxs(Fragment, { children: [
2097
2175
  /* @__PURE__ */ jsx("div", {
2098
- className: clsx("wg_shellControls__navigation"),
2099
- children: /* @__PURE__ */ jsx(WgNavigation, {
2176
+ className: clsx("wg_shellControls__sidebar"),
2177
+ children: /* @__PURE__ */ jsx(WgSidebar, {
2100
2178
  data: props.data,
2101
2179
  stores: props.stores
2102
2180
  })
@@ -2106,6 +2184,159 @@ var WgShellControls = (props) => {
2106
2184
  ] });
2107
2185
  };
2108
2186
  //#endregion
2187
+ //#region src/component/widget/sidebar/_function/to-doc-groups/_to-doc-branches.ts
2188
+ /**
2189
+ * 검증된 부모 관계로 문서 가지를 재귀 구성. 형제 순서와 URL 유지
2190
+ */
2191
+ var toDocBranches = (options) => {
2192
+ return options.docs.toSorted((a, b) => a.data.order - b.data.order || a.data.name.localeCompare(b.data.name, "en") || a.id.localeCompare(b.id, "en")).map((doc) => {
2193
+ const children = options.children.get(doc.id);
2194
+ return {
2195
+ ...doc,
2196
+ children: children === void 0 ? void 0 : toDocBranches({
2197
+ docs: children,
2198
+ children: options.children
2199
+ })
2200
+ };
2201
+ });
2202
+ };
2203
+ //#endregion
2204
+ //#region src/component/widget/sidebar/_function/to-doc-groups/_to-doc-group-branches.ts
2205
+ /**
2206
+ * 각 단계의 직접 문서와 하위 묶음을 분리. 고정된 깊이 제한 없이 구성
2207
+ */
2208
+ var toDocGroupBranches = (options) => {
2209
+ return [...Map.groupBy(options.docs.filter((doc) => doc.data.group.length > options.path.length), (doc) => doc.data.group[options.path.length])].map(([name, docs]) => {
2210
+ const path = [...options.path, name];
2211
+ return {
2212
+ name,
2213
+ path,
2214
+ docs,
2215
+ position: options.positions.get(JSON.stringify(path)) ?? options.unlistedPosition
2216
+ };
2217
+ }).toSorted((a, b) => a.position - b.position || a.name.localeCompare(b.name, "ko")).map((group) => {
2218
+ return {
2219
+ name: group.name,
2220
+ path: group.path,
2221
+ docs: toDocBranches({
2222
+ docs: group.docs.filter((doc) => doc.data.group.length === group.path.length && doc.data.parent === void 0),
2223
+ children: options.children
2224
+ }),
2225
+ groups: toDocGroupBranches({
2226
+ docs: group.docs,
2227
+ children: options.children,
2228
+ path: group.path,
2229
+ positions: options.positions,
2230
+ unlistedPosition: options.unlistedPosition
2231
+ })
2232
+ };
2233
+ });
2234
+ };
2235
+ //#endregion
2236
+ //#region src/component/widget/sidebar/_function/to-doc-groups/to-doc-groups.ts
2237
+ /**
2238
+ * 메타데이터 경로로 읽는 순서를 가진 문서 계층 구성
2239
+ *
2240
+ * - 같은 조상의 중복 지정은 최초 순서 유지
2241
+ * - 미지정 형제 묶음은 이름순
2242
+ */
2243
+ var toDocGroups = (options) => {
2244
+ const positions = new Map(uniqBy(options.navigation.flatMap((path, index) => {
2245
+ return path.map((_name, depth) => [JSON.stringify(path.slice(0, depth + 1)), index]);
2246
+ }), (entry) => entry[0]));
2247
+ return toDocGroupBranches({
2248
+ docs: options.docs,
2249
+ children: Map.groupBy(options.docs, (doc) => doc.data.parent),
2250
+ path: [],
2251
+ positions,
2252
+ unlistedPosition: options.navigation.length
2253
+ });
2254
+ };
2255
+ //#endregion
2256
+ //#region src/component/widget/sidebar/_function/to-navigation-data/_to-navigation-document.ts
2257
+ /**
2258
+ * 본문 없는 문서 가지. 재귀에서도 현재 페이지 한 항목만 표시
2259
+ */
2260
+ var toNavigationDocument = (options) => {
2261
+ return {
2262
+ id: options.doc.id,
2263
+ name: options.doc.data.name,
2264
+ label: options.doc.data.label,
2265
+ active: options.doc.id === options.current,
2266
+ children: options.doc.children === void 0 ? [] : options.doc.children.map((doc) => toNavigationDocument({
2267
+ ...options,
2268
+ doc
2269
+ }))
2270
+ };
2271
+ };
2272
+ //#endregion
2273
+ //#region src/component/widget/sidebar/_function/to-navigation-data/_to-navigation-group.ts
2274
+ /**
2275
+ * 전체 묶음 경로와 본문 없는 하위 가지
2276
+ */
2277
+ var toNavigationGroup = (options) => {
2278
+ return {
2279
+ name: options.group.name,
2280
+ path: options.group.path,
2281
+ docs: options.group.docs.map((doc) => toNavigationDocument({
2282
+ doc,
2283
+ current: options.current
2284
+ })),
2285
+ groups: options.group.groups.map((group) => toNavigationGroup({
2286
+ ...options,
2287
+ group
2288
+ }))
2289
+ };
2290
+ };
2291
+ //#endregion
2292
+ //#region src/component/widget/sidebar/_function/to-navigation-data/to-navigation-data.ts
2293
+ /**
2294
+ * 현재 문서의 이름, 계층, 목차만 전송. 다른 문서 본문은 브라우저 탐색 입력에서 제외
2295
+ *
2296
+ * - 목차는 h2별 h3 수집, 가름 시작 절에서 목차 묶음 분리
2297
+ * - 첫 h2 앞의 h3은 목차 제외
2298
+ */
2299
+ var toNavigationData = (options) => {
2300
+ const entries = options.outline === void 0 ? [] : zip(options.outline.headings.filter((heading) => heading.depth === section_heading_depth.section || heading.depth === section_heading_depth.sub), options.outline.sections);
2301
+ const sections = entries.flatMap(([heading, section], index) => {
2302
+ if (heading.depth !== section_heading_depth.section) return [];
2303
+ return [{
2304
+ heading,
2305
+ part: section.part,
2306
+ subs: takeWhile(entries.slice(index + 1), ([sub]) => sub.depth !== section_heading_depth.section).map(([sub]) => {
2307
+ return { heading: sub };
2308
+ })
2309
+ }];
2310
+ });
2311
+ return {
2312
+ title: options.site.title,
2313
+ groups: toDocGroups({
2314
+ docs: options.docs,
2315
+ navigation: options.site.navigation
2316
+ }).map((group) => toNavigationGroup({
2317
+ group,
2318
+ current: options.current
2319
+ })),
2320
+ outline: sections.flatMap((section, index) => {
2321
+ if (index > 0 && section.part === void 0) return [];
2322
+ return [{
2323
+ part: section.part,
2324
+ sections: [section, ...takeWhile(sections.slice(index + 1), (next) => next.part === void 0)]
2325
+ }];
2326
+ }),
2327
+ home: options.current === void 0,
2328
+ cursorFace: options.site.cursorFace
2329
+ };
2330
+ };
2331
+ //#endregion
2332
+ //#region src/component/widget/sidebar/_function/to-navigation-json.ts
2333
+ /**
2334
+ * 작성한 이름이 script 종료 태그나 JavaScript 줄 구분자로 해석되지 않는 JSON
2335
+ */
2336
+ var toNavigationJson = (data) => {
2337
+ return JSON.stringify(data).replaceAll("<", "\\u003c").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
2338
+ };
2339
+ //#endregion
2109
2340
  //#region src/component/widget/shell/wg-shell.tsx
2110
2341
  var WgShell = (props) => {
2111
2342
  const navigationData = toNavigationData({
@@ -2186,7 +2417,7 @@ var PgDoc = (props) => {
2186
2417
  title: props.doc.data.name,
2187
2418
  current: props.doc.id,
2188
2419
  outline: props.doc.outline,
2189
- children: /* @__PURE__ */ jsx(WgProse, { html: props.doc.html })
2420
+ children: /* @__PURE__ */ jsx(WgArticle, { html: props.doc.html })
2190
2421
  });
2191
2422
  };
2192
2423
  //#endregion
@@ -2197,7 +2428,7 @@ var PgHome = (props) => {
2197
2428
  docs: props.docs,
2198
2429
  assets: props.assets,
2199
2430
  outline: props.home?.outline,
2200
- children: [props.home !== void 0 && /* @__PURE__ */ jsx(WgProse, { html: props.home.html }), props.home === void 0 && /* @__PURE__ */ jsx(WgProse, { head: {
2431
+ children: [props.home !== void 0 && /* @__PURE__ */ jsx(WgArticle, { html: props.home.html }), props.home === void 0 && /* @__PURE__ */ jsx(WgArticle, { head: {
2201
2432
  title: props.site.title,
2202
2433
  lead: "문서 폴더에 README.md를 추가하면 이곳에 홈 화면으로 표시됩니다."
2203
2434
  } })]
@@ -2285,18 +2516,40 @@ var loadSite = async (params) => {
2285
2516
  ...brand.files
2286
2517
  ]);
2287
2518
  if (params.stylePaths === void 0) files.set(stylePaths[0], join(params.kitRoot, "dist/cli.css"));
2519
+ if (!existsSync(params.root) || !statSync(params.root).isDirectory()) throw new Error(`content 문서 루트가 없습니다: ${params.root}`);
2520
+ const root = realpathSync(params.root);
2521
+ const publicRoot = join(root, params.site.content.public);
2522
+ const privateRoot = join(root, params.site.content.private);
2523
+ for (const path of [publicRoot, privateRoot]) {
2524
+ if (!existsSync(path)) {
2525
+ if (path === privateRoot) continue;
2526
+ throw new Error(`공개 문서 폴더가 없습니다: ${path}. content/public 구조 또는 content 설정을 확인하세요`);
2527
+ }
2528
+ const actual = realpathSync(path);
2529
+ const location = relative(root, actual);
2530
+ if (!statSync(path).isDirectory() || actual !== path || location === ".." || location.startsWith(`..${sep}`) || isAbsolute(location)) throw new Error(`문서 폴더는 content 내부의 실제 폴더여야 합니다: ${path}`);
2531
+ }
2532
+ const scope = {
2533
+ root,
2534
+ publicRoot,
2535
+ privateRoot,
2536
+ includePrivate: params.includePrivate === true
2537
+ };
2288
2538
  const processor = createProcessor({
2289
2539
  site: params.site,
2290
- root: params.root,
2540
+ root: scope.root,
2291
2541
  files,
2292
- mermaid: loadMermaidAssets(params.kitRoot)
2542
+ mermaid: loadMermaidAssets(params.kitRoot),
2543
+ scope
2293
2544
  });
2294
2545
  const [docs, home] = await Promise.all([readDocs({
2295
- root: params.root,
2546
+ root: scope.root,
2547
+ scope,
2296
2548
  processor,
2297
2549
  reserved: params.site.url === void 0 ? void 0 : crawling_reserved_roots
2298
2550
  }), readHome({
2299
- root: params.root,
2551
+ root: scope.root,
2552
+ scope,
2300
2553
  processor
2301
2554
  })]);
2302
2555
  const store = {