for-humanity 0.14.0 → 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";
@@ -210,8 +210,7 @@ var rehypeContentLinks = (options) => (tree, file) => {
210
210
  ]) {
211
211
  const value = node.properties[property];
212
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) {
213
+ for (const url of property === "srcSet" ? value.split(",").map((entry) => entry.trim().split(/\s+/)[0]) : [value]) {
215
214
  if (/^(?:[a-z][a-z\d+.-]*:|\/\/|#)/i.test(url)) continue;
216
215
  const path = decodeURIComponent(url.split(/[?#]/)[0]);
217
216
  try {
@@ -262,7 +261,7 @@ var rehypeHeadingIds = () => (tree, file) => {
262
261
  node.children = [{
263
262
  type: "element",
264
263
  tagName: "span",
265
- properties: { className: ["wg_prose__headingText"] },
264
+ properties: { className: ["wg_article__headingText"] },
266
265
  children: node.children
267
266
  }];
268
267
  });
@@ -309,11 +308,10 @@ var snippet_language_by_extension = {
309
308
  * - 빈 region은 빈 문자열, 없는 region은 undefined 반환
310
309
  */
311
310
  var toSnippetBody = (source, region) => {
312
- const lines = source.replace(/\r\n/g, "\n").split("\n");
313
311
  let inside = region === void 0;
314
312
  let found = region === void 0;
315
313
  const kept = [];
316
- for (const line of lines) {
314
+ for (const line of source.replace(/\r\n/g, "\n").split("\n")) {
317
315
  const marker = snippet_region_pattern.exec(line);
318
316
  if (marker !== null) {
319
317
  if (region !== void 0 && marker[2] === region) {
@@ -367,10 +365,9 @@ var remarkSnippets = (options) => (tree, file) => {
367
365
  return;
368
366
  }
369
367
  const extension = extname(actual).toLowerCase();
370
- const lang = snippet_language_by_extension[extension] === void 0 ? extension.slice(1) : snippet_language_by_extension[extension];
371
368
  parent.children.splice(index, 1, {
372
369
  type: "code",
373
- lang,
370
+ lang: snippet_language_by_extension[extension] === void 0 ? extension.slice(1) : snippet_language_by_extension[extension],
374
371
  meta: meta === void 0 ? null : meta,
375
372
  value: body
376
373
  });
@@ -389,26 +386,26 @@ var shikiTransformers = [
389
386
  name: "for-humanity:code-metadata",
390
387
  root(root) {
391
388
  this.pre.properties.style = void 0;
392
- this.addClassToHast(this.pre, "wg_prose__code");
389
+ this.addClassToHast(this.pre, "wg_article__code");
393
390
  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");
391
+ if (/\bline-numbers\b/.test(this.options.meta.__raw)) this.addClassToHast(this.pre, "wg_article__lineNumbers");
395
392
  const isBad = /\bbad\b/.test(this.options.meta.__raw);
396
393
  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");
394
+ if (isBad) this.addClassToHast(this.pre, "wg_article__code--bad");
395
+ if (isGood) this.addClassToHast(this.pre, "wg_article__code--good");
399
396
  const title = /\[([^\]]+)\]/.exec(this.options.meta.__raw)?.[1];
400
397
  if (title === void 0) return;
401
398
  root.children = [{
402
399
  type: "element",
403
400
  tagName: "figure",
404
- properties: { className: ["wg_prose__codeFile"] },
401
+ properties: { className: ["wg_article__codeFile"] },
405
402
  children: [{
406
403
  type: "element",
407
404
  tagName: "figcaption",
408
405
  properties: { className: [
409
- "wg_prose__codeFileTitle",
410
- ...isBad ? ["wg_prose__codeFileTitle--bad"] : [],
411
- ...isGood ? ["wg_prose__codeFileTitle--good"] : []
406
+ "wg_article__codeFileTitle",
407
+ ...isBad ? ["wg_article__codeFileTitle--bad"] : [],
408
+ ...isGood ? ["wg_article__codeFileTitle--good"] : []
412
409
  ] },
413
410
  children: [{
414
411
  type: "text",
@@ -416,7 +413,7 @@ var shikiTransformers = [
416
413
  }, {
417
414
  type: "element",
418
415
  tagName: "span",
419
- properties: { className: ["wg_prose__codeFileLang"] },
416
+ properties: { className: ["wg_article__codeFileLang"] },
420
417
  children: [{
421
418
  type: "text",
422
419
  value: this.options.lang
@@ -428,22 +425,7 @@ var shikiTransformers = [
428
425
  }
429
426
  ];
430
427
  //#endregion
431
- //#region src/component/widget/mermaid/_function/remark-mermaid.ts
432
- /**
433
- * Mermaid 원문과 브라우저 렌더 영역. JavaScript 비활성 환경에서도 원문 유지
434
- */
435
- var remarkMermaid = (options) => (tree) => {
436
- visit(tree, "code", (node, index, parent) => {
437
- if (node.lang !== "mermaid" || parent === void 0 || index === void 0) return;
438
- if (options.assets !== void 0) for (const [url, path] of options.assets.files) options.files.set(url, path);
439
- parent.children[index] = {
440
- type: "html",
441
- 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>`
442
- };
443
- });
444
- };
445
- //#endregion
446
- //#region src/component/widget/prose/_constant/section.ts
428
+ //#region src/component/widget/article/_constant/section.ts
447
429
  /**
448
430
  * 목차 대상 제목 깊이. h2 절, h3 소제목
449
431
  */
@@ -452,7 +434,7 @@ var section_heading_depth = {
452
434
  sub: 3
453
435
  };
454
436
  //#endregion
455
- //#region src/component/widget/prose/_function/rehype-head.ts
437
+ //#region src/component/widget/article/_function/rehype-head.ts
456
438
  /**
457
439
  * name 제목과 첫 절 앞 본문의 공통 머리. README는 원문의 h1 사용
458
440
  */
@@ -462,13 +444,13 @@ var rehypeHead = () => (tree, file) => {
462
444
  const head = {
463
445
  type: "element",
464
446
  tagName: "header",
465
- properties: { className: ["wg_prose__head"] },
447
+ properties: { className: ["wg_article__head"] },
466
448
  children: tree.children.slice(0, leadEnd).filter((node) => node.type !== "doctype")
467
449
  };
468
450
  if (file.data.fh?.frontmatter !== void 0) head.children.unshift({
469
451
  type: "element",
470
452
  tagName: "h1",
471
- properties: { className: ["wg_prose__title"] },
453
+ properties: { className: ["wg_article__title"] },
472
454
  children: [{
473
455
  type: "text",
474
456
  value: file.data.fh.frontmatter.name
@@ -478,7 +460,7 @@ var rehypeHead = () => (tree, file) => {
478
460
  tree.children = [head, ...tree.children.slice(leadEnd)];
479
461
  };
480
462
  //#endregion
481
- //#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
482
464
  /**
483
465
  * 목차 대상 h2, h3. 중첩 블록까지 문서 순서 수집
484
466
  *
@@ -492,7 +474,7 @@ var toSectionHeadings = (node) => {
492
474
  });
493
475
  };
494
476
  //#endregion
495
- //#region src/component/widget/prose/_function/rehype-sections/rehype-sections.ts
477
+ //#region src/component/widget/article/_function/rehype-sections/rehype-sections.ts
496
478
  /**
497
479
  * h2, h3의 가름 메타데이터 수집. 작성한 제목은 그대로 유지
498
480
  *
@@ -506,7 +488,7 @@ var rehypeSections = () => (tree, file) => {
506
488
  });
507
489
  };
508
490
  //#endregion
509
- //#region src/component/widget/prose/_function/rehype-tables.ts
491
+ //#region src/component/widget/article/_function/rehype-tables.ts
510
492
  /**
511
493
  * 넓은 표의 내부 가로 스크롤 래퍼
512
494
  */
@@ -516,14 +498,14 @@ var rehypeTables = () => (tree) => {
516
498
  parent.children[index] = {
517
499
  type: "element",
518
500
  tagName: "div",
519
- properties: { className: ["wg_prose__table"] },
501
+ properties: { className: ["wg_article__table"] },
520
502
  children: [node]
521
503
  };
522
504
  return [SKIP, index + 1];
523
505
  });
524
506
  };
525
507
  //#endregion
526
- //#region src/component/widget/prose/_function/has-block-content.ts
508
+ //#region src/component/widget/article/_function/has-block-content.ts
527
509
  /**
528
510
  * 부품 본문의 내용 판정. 참조 정의, 주석, 빈 텍스트와 빈 구조 제외
529
511
  *
@@ -538,7 +520,7 @@ var hasBlockContent = (node) => {
538
520
  return true;
539
521
  };
540
522
  //#endregion
541
- //#region src/component/widget/prose/_function/remark-blocks.ts
523
+ //#region src/component/widget/article/_function/remark-blocks.ts
542
524
  /**
543
525
  * Note, Details의 제목, 본문, 속성, 닫힘 계약
544
526
  *
@@ -558,7 +540,6 @@ var remarkBlocks = () => (tree, file) => {
558
540
  if (!node.children.slice(1).some(hasBlockContent)) file.fail(`${copy_error_directive_syntax}: ${node.name} · 본문 필수`, node);
559
541
  const unsupported = node.attributes ? Object.keys(node.attributes).find((key) => node.name !== "details" || key !== "open") : void 0;
560
542
  if (unsupported !== void 0) file.fail(`${copy_error_directive_syntax}: ${node.name} · 미지원 속성 ${unsupported}`, node);
561
- const open = node.attributes?.open;
562
543
  const attributeSource = source.slice(label.position?.end.offset, node.position?.end.offset).split(/\r?\n/, 1)[0].trim();
563
544
  if (attributeSource !== "" && (node.name !== "details" || !/^\{[ \t]*open[ \t]*\}$/.test(attributeSource))) file.fail(`${copy_error_directive_syntax}: ${node.name} · 허용 속성은 Details의 {open}, 나머지는 생략`, node);
564
545
  const original = source.slice(node.position?.start.offset, node.position?.end.offset);
@@ -581,23 +562,23 @@ var remarkBlocks = () => (tree, file) => {
581
562
  label.data = {
582
563
  ...label.data,
583
564
  hName: node.name === "note" ? "strong" : "summary",
584
- hProperties: { className: [node.name === "note" ? "wg_prose__noteTitle" : "wg_prose__detailsSummary"] }
565
+ hProperties: { className: [node.name === "note" ? "wg_article__noteTitle" : "wg_article__detailsSummary"] }
585
566
  };
586
567
  node.data = {
587
568
  ...node.data,
588
569
  hName: node.name === "note" ? "aside" : "details",
589
570
  hProperties: node.name === "note" ? {
590
- className: ["wg_prose__note"],
571
+ className: ["wg_article__note"],
591
572
  ariaLabel: title
592
573
  } : {
593
- className: ["wg_prose__details"],
594
- open: open === ""
574
+ className: ["wg_article__details"],
575
+ open: node.attributes?.open === ""
595
576
  }
596
577
  };
597
578
  });
598
579
  };
599
580
  //#endregion
600
- //#region src/component/widget/prose/_function/remark-links.ts
581
+ //#region src/component/widget/article/_function/remark-links.ts
601
582
  /**
602
583
  * 상대 Markdown 링크의 사이트 URL 변환, query와 hash 원문 유지
603
584
  *
@@ -634,7 +615,7 @@ var remarkLinks = (options) => (tree, file) => {
634
615
  });
635
616
  };
636
617
  //#endregion
637
- //#region src/component/widget/prose/_function/remark-parts.ts
618
+ //#region src/component/widget/article/_function/remark-parts.ts
638
619
  /**
639
620
  * ::part[이름]의 본문 가름과 다음 h2의 data-part
640
621
  *
@@ -646,7 +627,7 @@ var remarkParts = () => (tree) => {
646
627
  const section = tree.children.slice(index + 1).find((next) => next.type === "heading" && next.depth === section_heading_depth.section);
647
628
  node.data = {
648
629
  hName: "div",
649
- hProperties: { className: ["wg_prose__part"] }
630
+ hProperties: { className: ["wg_article__part"] }
650
631
  };
651
632
  if (section) section.data = {
652
633
  ...section.data,
@@ -658,7 +639,7 @@ var remarkParts = () => (tree) => {
658
639
  }
659
640
  };
660
641
  //#endregion
661
- //#region src/component/widget/prose/_constant/status.ts
642
+ //#region src/component/widget/article/_constant/status.ts
662
643
  /**
663
644
  * 상태 문구 뒤 날짜 패턴. YYYY-MM-DD
664
645
  */
@@ -668,7 +649,7 @@ var status_date_pattern = String.raw`\d{4}-\d{2}-\d{2}`;
668
649
  */
669
650
  var status_candidate_pattern = new RegExp(String.raw`\([^()\n]*${status_date_pattern}\)`, "g");
670
651
  //#endregion
671
- //#region src/component/widget/prose/_function/remark-status.ts
652
+ //#region src/component/widget/article/_function/remark-status.ts
672
653
  /**
673
654
  * 설정 문구의 상태 표지 변환. 괄호 제거. 제목, 링크 제외
674
655
  *
@@ -690,7 +671,7 @@ var remarkStatus = (options) => {
690
671
  if (text === void 0 || item === void 0) return false;
691
672
  return {
692
673
  type: "html",
693
- 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>`
694
675
  };
695
676
  }], { ignore });
696
677
  findAndReplace(tree, [status_candidate_pattern, (match, info) => {
@@ -700,22 +681,22 @@ var remarkStatus = (options) => {
700
681
  };
701
682
  };
702
683
  //#endregion
703
- //#region src/component/widget/prose/_function/remark-swatch.ts
684
+ //#region src/component/widget/article/_function/remark-swatch.ts
704
685
  /**
705
686
  * 인라인 hex 색 값의 칩 표시
706
687
  *
707
- * - 문서별 색은 --wg-prose-chip-color로 전달. 고정 스타일시트 값 불가
688
+ * - 문서별 색은 --wg-article-chip-color로 전달. 고정 스타일시트 값 불가
708
689
  */
709
690
  var remarkSwatch = () => (tree) => {
710
691
  visit(tree, "inlineCode", (node) => {
711
692
  if (/^#[0-9a-f]{3,8}$/i.test(node.value.trim())) node.data = { hProperties: {
712
- className: ["wg_prose__color"],
713
- style: `--wg-prose-chip-color: ${node.value.trim()}`
693
+ className: ["wg_article__color"],
694
+ style: `--wg-article-chip-color: ${node.value.trim()}`
714
695
  } };
715
696
  });
716
697
  };
717
698
  //#endregion
718
- //#region src/component/widget/prose/_function/remark-unknown-directives.ts
699
+ //#region src/component/widget/article/_function/remark-unknown-directives.ts
719
700
  /**
720
701
  * hName 없는 블록 지시문은 작성 오류
721
702
  *
@@ -734,13 +715,28 @@ var remarkUnknownDirectives = () => (tree, file) => {
734
715
  });
735
716
  };
736
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
737
733
  //#region src/app/_function/create-processor/create-processor.ts
738
734
  /**
739
735
  * Markdown 변환과 문서 메타데이터 수집
740
736
  *
741
737
  * - 코드와 문서 머리 생성 후 제목 ID와 가름 수집
742
738
  * - 플러그인 HTML 조각은 마지막 rehype-raw에서 해석
743
- * - 코드 상자의 색상은 prose CSS 소유, Shiki 토큰 색상은 --app-code-* 참조
739
+ * - 코드 상자의 색상은 article CSS 소유, Shiki 토큰 색상은 --app-code-* 참조
744
740
  */
745
741
  var createProcessor = (options) => {
746
742
  const files = options.files ?? /* @__PURE__ */ new Map();
@@ -781,32 +777,6 @@ var loadMermaidAssets = (kitRoot) => {
781
777
  };
782
778
  };
783
779
  //#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
810
780
  //#region src/constant/doc.ts
811
781
  /**
812
782
  * 기존 frontmatter type의 호환 값. 모두 같은 Markdown 렌더링
@@ -857,7 +827,7 @@ var docDataSchema = z.object({
857
827
  /**
858
828
  * 공개 범위의 Markdown 수집과 렌더링, 범위 미지정 시 독립 Markdown 처리
859
829
  *
860
- * - 사이트 실행에서 공개·내부 문서만 수집, content 루트 안내 파일 제외
830
+ * - 사이트 실행에서 공개와 내부 문서만 수집, content 루트 안내 파일 제외
861
831
  * - 머리말, 문서 식별자 중복, 플러그인 오류 시 실패
862
832
  */
863
833
  var readDocs = async (options) => {
@@ -1038,190 +1008,32 @@ var toHtml = (page) => {
1038
1008
  return html`<!DOCTYPE html>${page}`;
1039
1009
  };
1040
1010
  //#endregion
1041
- //#region src/component/widget/prose/wg-prose.tsx
1042
- var WgProse = (props) => {
1011
+ //#region src/component/widget/article/wg-article.tsx
1012
+ var WgArticle = (props) => {
1043
1013
  if (props.html !== void 0) return /* @__PURE__ */ jsx("article", {
1044
- className: clsx("wg_prose__root"),
1014
+ className: clsx("wg_article__root"),
1045
1015
  dangerouslySetInnerHTML: { __html: props.html }
1046
1016
  });
1047
1017
  return /* @__PURE__ */ jsx("article", {
1048
- className: clsx("wg_prose__root"),
1018
+ className: clsx("wg_article__root"),
1049
1019
  children: props.head !== void 0 && /* @__PURE__ */ jsxs("header", {
1050
- className: clsx("wg_prose__head"),
1020
+ className: clsx("wg_article__head"),
1051
1021
  children: [/* @__PURE__ */ jsx("h1", {
1052
- className: clsx("wg_prose__title"),
1022
+ className: clsx("wg_article__title"),
1053
1023
  children: props.head.title
1054
1024
  }), props.head.lead !== void 0 && /* @__PURE__ */ jsx("p", {
1055
- className: clsx("wg_prose__lead"),
1025
+ className: clsx("wg_article__lead"),
1056
1026
  children: props.head.lead
1057
1027
  })]
1058
1028
  })
1059
1029
  });
1060
1030
  };
1061
1031
  //#endregion
1062
- //#region src/component/widget/navigation/_function/to-doc-groups/_to-doc-branches.ts
1032
+ //#region src/component/widget/article/_constant/heading-highlight.ts
1063
1033
  /**
1064
- * 검증된 부모 관계로 문서 가지를 재귀 구성. 형제 순서와 URL 유지
1065
- */
1066
- var toDocBranches = (options) => {
1067
- 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) => {
1068
- const children = options.children.get(doc.id);
1069
- return {
1070
- ...doc,
1071
- children: children === void 0 ? void 0 : toDocBranches({
1072
- docs: children,
1073
- children: options.children
1074
- })
1075
- };
1076
- });
1077
- };
1078
- //#endregion
1079
- //#region src/component/widget/navigation/_function/to-doc-groups/_to-doc-group-branches.ts
1080
- /**
1081
- * 각 단계의 직접 문서와 하위 묶음을 분리. 고정된 깊이 제한 없이 구성
1082
- */
1083
- var toDocGroupBranches = (options) => {
1084
- return [...Map.groupBy(options.docs.filter((doc) => doc.data.group.length > options.path.length), (doc) => doc.data.group[options.path.length])].map(([name, docs]) => {
1085
- const path = [...options.path, name];
1086
- return {
1087
- name,
1088
- path,
1089
- docs,
1090
- position: options.positions.get(JSON.stringify(path)) ?? options.unlistedPosition
1091
- };
1092
- }).toSorted((a, b) => a.position - b.position || a.name.localeCompare(b.name, "ko")).map((group) => {
1093
- return {
1094
- name: group.name,
1095
- path: group.path,
1096
- docs: toDocBranches({
1097
- docs: group.docs.filter((doc) => doc.data.group.length === group.path.length && doc.data.parent === void 0),
1098
- children: options.children
1099
- }),
1100
- groups: toDocGroupBranches({
1101
- docs: group.docs,
1102
- children: options.children,
1103
- path: group.path,
1104
- positions: options.positions,
1105
- unlistedPosition: options.unlistedPosition
1106
- })
1107
- };
1108
- });
1109
- };
1110
- //#endregion
1111
- //#region src/component/widget/navigation/_function/to-doc-groups/to-doc-groups.ts
1112
- /**
1113
- * 메타데이터 경로로 읽는 순서를 가진 문서 계층 구성
1114
- *
1115
- * - 같은 조상의 중복 지정은 최초 순서 유지
1116
- * - 미지정 형제 묶음은 이름순
1117
- */
1118
- var toDocGroups = (options) => {
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]));
1122
- const children = Map.groupBy(options.docs, (doc) => doc.data.parent);
1123
- return toDocGroupBranches({
1124
- docs: options.docs,
1125
- children,
1126
- path: [],
1127
- positions,
1128
- unlistedPosition: options.navigation.length
1129
- });
1130
- };
1131
- //#endregion
1132
- //#region src/component/widget/navigation/_function/to-navigation-data/_to-navigation-document.ts
1133
- /**
1134
- * 본문 없는 문서 가지. 재귀에서도 현재 페이지 한 항목만 표시
1135
- */
1136
- var toNavigationDocument = (options) => {
1137
- return {
1138
- id: options.doc.id,
1139
- name: options.doc.data.name,
1140
- label: options.doc.data.label,
1141
- active: options.doc.id === options.current,
1142
- children: options.doc.children === void 0 ? [] : options.doc.children.map((doc) => toNavigationDocument({
1143
- ...options,
1144
- doc
1145
- }))
1146
- };
1147
- };
1148
- //#endregion
1149
- //#region src/component/widget/navigation/_function/to-navigation-data/_to-navigation-group.ts
1150
- /**
1151
- * 전체 묶음 경로와 본문 없는 하위 가지
1152
- */
1153
- var toNavigationGroup = (options) => {
1154
- return {
1155
- name: options.group.name,
1156
- path: options.group.path,
1157
- docs: options.group.docs.map((doc) => toNavigationDocument({
1158
- doc,
1159
- current: options.current
1160
- })),
1161
- groups: options.group.groups.map((group) => toNavigationGroup({
1162
- ...options,
1163
- group
1164
- }))
1165
- };
1166
- };
1167
- //#endregion
1168
- //#region src/component/widget/navigation/_function/to-toc-groups.ts
1169
- /**
1170
- * h2별 h3 수집. 가름 시작 절에서 목차 묶음 분리
1171
- *
1172
- * - 첫 h2 앞의 h3은 목차 제외
1173
- */
1174
- var toTocGroups = (outline) => {
1175
- const entries = outline.headings.filter((heading) => heading.depth === section_heading_depth.section || heading.depth === section_heading_depth.sub).map((heading, index) => ({
1176
- heading,
1177
- section: outline.sections[index]
1178
- }));
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
- });
1196
- };
1197
- //#endregion
1198
- //#region src/component/widget/navigation/_function/to-navigation-data/to-navigation-data.ts
1199
- /**
1200
- * 현재 문서의 이름, 계층, 목차만 전송. 다른 문서 본문은 브라우저 탐색 입력에서 제외
1201
- */
1202
- var toNavigationData = (options) => {
1203
- return {
1204
- title: options.site.title,
1205
- groups: toDocGroups({
1206
- docs: options.docs,
1207
- navigation: options.site.navigation
1208
- }).map((group) => toNavigationGroup({
1209
- group,
1210
- current: options.current
1211
- })),
1212
- outline: options.outline === void 0 ? [] : toTocGroups(options.outline),
1213
- home: options.current === void 0,
1214
- cursorFace: options.site.cursorFace
1215
- };
1216
- };
1217
- //#endregion
1218
- //#region src/component/widget/navigation/_function/to-navigation-json.ts
1219
- /**
1220
- * 작성한 이름이 script 종료 태그나 JavaScript 줄 구분자로 해석되지 않는 JSON
1034
+ * 앵커 이동 후 제목 강조 유지 시간. 이후 CSS로 서서히 해제
1221
1035
  */
1222
- var toNavigationJson = (data) => {
1223
- return JSON.stringify(data).replaceAll("<", "\\u003c").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
1224
- };
1036
+ var heading_highlight_hold_ms = 3e3;
1225
1037
  //#endregion
1226
1038
  //#region src/component/widget/cursor-face/_constant/cursor-face.ts
1227
1039
  /**
@@ -1420,7 +1232,8 @@ var WgMermaid = (props) => {
1420
1232
  * 공유 툴팁 재사용, 테마나 접힘 변경에서 숨긴 안내의 다음 호버 복원
1421
1233
  */
1422
1234
  const handleTooltipShow = (event) => {
1423
- 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) {
1424
1237
  tooltip.hidden = false;
1425
1238
  const bounds = tooltip.getBoundingClientRect();
1426
1239
  tooltip.style.left = `${scrollX + Math.max(0, Math.min(bounds.left, document.documentElement.clientWidth - bounds.width))}px`;
@@ -1432,6 +1245,9 @@ var WgMermaid = (props) => {
1432
1245
  const handleTooltipHide = () => {
1433
1246
  for (const tooltip of tooltips) tooltip.hidden = true;
1434
1247
  };
1248
+ /**
1249
+ * 보이는 다이어그램만 현재 테마로 차례 렌더. 모듈 수신 실패는 모든 영역에 오류 안내
1250
+ */
1435
1251
  const render = toChangeQueue({
1436
1252
  run: async () => {
1437
1253
  const elements = [...document.querySelectorAll(".wg_mermaid__root[data-mermaid]")];
@@ -1461,10 +1277,9 @@ var WgMermaid = (props) => {
1461
1277
  library.default.initialize(mode === "dark" ? render_dark_config : render_config);
1462
1278
  const rendered = await library.default.render(`fh-mermaid-${crypto.getRandomValues(/* @__PURE__ */ new Uint32Array(4)).join("-")}`, source.textContent, measurement);
1463
1279
  if (lifetime.signal.aborted || !element.isConnected || theme.matches !== (mode === "dark")) return;
1464
- const shadow = result.shadowRoot ?? result.attachShadow({ mode: "open" });
1465
1280
  const canvas = document.createElement("div");
1466
1281
  canvas.innerHTML = rendered.svg;
1467
- shadow.replaceChildren(canvas);
1282
+ (result.shadowRoot ?? result.attachShadow({ mode: "open" })).replaceChildren(canvas);
1468
1283
  rendered.bindFunctions?.(canvas);
1469
1284
  canvas.addEventListener("mouseover", handleTooltipShow, { signal: lifetime.signal });
1470
1285
  canvas.addEventListener("mouseout", handleTooltipHide, { signal: lifetime.signal });
@@ -1535,8 +1350,10 @@ var WgMermaid = (props) => {
1535
1350
  const source = element.querySelector(".wg_mermaid__source");
1536
1351
  const result = element.querySelector(".wg_mermaid__result");
1537
1352
  const error = element.querySelector(".wg_mermaid__error");
1538
- result?.shadowRoot?.replaceChildren();
1539
- if (result !== null) result.hidden = true;
1353
+ if (result !== null) {
1354
+ result.shadowRoot?.replaceChildren();
1355
+ result.hidden = true;
1356
+ }
1540
1357
  if (source !== null) source.hidden = false;
1541
1358
  if (error !== null) error.hidden = true;
1542
1359
  delete element.dataset.mermaidTheme;
@@ -1548,7 +1365,7 @@ var WgMermaid = (props) => {
1548
1365
  return null;
1549
1366
  };
1550
1367
  //#endregion
1551
- //#region src/component/widget/navigation/_constant/navigation.ts
1368
+ //#region src/component/widget/sidebar/_constant/navigation.ts
1552
1369
  /**
1553
1370
  * 모바일 탐색 전환. wg-shell.css의 폭 조건과 일치
1554
1371
  */
@@ -1558,66 +1375,65 @@ var navigation_mobile_query = "(width < 1024px)";
1558
1375
  */
1559
1376
  var navigation_wide_query = "(width >= 1360px)";
1560
1377
  //#endregion
1561
- //#region src/component/widget/navigation/_wg-navigation-nav-list.tsx
1562
- var WgNavigationNavList = (props) => {
1563
- const entries = [...props.group.docs.map((doc) => ({
1564
- kind: "document",
1565
- key: `doc:${doc.id}`,
1566
- doc
1567
- })), ...props.group.groups.map((group) => ({
1568
- kind: "group",
1569
- key: `group:${JSON.stringify(group.path)}`,
1570
- group
1571
- }))];
1378
+ //#region src/component/widget/sidebar/_wg-doc-list.tsx
1379
+ var WgDocList = (props) => {
1572
1380
  return /* @__PURE__ */ jsx("ul", {
1573
- className: clsx("wg_navigationNavList__root"),
1381
+ className: clsx("wg_docList__root"),
1574
1382
  "aria-label": props.group.name,
1575
- 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) => {
1576
1392
  return /* @__PURE__ */ jsxs("li", {
1577
- className: clsx("wg_navigationNavList__item"),
1393
+ className: clsx("wg_docList__item"),
1578
1394
  children: [entry.kind === "document" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("a", {
1579
- 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 }),
1580
1396
  href: `/${entry.doc.id}/`,
1581
1397
  title: entry.doc.label,
1582
1398
  "aria-current": entry.doc.active ? "page" : void 0,
1583
1399
  children: entry.doc.name
1584
- }), entry.doc.children.length > 0 && /* @__PURE__ */ jsx(WgNavigationNavList, { group: {
1400
+ }), entry.doc.children.length > 0 && /* @__PURE__ */ jsx(WgDocList, { group: {
1585
1401
  name: entry.doc.name,
1586
1402
  path: props.group.path,
1587
1403
  docs: entry.doc.children,
1588
1404
  groups: []
1589
1405
  } })] }), entry.kind === "group" && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1590
- className: clsx("wg_navigationNavList__branch"),
1406
+ className: clsx("wg_docList__branch"),
1591
1407
  children: entry.group.name
1592
- }), /* @__PURE__ */ jsx(WgNavigationNavList, { group: entry.group })] })]
1408
+ }), /* @__PURE__ */ jsx(WgDocList, { group: entry.group })] })]
1593
1409
  }, entry.key);
1594
1410
  })
1595
1411
  });
1596
1412
  };
1597
1413
  //#endregion
1598
- //#region src/component/widget/navigation/_wg-navigation-nav.tsx
1599
- var WgNavigationNav = (props) => {
1414
+ //#region src/component/widget/sidebar/_wg-doc-tree.tsx
1415
+ var WgDocTree = (props) => {
1600
1416
  return /* @__PURE__ */ jsx("nav", {
1601
- className: clsx("wg_navigationNav__root"),
1417
+ className: clsx("wg_docTree__root"),
1602
1418
  "aria-label": copy_nav_aria_label,
1603
1419
  children: /* @__PURE__ */ jsx("div", {
1604
- className: clsx("wg_navigationNav__docs"),
1420
+ className: clsx("wg_docTree__docs"),
1605
1421
  children: props.groups.map((group) => {
1606
1422
  return /* @__PURE__ */ jsxs("section", {
1607
- className: clsx("wg_navigationNav__docGroup"),
1423
+ className: clsx("wg_docTree__docGroup"),
1608
1424
  "aria-label": group.name,
1609
1425
  children: [/* @__PURE__ */ jsx("div", {
1610
- className: clsx("wg_navigationNav__group"),
1426
+ className: clsx("wg_docTree__group"),
1611
1427
  children: group.name
1612
- }), /* @__PURE__ */ jsx(WgNavigationNavList, { group })]
1428
+ }), /* @__PURE__ */ jsx(WgDocList, { group })]
1613
1429
  }, JSON.stringify(group.path));
1614
1430
  })
1615
1431
  })
1616
1432
  });
1617
1433
  };
1618
1434
  //#endregion
1619
- //#region src/component/widget/navigation/_wg-navigation-scroll.tsx
1620
- var WgNavigationScroll = (props) => {
1435
+ //#region src/component/widget/sidebar/_wg-sidebar-scroll.tsx
1436
+ var WgSidebarScroll = (props) => {
1621
1437
  const [overflow, setOverflow] = useState({
1622
1438
  above: false,
1623
1439
  below: false
@@ -1655,9 +1471,9 @@ var WgNavigationScroll = (props) => {
1655
1471
  useLayoutEffect(() => {
1656
1472
  if (!props.ready || props.portRef.current === null) return;
1657
1473
  const observer = new ResizeObserver(scheduleOverflow);
1474
+ let active = true;
1658
1475
  observer.observe(props.portRef.current);
1659
1476
  if (props.portRef.current.firstElementChild !== null) observer.observe(props.portRef.current.firstElementChild);
1660
- let active = true;
1661
1477
  addEventListener("pageshow", scheduleOverflow);
1662
1478
  if (props.portRef.current.isConnected) updateOverflow();
1663
1479
  else queueMicrotask(() => {
@@ -1679,30 +1495,79 @@ var WgNavigationScroll = (props) => {
1679
1495
  }, [props.children, props.ready]);
1680
1496
  return /* @__PURE__ */ jsx("div", {
1681
1497
  ref: props.ready ? props.portRef : void 0,
1682
- 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"),
1683
- "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,
1684
1500
  onClick: props.onClick,
1685
1501
  onScroll: handlePortScroll,
1686
1502
  children: props.children
1687
1503
  });
1688
1504
  };
1689
1505
  //#endregion
1690
- //#region src/util/dom/find-hash-target.ts
1506
+ //#region src/component/widget/article/_constant/anchor-shift.ts
1691
1507
  /**
1692
- * URL hash 대상. 한국어 제목의 percent encoding 해석
1508
+ * 끝 구간 제목의 추가 도착 여백. 목차가 실행 중 계산해 제목에 지정
1693
1509
  */
1694
- var findHashTarget = (hash) => {
1695
- 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;
1696
1543
  try {
1697
- return document.getElementById(decodeURIComponent(hash.slice(1)));
1544
+ return decodeURIComponent(hash.slice(1));
1698
1545
  } catch (error) {
1699
- if (error instanceof URIError) return null;
1546
+ if (error instanceof URIError) return;
1700
1547
  throw error;
1701
1548
  }
1702
1549
  };
1703
1550
  //#endregion
1704
- //#region src/component/widget/navigation/_wg-navigation-toc.tsx
1705
- 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) => {
1706
1571
  const [currentSlug, setCurrentSlug] = useState();
1707
1572
  /**
1708
1573
  * 연결마다 구독 설치, 초기 위치 갱신은 본문 파싱 이후 실행
@@ -1710,71 +1575,133 @@ var WgNavigationToc = (props) => {
1710
1575
  useLayoutEffect(() => {
1711
1576
  const headings = props.groups.flatMap((group) => group.sections).flatMap((section) => [section.heading, ...section.subs.map((sub) => sub.heading)]);
1712
1577
  const frame = { id: 0 };
1578
+ const lifetime = new AbortController();
1579
+ const lock = { slug: isRestoringNavigation() ? void 0 : toHashId(location.hash) };
1713
1580
  /**
1714
- * 접힌 본문 제목은 제외하고 읽는 선을 통과한 마지막 제목 선택
1581
+ * 연속 스크롤의 프레임당 1회 현재 제목 갱신
1582
+ *
1583
+ * - 접힌 본문 제목은 제외하고 읽는 선을 통과한 마지막 제목 선택
1584
+ * - 링크, hash로 이동한 제목은 사용자 스크롤 전까지 유지
1715
1585
  */
1716
- const updateCurrentHeading = () => {
1717
- frame.id = 0;
1718
- 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) => {
1719
1609
  const target = findHashTarget(`#${heading.slug}`);
1720
1610
  return target?.checkVisibility() ? [{
1721
- slug: heading.slug,
1722
- top: target.getBoundingClientRect().top - Number.parseFloat(getComputedStyle(target).scrollMarginTop)
1611
+ target,
1612
+ arrival: scrollY + target.getBoundingClientRect().top - Number.parseFloat(getComputedStyle(target).scrollMarginTop)
1723
1613
  }] : [];
1724
1614
  });
1725
- 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();
1726
1623
  };
1727
1624
  /**
1728
- * 연속 스크롤의 프레임당 1회 위치 조회
1625
+ * 같은 문서 hash 링크의 도착 제목 고정. 같은 hash 재클릭 포함
1729
1626
  */
1730
- const handleReadingPositionChange = () => {
1731
- 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();
1732
1639
  };
1733
- addEventListener("scroll", handleReadingPositionChange, { passive: true });
1734
- addEventListener("resize", handleReadingPositionChange);
1735
- addEventListener("toggle", handleReadingPositionChange, true);
1736
- addEventListener("pageshow", handleReadingPositionChange);
1737
- document.addEventListener("DOMContentLoaded", handleReadingPositionChange);
1738
- 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();
1739
1668
  return () => {
1740
1669
  cancelAnimationFrame(frame.id);
1741
- removeEventListener("scroll", handleReadingPositionChange);
1742
- removeEventListener("resize", handleReadingPositionChange);
1743
- removeEventListener("toggle", handleReadingPositionChange, true);
1744
- removeEventListener("pageshow", handleReadingPositionChange);
1745
- document.removeEventListener("DOMContentLoaded", handleReadingPositionChange);
1670
+ lifetime.abort();
1671
+ observer.disconnect();
1672
+ for (const heading of headings) findHashTarget(`#${heading.slug}`)?.style.removeProperty(anchor_shift_property);
1746
1673
  };
1747
1674
  }, [props.groups]);
1748
1675
  return /* @__PURE__ */ jsxs("nav", {
1749
- className: clsx("wg_navigationToc__root"),
1676
+ className: clsx("wg_toc__root"),
1750
1677
  "aria-label": copy_nav_toc_label,
1751
1678
  children: [/* @__PURE__ */ jsx("div", {
1752
- className: clsx("wg_navigationToc__label"),
1679
+ className: clsx("wg_toc__label"),
1753
1680
  children: copy_nav_toc_label
1754
1681
  }), props.groups.map((group) => {
1755
1682
  return /* @__PURE__ */ jsxs("div", {
1756
- 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 }),
1757
1684
  children: [group.part !== void 0 && /* @__PURE__ */ jsx("div", {
1758
- className: clsx("wg_navigationToc__group"),
1685
+ className: clsx("wg_toc__group"),
1759
1686
  children: group.part
1760
1687
  }), /* @__PURE__ */ jsx("ul", {
1761
- className: clsx("wg_navigationToc__list"),
1688
+ className: clsx("wg_toc__list"),
1762
1689
  children: group.sections.map((section) => {
1763
1690
  return /* @__PURE__ */ jsxs("li", {
1764
- className: clsx("wg_navigationToc__item"),
1691
+ className: clsx("wg_toc__item"),
1765
1692
  children: [/* @__PURE__ */ jsx("a", {
1766
- 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 }),
1767
1694
  href: `#${section.heading.slug}`,
1768
1695
  "data-toc-link": "",
1769
1696
  "aria-current": section.heading.slug === currentSlug ? "location" : void 0,
1770
1697
  children: section.heading.text
1771
1698
  }), section.subs.length > 0 && /* @__PURE__ */ jsx("ul", {
1772
- className: clsx("wg_navigationToc__sub"),
1699
+ className: clsx("wg_toc__sub"),
1773
1700
  children: section.subs.map((sub) => {
1774
1701
  return /* @__PURE__ */ jsx("li", {
1775
- className: clsx("wg_navigationToc__subItem"),
1702
+ className: clsx("wg_toc__subItem"),
1776
1703
  children: /* @__PURE__ */ jsx("a", {
1777
- 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 }),
1778
1705
  href: `#${sub.heading.slug}`,
1779
1706
  "data-toc-sub-link": "",
1780
1707
  "aria-current": sub.heading.slug === currentSlug ? "location" : void 0,
@@ -1803,8 +1730,8 @@ var revealHashTarget = (hash) => {
1803
1730
  return target;
1804
1731
  };
1805
1732
  //#endregion
1806
- //#region src/component/widget/navigation/wg-navigation.tsx
1807
- var WgNavigation = (props) => {
1733
+ //#region src/component/widget/sidebar/wg-sidebar.tsx
1734
+ var WgSidebar = (props) => {
1808
1735
  const [layout, setLayout] = useState(() => {
1809
1736
  if (props.stores === void 0) return "desktop";
1810
1737
  if (matchMedia("(width < 1024px)").matches) return "drawer";
@@ -1834,12 +1761,6 @@ var WgNavigation = (props) => {
1834
1761
  return wideRef.current.matches ? "wide" : "desktop";
1835
1762
  };
1836
1763
  /**
1837
- * 배치별 DOM 참조 분리, 이전 노드 정리 시 새 배치 참조 유지
1838
- */
1839
- const getNavigationPort = () => {
1840
- return layoutRef.current === "drawer" ? drawerNavigationRef.current : navigationRef.current;
1841
- };
1842
- /**
1843
1764
  * 배치별 DOM은 분리하되 좌표 저장과 복원은 현재 표시되는 한 영역을 소비
1844
1765
  */
1845
1766
  const getDocumentsPort = () => {
@@ -1954,8 +1875,8 @@ var WgNavigation = (props) => {
1954
1875
  const nextLayout = getNavigationLayout();
1955
1876
  if (nextLayout === layoutRef.current) return;
1956
1877
  if (document.activeElement instanceof HTMLElement) focusRef.current = {
1957
- navigation: document.activeElement.closest("[data-navigation]") !== null,
1958
- 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,
1959
1880
  href: document.activeElement.getAttribute("href")
1960
1881
  };
1961
1882
  if (nextLayout !== "drawer") closeNavigation();
@@ -2000,11 +1921,14 @@ var WgNavigation = (props) => {
2000
1921
  useLayoutEffect(() => {
2001
1922
  if (props.stores === void 0) return;
2002
1923
  let active = true;
1924
+ /**
1925
+ * 연결된 배치의 좌표 복원 후 전환 전 포커스를 같은 링크나 진입 버튼으로 이동
1926
+ */
2003
1927
  const restoreNavigation = () => {
2004
1928
  if (!active) return;
2005
1929
  restoreScroll();
2006
1930
  if (focusRef.current === null) return;
2007
- const navigation = getNavigationPort();
1931
+ const navigation = layoutRef.current === "drawer" ? drawerNavigationRef.current : navigationRef.current;
2008
1932
  if (layout === "drawer") {
2009
1933
  if (focusRef.current.navigation) openButtonRef.current?.focus({ preventScroll: true });
2010
1934
  } else if (focusRef.current.navigation && navigation !== null) {
@@ -2021,26 +1945,25 @@ var WgNavigation = (props) => {
2021
1945
  }, [layout, props.stores]);
2022
1946
  return /* @__PURE__ */ jsxs(Fragment, { children: [
2023
1947
  /* @__PURE__ */ jsxs("div", {
2024
- 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"),
2025
1949
  children: [/* @__PURE__ */ jsxs("div", {
2026
- className: clsx("wg_navigation__brandRow"),
1950
+ className: clsx("wg_sidebar__brandRow"),
2027
1951
  children: [/* @__PURE__ */ jsx("a", {
2028
1952
  ref: props.stores === void 0 ? void 0 : brandRef,
2029
- className: clsx("wg_navigation__brand"),
1953
+ className: clsx("wg_sidebar__brand"),
2030
1954
  href: "/",
2031
1955
  "aria-current": props.data.home ? "page" : void 0,
2032
1956
  children: props.data.title
2033
1957
  }), /* @__PURE__ */ jsx("button", {
2034
1958
  ref: props.stores === void 0 ? void 0 : openButtonRef,
2035
- 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"),
2036
1960
  type: "button",
2037
1961
  "aria-label": copy_nav_open,
2038
1962
  "aria-controls": "fh-navigation-drawer",
2039
1963
  "aria-expanded": String(drawerOpen),
2040
- "data-navigation-open": "",
2041
1964
  onClick: handleOpenButtonClick,
2042
1965
  children: /* @__PURE__ */ jsx("svg", {
2043
- className: clsx("wg_navigation__menuIcon"),
1966
+ className: clsx("wg_sidebar__menuIcon"),
2044
1967
  viewBox: "0 0 24 24",
2045
1968
  "aria-hidden": "true",
2046
1969
  focusable: "false",
@@ -2049,58 +1972,56 @@ var WgNavigation = (props) => {
2049
1972
  })]
2050
1973
  }), layout !== "drawer" && /* @__PURE__ */ jsx("div", {
2051
1974
  ref: props.stores === void 0 ? void 0 : navigationRef,
2052
- className: clsx("wg_navigation__navigation"),
2053
- "data-navigation": "",
2054
- "data-navigation-layout": layout,
2055
- children: /* @__PURE__ */ jsx(WgNavigationScroll, {
1975
+ className: clsx("wg_sidebar__docsPanel"),
1976
+ "data-sidebar-docs": "",
1977
+ "data-sidebar-layout": layout,
1978
+ children: /* @__PURE__ */ jsx(WgSidebarScroll, {
2056
1979
  portRef: documentsRef,
2057
1980
  kind: "documents",
2058
1981
  ready: props.stores !== void 0,
2059
1982
  onScroll: saveScroll,
2060
1983
  onClick: handleNavigationClick,
2061
- children: /* @__PURE__ */ jsx(WgNavigationNav, { groups: props.data.groups })
1984
+ children: /* @__PURE__ */ jsx(WgDocTree, { groups: props.data.groups })
2062
1985
  })
2063
1986
  })]
2064
1987
  }),
2065
1988
  /* @__PURE__ */ jsx("aside", {
2066
- className: clsx("wg_navigation__tocRail"),
2067
- "data-navigation-rail": "",
1989
+ className: clsx("wg_sidebar__tocColumn"),
1990
+ "data-sidebar-toc": "",
2068
1991
  children: /* @__PURE__ */ jsx("div", {
2069
- className: clsx("wg_navigation__outline"),
2070
- "data-navigation-outline": "",
2071
- children: /* @__PURE__ */ jsx(WgNavigationScroll, {
1992
+ className: clsx("wg_sidebar__tocPanel"),
1993
+ children: /* @__PURE__ */ jsx(WgSidebarScroll, {
2072
1994
  portRef: outlineRef,
2073
1995
  kind: "outline",
2074
1996
  ready: props.stores !== void 0,
2075
1997
  onScroll: saveScroll,
2076
1998
  onClick: handleNavigationClick,
2077
- 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 })
2078
2000
  })
2079
2001
  })
2080
2002
  }),
2081
2003
  /* @__PURE__ */ jsxs("dialog", {
2082
2004
  ref: props.stores === void 0 ? void 0 : dialogRef,
2083
- className: clsx("wg_navigation__drawer"),
2005
+ className: clsx("wg_sidebar__drawer"),
2084
2006
  id: "fh-navigation-drawer",
2085
2007
  "aria-labelledby": "fh-navigation-title",
2086
- "data-navigation-drawer": "",
2008
+ "data-sidebar-drawer": "",
2087
2009
  onClose: handleDrawerClose,
2088
2010
  onCancel: handleDrawerCancel,
2089
2011
  onClick: handleDrawerClick,
2090
2012
  children: [/* @__PURE__ */ jsxs("div", {
2091
- className: clsx("wg_navigation__drawerHeader"),
2013
+ className: clsx("wg_sidebar__drawerHeader"),
2092
2014
  children: [/* @__PURE__ */ jsx("h2", {
2093
- className: clsx("wg_navigation__drawerTitle"),
2015
+ className: clsx("wg_sidebar__drawerTitle"),
2094
2016
  id: "fh-navigation-title",
2095
2017
  children: copy_nav_title
2096
2018
  }), /* @__PURE__ */ jsx("button", {
2097
- className: clsx("wg_navigation__drawerClose"),
2019
+ className: clsx("wg_sidebar__drawerClose"),
2098
2020
  type: "button",
2099
2021
  "aria-label": copy_nav_close,
2100
- "data-navigation-close": "",
2101
2022
  onClick: handleCloseButtonClick,
2102
2023
  children: /* @__PURE__ */ jsx("svg", {
2103
- className: clsx("wg_navigation__menuIcon"),
2024
+ className: clsx("wg_sidebar__menuIcon"),
2104
2025
  viewBox: "0 0 24 24",
2105
2026
  "aria-hidden": "true",
2106
2027
  focusable: "false",
@@ -2108,20 +2029,19 @@ var WgNavigation = (props) => {
2108
2029
  })
2109
2030
  })]
2110
2031
  }), /* @__PURE__ */ jsx("div", {
2111
- className: clsx("wg_navigation__drawerContent"),
2112
- "data-navigation-content": "",
2032
+ className: clsx("wg_sidebar__drawerContent"),
2113
2033
  children: layout === "drawer" && /* @__PURE__ */ jsx("div", {
2114
2034
  ref: props.stores === void 0 ? void 0 : drawerNavigationRef,
2115
- className: clsx("wg_navigation__navigation"),
2116
- "data-navigation": "",
2117
- "data-navigation-layout": layout,
2118
- children: /* @__PURE__ */ jsx(WgNavigationScroll, {
2035
+ className: clsx("wg_sidebar__docsPanel"),
2036
+ "data-sidebar-docs": "",
2037
+ "data-sidebar-layout": layout,
2038
+ children: /* @__PURE__ */ jsx(WgSidebarScroll, {
2119
2039
  portRef: drawerDocumentsRef,
2120
2040
  kind: "documents",
2121
2041
  ready: props.stores !== void 0,
2122
2042
  onScroll: saveScroll,
2123
2043
  onClick: handleNavigationClick,
2124
- children: /* @__PURE__ */ jsx(WgNavigationNav, { groups: props.data.groups })
2044
+ children: /* @__PURE__ */ jsx(WgDocTree, { groups: props.data.groups })
2125
2045
  })
2126
2046
  })
2127
2047
  })]
@@ -2129,12 +2049,6 @@ var WgNavigation = (props) => {
2129
2049
  ] });
2130
2050
  };
2131
2051
  //#endregion
2132
- //#region src/component/widget/prose/_constant/heading-highlight.ts
2133
- /**
2134
- * 앵커 이동 후 제목 강조 유지 시간. 이후 CSS로 서서히 해제
2135
- */
2136
- var heading_highlight_hold_ms = 3e3;
2137
- //#endregion
2138
2052
  //#region src/component/widget/shell-controls/wg-shell-controls.tsx
2139
2053
  var WgShellControls = (props) => {
2140
2054
  const headingRef = useRef(null);
@@ -2150,7 +2064,7 @@ var WgShellControls = (props) => {
2150
2064
  timerRef.current = 0;
2151
2065
  arrivalRef.current?.abort();
2152
2066
  arrivalRef.current = null;
2153
- headingRef.current?.classList.remove("wg_prose__headingText--highlighted");
2067
+ headingRef.current?.classList.remove("wg_article__headingText--highlighted");
2154
2068
  headingRef.current = null;
2155
2069
  };
2156
2070
  /**
@@ -2168,9 +2082,9 @@ var WgShellControls = (props) => {
2168
2082
  */
2169
2083
  const highlightHeading = (target) => {
2170
2084
  clearHeadingHighlight();
2171
- headingRef.current = target === null ? null : target.querySelector(".wg_prose__headingText");
2085
+ headingRef.current = target === null ? null : target.querySelector(".wg_article__headingText");
2172
2086
  if (headingRef.current === null) return;
2173
- headingRef.current.classList.add("wg_prose__headingText--highlighted");
2087
+ headingRef.current.classList.add("wg_article__headingText--highlighted");
2174
2088
  timerRef.current = window.setTimeout(clearHeadingHighlight, heading_highlight_hold_ms);
2175
2089
  arrivalRef.current = new AbortController();
2176
2090
  document.addEventListener("scrollend", handleHighlightArrival, { signal: arrivalRef.current.signal });
@@ -2179,12 +2093,10 @@ var WgShellControls = (props) => {
2179
2093
  * 탐색 DOM이 다시 만들어져도 같은 hash의 접힌 Details 공개를 유지
2180
2094
  */
2181
2095
  const handleHashClick = (event) => {
2182
- if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
2183
- const link = event.target instanceof Element ? event.target.closest("a[href]") : null;
2184
- 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) {
2185
- const target = revealHashTarget(link.hash);
2186
- if (link.hash === location.hash) highlightHeading(target);
2187
- }
2096
+ const link = findHashLink(event);
2097
+ if (link === null) return;
2098
+ const target = revealHashTarget(link.hash);
2099
+ if (link.hash === location.hash) highlightHeading(target);
2188
2100
  };
2189
2101
  /**
2190
2102
  * hash 변경 시 접힌 대상을 공개하고 네이티브 스크롤, 강조 적용
@@ -2194,23 +2106,31 @@ var WgShellControls = (props) => {
2194
2106
  target?.scrollIntoView();
2195
2107
  highlightHeading(target);
2196
2108
  };
2109
+ /**
2110
+ * 첫 사용자 입력. 초기 앵커 보정 중단, 부드러운 절 이동 활성화
2111
+ */
2197
2112
  const handleUserScroll = () => {
2198
2113
  userScrollRef.current?.abort();
2114
+ document.documentElement.style.scrollBehavior = "smooth";
2199
2115
  };
2200
2116
  /**
2201
2117
  * 초기 폰트 로드 후 앵커 보정. 해제된 인스턴스와 사용자 이동에 개입하지 않음
2118
+ *
2119
+ * - 새로고침, 뒤로가기는 브라우저의 이전 위치 복원 사용
2120
+ * - 부드러운 절 이동은 보정 후 활성화, 복원 진입은 첫 사용자 입력까지 즉시 스크롤 유지
2202
2121
  */
2203
2122
  const handlePageLoad = async () => {
2204
2123
  const lifetime = lifetimeRef.current;
2205
2124
  const hash = location.hash;
2206
2125
  const target = revealHashTarget(hash);
2126
+ if (isRestoringNavigation()) return;
2207
2127
  await document.fonts.ready;
2208
2128
  if (lifetime === null || lifetime.signal.aborted) return;
2209
2129
  if (target !== null && !userScrollRef.current?.signal.aborted && hash === location.hash) {
2210
2130
  target.scrollIntoView({ behavior: "instant" });
2211
2131
  highlightHeading(target);
2212
2132
  }
2213
- document.documentElement.style.removeProperty("scroll-behavior");
2133
+ document.documentElement.style.scrollBehavior = "smooth";
2214
2134
  };
2215
2135
  /**
2216
2136
  * 셸은 본문 앞에서 마운트. 본문 파싱 이후에만 초기 접힘 대상 조회
@@ -2226,19 +2146,13 @@ var WgShellControls = (props) => {
2226
2146
  const lifetime = new AbortController();
2227
2147
  lifetimeRef.current = lifetime;
2228
2148
  userScrollRef.current = new AbortController();
2229
- document.documentElement.style.scrollBehavior = "auto";
2230
2149
  document.addEventListener("click", handleHashClick, { signal: lifetime.signal });
2231
2150
  addEventListener("hashchange", handleHashChange, { signal: lifetime.signal });
2232
2151
  addEventListener("load", handlePageLoad, {
2233
2152
  once: true,
2234
2153
  signal: lifetime.signal
2235
2154
  });
2236
- for (const type of [
2237
- "wheel",
2238
- "touchmove",
2239
- "keydown",
2240
- "mousedown"
2241
- ]) addEventListener(type, handleUserScroll, {
2155
+ for (const type of scroll_user_input_events) addEventListener(type, handleUserScroll, {
2242
2156
  once: true,
2243
2157
  passive: true,
2244
2158
  signal: lifetime.signal
@@ -2259,8 +2173,8 @@ var WgShellControls = (props) => {
2259
2173
  }, []);
2260
2174
  return /* @__PURE__ */ jsxs(Fragment, { children: [
2261
2175
  /* @__PURE__ */ jsx("div", {
2262
- className: clsx("wg_shellControls__navigation"),
2263
- children: /* @__PURE__ */ jsx(WgNavigation, {
2176
+ className: clsx("wg_shellControls__sidebar"),
2177
+ children: /* @__PURE__ */ jsx(WgSidebar, {
2264
2178
  data: props.data,
2265
2179
  stores: props.stores
2266
2180
  })
@@ -2270,6 +2184,159 @@ var WgShellControls = (props) => {
2270
2184
  ] });
2271
2185
  };
2272
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
2273
2340
  //#region src/component/widget/shell/wg-shell.tsx
2274
2341
  var WgShell = (props) => {
2275
2342
  const navigationData = toNavigationData({
@@ -2350,7 +2417,7 @@ var PgDoc = (props) => {
2350
2417
  title: props.doc.data.name,
2351
2418
  current: props.doc.id,
2352
2419
  outline: props.doc.outline,
2353
- children: /* @__PURE__ */ jsx(WgProse, { html: props.doc.html })
2420
+ children: /* @__PURE__ */ jsx(WgArticle, { html: props.doc.html })
2354
2421
  });
2355
2422
  };
2356
2423
  //#endregion
@@ -2361,7 +2428,7 @@ var PgHome = (props) => {
2361
2428
  docs: props.docs,
2362
2429
  assets: props.assets,
2363
2430
  outline: props.home?.outline,
2364
- 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: {
2365
2432
  title: props.site.title,
2366
2433
  lead: "문서 폴더에 README.md를 추가하면 이곳에 홈 화면으로 표시됩니다."
2367
2434
  } })]
@@ -2449,11 +2516,25 @@ var loadSite = async (params) => {
2449
2516
  ...brand.files
2450
2517
  ]);
2451
2518
  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,
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,
2455
2536
  includePrivate: params.includePrivate === true
2456
- });
2537
+ };
2457
2538
  const processor = createProcessor({
2458
2539
  site: params.site,
2459
2540
  root: scope.root,