for-humanity 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +4 -3
- package/dist/cli.css +66 -54
- package/dist/cli.js +83 -22
- package/package.json +1 -1
- package/src/component/widget/shell/_wg-shell-nav-list.css +70 -57
- package/src/component/widget/shell/_wg-shell-nav-list.tsx +25 -10
- package/src/component/widget/shell/_wg-shell-nav.css +3 -2
- package/src/component/widget/shell/_wg-shell-toc.css +6 -5
- package/src/constant/copy.ts +16 -1
- package/src/content/read-docs.test.ts +127 -0
- package/src/content/read-docs.ts +39 -5
- package/src/content/to-doc-groups/_to-doc-branches.ts +15 -0
- package/src/content/to-doc-groups/_to-doc-group-branches.ts +17 -5
- package/src/content/to-doc-groups/to-doc-groups.ts +2 -1
- package/src/style/token.css +4 -3
- package/src/type/doc-branch.ts +11 -0
- package/src/type/doc-data.ts +4 -0
- package/src/type/doc-group.ts +3 -2
- package/src/type/doc.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.3.0 — 2026-10-04
|
|
6
|
+
|
|
7
|
+
- Support nested document pages with optional `parent` IDs, preserving existing groups and document URLs.
|
|
8
|
+
- Resolve `./` and `../` parent references relative to document files so project folders can also be used as document roots.
|
|
9
|
+
- Reject missing parents, self references, parent cycles and parent links across different group paths.
|
|
10
|
+
- Extend active, hover and keyboard focus tree paths through clickable parent documents.
|
|
11
|
+
- Distinguish sidebar groups from document links and page headings, and rename the table of contents to "On this page" with a gray label matching the groups.
|
|
12
|
+
- Clarify trunk-based development, site deployment and package release policy.
|
|
13
|
+
|
|
3
14
|
## 0.2.1 — 2026-10-04
|
|
4
15
|
|
|
5
16
|
- Fingerprint stylesheet and browser-script URLs so new pages cannot reuse assets cached from an older deployment.
|
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ Write Markdown. Keep the context. Share a static site.
|
|
|
33
33
|
Node.js 22 or later and pnpm. Install the CLI in your document project:
|
|
34
34
|
|
|
35
35
|
```sh
|
|
36
|
-
pnpm add -D --save-exact for-humanity@0.
|
|
36
|
+
pnpm add -D --save-exact for-humanity@0.3.0
|
|
37
37
|
mkdir docs
|
|
38
38
|
```
|
|
39
39
|
|
|
@@ -52,7 +52,7 @@ pnpm exec for-humanity preview docs
|
|
|
52
52
|
|
|
53
53
|
If your Markdown files are in the project root, use `.` instead of `docs`. The root `README.md` becomes the home page; root `AGENTS.md` and `CLAUDE.md` stay out of the site.
|
|
54
54
|
|
|
55
|
-
Version `0.
|
|
55
|
+
Version `0.3.0` supports `dev`, `build` and `preview`. `init` is planned; see the [Roadmap](https://github.com/minu-ha/for-humanity/blob/main/templates/document/roadmap.md) and [Changelog](https://github.com/minu-ha/for-humanity/blob/main/CHANGELOG.md).
|
|
56
56
|
Cloudflare Pages can host the generated site. See [Deployment](https://github.com/minu-ha/for-humanity/blob/main/templates/document/deployment.md) for the build settings.
|
|
57
57
|
|
|
58
58
|
### Develop the kit
|
|
@@ -74,7 +74,7 @@ pnpm preview # Preview the static site
|
|
|
74
74
|
pnpm build:pages # Add Cloudflare Pages headers and a 404 page
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
Changes pushed to `main` update the documentation site. A stable version tag such as `v0.
|
|
77
|
+
Changes pushed to `main` update the documentation site. A stable version tag such as `v0.3.0` runs checks, publishes the package to npm, then creates a GitHub Release with the same package file.
|
|
78
78
|
The npm README updates with each package release. See [Maintenance](https://github.com/minu-ha/for-humanity/blob/main/templates/document/maintenance.md#packaging-and-release) for the release steps.
|
|
79
79
|
|
|
80
80
|
## Make it yours
|
|
@@ -95,6 +95,7 @@ export default {
|
|
|
95
95
|
See [Commands](https://github.com/minu-ha/for-humanity/blob/main/templates/document/commands.md) for using another document folder, and [Settings](https://github.com/minu-ha/for-humanity/blob/main/templates/document/settings.md) for configuration.
|
|
96
96
|
Each document other than the root README chooses a `group` and optional `order`. Pages stay independent; groups do not need parent documents or nested folders. Document names may share the same first letter.
|
|
97
97
|
Use `group: Guide` for one group or `group: [Projects, Example project, Research]` for nested navigation. Group paths do not change document URLs.
|
|
98
|
+
Use `parent: writing` to place a document below another document in the same group. File-relative references such as `parent: ./readme` also work. Parent pages remain clickable and each page keeps its own URL; see [Navigation](https://github.com/minu-ha/for-humanity/blob/main/templates/document/navigation.md).
|
|
98
99
|
|
|
99
100
|
## Write with context
|
|
100
101
|
|
package/dist/cli.css
CHANGED
|
@@ -306,8 +306,27 @@
|
|
|
306
306
|
display: block;
|
|
307
307
|
padding: var(--app-space-tree-row) 0 var(--app-space-tree-row) calc(var(--app-space-tree-indent) + var(--app-space-inline));
|
|
308
308
|
color: var(--app-color-text-muted);
|
|
309
|
-
font-size: var(--app-font-size-
|
|
309
|
+
font-size: var(--app-font-size-group);
|
|
310
|
+
line-height: calc(var(--app-font-size-dense) * var(--app-line-height-tight));
|
|
311
|
+
overflow-wrap: anywhere;
|
|
312
|
+
&::before {
|
|
313
|
+
position: absolute;
|
|
314
|
+
top: calc(0.5lh + var(--app-space-tree-row));
|
|
315
|
+
left: 0;
|
|
316
|
+
width: var(--app-space-tree-indent);
|
|
317
|
+
border-top: 1px solid var(--app-color-border-soft);
|
|
318
|
+
content: "";
|
|
319
|
+
pointer-events: none;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
.wg_shellNavList__link {
|
|
323
|
+
position: relative;
|
|
324
|
+
display: block;
|
|
325
|
+
padding: var(--app-space-tree-row) 0 var(--app-space-tree-row) calc(var(--app-space-tree-indent) + var(--app-space-inline));
|
|
326
|
+
color: var(--app-color-link);
|
|
327
|
+
font-size: var(--app-font-size-dense);
|
|
310
328
|
line-height: var(--app-line-height-tight);
|
|
329
|
+
text-decoration: none;
|
|
311
330
|
overflow-wrap: anywhere;
|
|
312
331
|
&::before {
|
|
313
332
|
position: absolute;
|
|
@@ -318,6 +337,26 @@
|
|
|
318
337
|
content: "";
|
|
319
338
|
pointer-events: none;
|
|
320
339
|
}
|
|
340
|
+
&:where(:hover) {
|
|
341
|
+
text-decoration: underline;
|
|
342
|
+
}
|
|
343
|
+
&:where(:hover)::before {
|
|
344
|
+
border-top-color: currentcolor;
|
|
345
|
+
}
|
|
346
|
+
&:where(:focus-visible) {
|
|
347
|
+
outline: none;
|
|
348
|
+
text-decoration: underline;
|
|
349
|
+
}
|
|
350
|
+
&:where(:focus-visible)::before {
|
|
351
|
+
border-top-color: currentcolor;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
.wg_shellNavList__link--active {
|
|
355
|
+
color: var(--app-color-link);
|
|
356
|
+
font-weight: var(--app-font-weight-active);
|
|
357
|
+
&::before {
|
|
358
|
+
border-top-color: currentcolor;
|
|
359
|
+
}
|
|
321
360
|
}
|
|
322
361
|
.wg_shellNavList__item {
|
|
323
362
|
display: flow-root;
|
|
@@ -345,77 +384,47 @@
|
|
|
345
384
|
pointer-events: none;
|
|
346
385
|
}
|
|
347
386
|
&:where(:hover)::after {
|
|
348
|
-
border-left-color: var(--app-color-
|
|
387
|
+
border-left-color: var(--app-color-link);
|
|
349
388
|
}
|
|
350
389
|
&:where(:has(:focus-visible))::after {
|
|
351
|
-
border-left-color: var(--app-color-
|
|
390
|
+
border-left-color: var(--app-color-link);
|
|
352
391
|
}
|
|
353
392
|
&:where(:has(~ .wg_shellNavList__item:hover))::before {
|
|
354
|
-
border-left-color: var(--app-color-
|
|
393
|
+
border-left-color: var(--app-color-link);
|
|
355
394
|
}
|
|
356
395
|
&:where(:has(~ .wg_shellNavList__item :focus-visible))::before {
|
|
357
|
-
border-left-color: var(--app-color-
|
|
396
|
+
border-left-color: var(--app-color-link);
|
|
358
397
|
}
|
|
359
398
|
& > .wg_shellNavList__root {
|
|
360
399
|
margin-left: var(--app-space-tree-indent);
|
|
361
400
|
}
|
|
362
401
|
&:where(:hover) > .wg_shellNavList__branch::before {
|
|
363
|
-
border-top-color: var(--app-color-
|
|
402
|
+
border-top-color: var(--app-color-link);
|
|
364
403
|
}
|
|
365
404
|
&:where(:has(:focus-visible)) > .wg_shellNavList__branch::before {
|
|
366
|
-
border-top-color: var(--app-color-
|
|
405
|
+
border-top-color: var(--app-color-link);
|
|
406
|
+
}
|
|
407
|
+
&:where(:hover) > .wg_shellNavList__link::before {
|
|
408
|
+
border-top-color: var(--app-color-link);
|
|
409
|
+
}
|
|
410
|
+
&:where(:has(:focus-visible)) > .wg_shellNavList__link::before {
|
|
411
|
+
border-top-color: var(--app-color-link);
|
|
367
412
|
}
|
|
368
413
|
}
|
|
369
414
|
.wg_shellNavList__item--trail {
|
|
370
415
|
&::before {
|
|
371
|
-
border-left-color: var(--app-color-
|
|
416
|
+
border-left-color: var(--app-color-link);
|
|
372
417
|
}
|
|
373
418
|
}
|
|
374
419
|
.wg_shellNavList__item--current {
|
|
375
420
|
&::after {
|
|
376
|
-
border-left-color: var(--app-color-
|
|
421
|
+
border-left-color: var(--app-color-link);
|
|
377
422
|
}
|
|
378
423
|
& > .wg_shellNavList__branch::before {
|
|
379
|
-
border-top-color: var(--app-color-
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
.wg_shellNavList__link {
|
|
383
|
-
position: relative;
|
|
384
|
-
display: block;
|
|
385
|
-
padding: var(--app-space-tree-row) 0 var(--app-space-tree-row) calc(var(--app-space-tree-indent) + var(--app-space-inline));
|
|
386
|
-
color: var(--app-color-text);
|
|
387
|
-
font-size: var(--app-font-size-dense);
|
|
388
|
-
line-height: var(--app-line-height-tight);
|
|
389
|
-
text-decoration: none;
|
|
390
|
-
overflow-wrap: anywhere;
|
|
391
|
-
&::before {
|
|
392
|
-
position: absolute;
|
|
393
|
-
top: calc(0.5lh + var(--app-space-tree-row));
|
|
394
|
-
left: 0;
|
|
395
|
-
width: var(--app-space-tree-indent);
|
|
396
|
-
border-top: 1px solid var(--app-color-border-soft);
|
|
397
|
-
content: "";
|
|
398
|
-
pointer-events: none;
|
|
399
|
-
}
|
|
400
|
-
&:where(:hover) {
|
|
401
|
-
text-decoration: underline;
|
|
424
|
+
border-top-color: var(--app-color-link);
|
|
402
425
|
}
|
|
403
|
-
|
|
404
|
-
border-top-color:
|
|
405
|
-
}
|
|
406
|
-
&:where(:focus-visible) {
|
|
407
|
-
outline: none;
|
|
408
|
-
text-decoration: underline;
|
|
409
|
-
}
|
|
410
|
-
&:where(:focus-visible)::before {
|
|
411
|
-
border-top-color: currentcolor;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
.wg_shellNavList__link--active {
|
|
415
|
-
color: var(--app-color-text-strong);
|
|
416
|
-
font-weight: var(--app-font-weight-active);
|
|
417
|
-
&::before {
|
|
418
|
-
border-top-color: currentcolor;
|
|
426
|
+
& > .wg_shellNavList__link::before {
|
|
427
|
+
border-top-color: var(--app-color-link);
|
|
419
428
|
}
|
|
420
429
|
}
|
|
421
430
|
|
|
@@ -432,7 +441,8 @@
|
|
|
432
441
|
.wg_shellNav__group {
|
|
433
442
|
margin: 0 0 4px;
|
|
434
443
|
color: var(--app-color-text-muted);
|
|
435
|
-
font-size: var(--app-font-size-
|
|
444
|
+
font-size: var(--app-font-size-group);
|
|
445
|
+
line-height: calc(var(--app-font-size-dense) * var(--app-line-height-tight));
|
|
436
446
|
text-box: trim-start cap alphabetic;
|
|
437
447
|
}
|
|
438
448
|
|
|
@@ -448,13 +458,14 @@
|
|
|
448
458
|
margin-bottom: var(--app-space-inline);
|
|
449
459
|
color: var(--app-color-text-muted);
|
|
450
460
|
font-size: var(--app-font-size-label);
|
|
451
|
-
font-weight: var(--app-font-weight-
|
|
461
|
+
font-weight: var(--app-font-weight-strong);
|
|
452
462
|
letter-spacing: var(--app-letter-spacing-label);
|
|
453
463
|
}
|
|
454
464
|
.wg_shellToc__group {
|
|
455
465
|
margin: var(--app-space-stack) 0 4px;
|
|
456
466
|
color: var(--app-color-text-muted);
|
|
457
|
-
font-size: var(--app-font-size-
|
|
467
|
+
font-size: var(--app-font-size-group);
|
|
468
|
+
line-height: calc(var(--app-font-size-dense) * var(--app-line-height-tight));
|
|
458
469
|
}
|
|
459
470
|
.wg_shellToc__list {
|
|
460
471
|
margin: 0 0 0 var(--app-space-tree-inset);
|
|
@@ -465,7 +476,7 @@
|
|
|
465
476
|
position: relative;
|
|
466
477
|
display: block;
|
|
467
478
|
padding: var(--app-space-tree-row) 0 var(--app-space-tree-row) calc(var(--app-space-tree-indent) + var(--app-space-inline));
|
|
468
|
-
color: var(--app-color-text
|
|
479
|
+
color: var(--app-color-text);
|
|
469
480
|
font-size: var(--app-font-size-dense);
|
|
470
481
|
line-height: var(--app-line-height-tight);
|
|
471
482
|
text-decoration: none;
|
|
@@ -611,7 +622,7 @@
|
|
|
611
622
|
position: relative;
|
|
612
623
|
display: block;
|
|
613
624
|
padding: var(--app-space-tree-row) 0 var(--app-space-tree-row) calc(var(--app-space-tree-indent) + var(--app-space-inline));
|
|
614
|
-
color: var(--app-color-text
|
|
625
|
+
color: var(--app-color-text);
|
|
615
626
|
font-size: var(--app-font-size-detail);
|
|
616
627
|
line-height: var(--app-line-height-tight);
|
|
617
628
|
text-decoration: none;
|
|
@@ -702,6 +713,7 @@
|
|
|
702
713
|
sans-serif;
|
|
703
714
|
--app-font-mono: "JetBrains Mono Variable", var(--app-font-sans);
|
|
704
715
|
--app-font-brand: "Architects Daughter", var(--app-font-sans);
|
|
716
|
+
--app-font-size-group: 11px;
|
|
705
717
|
--app-font-size-label: 12px;
|
|
706
718
|
--app-font-size-detail: 12px;
|
|
707
719
|
--app-font-size-dense: 12px;
|
|
@@ -711,7 +723,7 @@
|
|
|
711
723
|
--app-font-size-section: 14px;
|
|
712
724
|
--app-font-size-title: 16px;
|
|
713
725
|
--app-font-weight-mark: 400;
|
|
714
|
-
--app-font-weight-active:
|
|
726
|
+
--app-font-weight-active: 600;
|
|
715
727
|
--app-font-weight-strong: 600;
|
|
716
728
|
--app-font-weight-title: 600;
|
|
717
729
|
--app-line-height-title: 1.5;
|
package/dist/cli.js
CHANGED
|
@@ -73,8 +73,12 @@ import clsx2 from "clsx";
|
|
|
73
73
|
import { Fragment } from "react";
|
|
74
74
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
75
75
|
var WgShellNavList = (props) => {
|
|
76
|
+
const currentAncestors = new Set(props.currentDoc?.ancestors);
|
|
76
77
|
const entries = [
|
|
77
|
-
...props.group.docs.map((doc) =>
|
|
78
|
+
...props.group.docs.map((doc) => {
|
|
79
|
+
const active = doc.id === props.currentDoc?.id;
|
|
80
|
+
return { kind: "document", key: `doc:${doc.id}`, doc, active, current: active || currentAncestors.has(doc.id) };
|
|
81
|
+
}),
|
|
78
82
|
...props.group.groups.map((group) => ({
|
|
79
83
|
kind: "group",
|
|
80
84
|
key: `group:${JSON.stringify(group.path)}`,
|
|
@@ -91,16 +95,25 @@ var WgShellNavList = (props) => {
|
|
|
91
95
|
"wg_shellNavList__item--current": entry.current
|
|
92
96
|
}),
|
|
93
97
|
children: [
|
|
94
|
-
entry.kind === "document" && /* @__PURE__ */
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
98
|
+
entry.kind === "document" && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
99
|
+
/* @__PURE__ */ jsx2(
|
|
100
|
+
"a",
|
|
101
|
+
{
|
|
102
|
+
className: clsx2("wg_shellNavList__link", { "wg_shellNavList__link--active": entry.active }),
|
|
103
|
+
href: `/${entry.doc.id}/`,
|
|
104
|
+
title: entry.doc.data.label,
|
|
105
|
+
"aria-current": entry.active ? "page" : void 0,
|
|
106
|
+
children: entry.doc.data.name
|
|
107
|
+
}
|
|
108
|
+
),
|
|
109
|
+
entry.doc.children !== void 0 && /* @__PURE__ */ jsx2(
|
|
110
|
+
WgShellNavList,
|
|
111
|
+
{
|
|
112
|
+
group: { name: entry.doc.data.name, path: entry.doc.data.group, docs: entry.doc.children, groups: [] },
|
|
113
|
+
currentDoc: props.currentDoc
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
] }),
|
|
104
117
|
entry.kind === "group" && /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
105
118
|
/* @__PURE__ */ jsx2("span", { className: clsx2("wg_shellNavList__branch"), children: entry.group.name }),
|
|
106
119
|
/* @__PURE__ */ jsx2(WgShellNavList, { group: entry.group, currentDoc: props.currentDoc })
|
|
@@ -118,7 +131,7 @@ var copy_nav_open = "Open navigation";
|
|
|
118
131
|
var copy_nav_close = "Close navigation";
|
|
119
132
|
var copy_nav_title = "Navigation";
|
|
120
133
|
var copy_home_missing = "\uBB38\uC11C \uD3F4\uB354\uC5D0 README.md\uB97C \uCD94\uAC00\uD558\uBA74 \uC774\uACF3\uC5D0 \uD648 \uD654\uBA74\uC73C\uB85C \uD45C\uC2DC\uB429\uB2C8\uB2E4.";
|
|
121
|
-
var copy_nav_toc_label = "
|
|
134
|
+
var copy_nav_toc_label = "On this page";
|
|
122
135
|
var copy_error_home_clash = "\uD648 README\uAC00 \uACB9\uCE5C\uB2E4";
|
|
123
136
|
var copy_error_prefix = "for-humanity";
|
|
124
137
|
var copy_error_font_preload = "Missing bundled font for preload";
|
|
@@ -128,6 +141,9 @@ var copy_error_frontmatter = "frontmatter\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\
|
|
|
128
141
|
var copy_error_navigation_clash = "navigation\uC758 \uBB36\uC74C\uC774 \uACB9\uCE5C\uB2E4";
|
|
129
142
|
var copy_error_doc_id = "\uBB38\uC11C id\uC5D0 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uB294 \uACBD\uB85C";
|
|
130
143
|
var copy_error_doc_id_clash = "\uBB38\uC11C id\uAC00 \uACB9\uCE5C\uB2E4";
|
|
144
|
+
var copy_error_doc_parent_missing = "\uBD80\uBAA8 \uBB38\uC11C\uAC00 \uC5C6\uB2E4";
|
|
145
|
+
var copy_error_doc_parent_cycle = "\uBB38\uC11C parent\uAC00 \uC21C\uD658\uD55C\uB2E4";
|
|
146
|
+
var copy_error_doc_parent_group = "\uBD80\uBAA8 \uBB38\uC11C\uC640 group\uC774 \uB2E4\uB974\uB2E4";
|
|
131
147
|
var copy_error_flow = "\uD750\uB984\uB3C4\uB97C \uADF8\uB9AC\uC9C0 \uBABB\uD588\uB2E4";
|
|
132
148
|
var copy_error_unknown_directive = "\uBAA8\uB974\uB294 \uBD80\uD488\uC774\uB2E4";
|
|
133
149
|
var copy_error_directive_syntax = "\uBD80\uD488 \uBB38\uBC95\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uB2E4";
|
|
@@ -138,6 +154,14 @@ var copy_warn_unknown_status = "\uC124\uC815\uC5D0 \uC5C6\uB294 \uC0C1\uD0DC \uB
|
|
|
138
154
|
var locale_doc_name = "en";
|
|
139
155
|
var locale_doc_group = "ko";
|
|
140
156
|
|
|
157
|
+
// src/content/to-doc-groups/_to-doc-branches.ts
|
|
158
|
+
var toDocBranches = (options) => {
|
|
159
|
+
return options.docs.toSorted((a, b) => a.data.order - b.data.order || a.data.name.localeCompare(b.data.name, locale_doc_name) || a.id.localeCompare(b.id, locale_doc_name)).map((doc) => {
|
|
160
|
+
const children = options.children.get(doc.id);
|
|
161
|
+
return { ...doc, children: children === void 0 ? void 0 : toDocBranches({ docs: children, children: options.children }) };
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
|
|
141
165
|
// src/content/to-doc-groups/_to-doc-group-branches.ts
|
|
142
166
|
var toDocGroupBranches = (options) => {
|
|
143
167
|
return [
|
|
@@ -157,8 +181,17 @@ var toDocGroupBranches = (options) => {
|
|
|
157
181
|
return {
|
|
158
182
|
name: group.name,
|
|
159
183
|
path: group.path,
|
|
160
|
-
docs:
|
|
161
|
-
|
|
184
|
+
docs: toDocBranches({
|
|
185
|
+
docs: group.docs.filter((doc) => doc.data.group.length === group.path.length && doc.data.parent === void 0),
|
|
186
|
+
children: options.children
|
|
187
|
+
}),
|
|
188
|
+
groups: toDocGroupBranches({
|
|
189
|
+
docs: group.docs,
|
|
190
|
+
children: options.children,
|
|
191
|
+
path: group.path,
|
|
192
|
+
positions: options.positions,
|
|
193
|
+
unlistedPosition: options.unlistedPosition
|
|
194
|
+
})
|
|
162
195
|
};
|
|
163
196
|
});
|
|
164
197
|
};
|
|
@@ -166,7 +199,8 @@ var toDocGroupBranches = (options) => {
|
|
|
166
199
|
// src/content/to-doc-groups/to-doc-groups.ts
|
|
167
200
|
var toDocGroups = (options) => {
|
|
168
201
|
const positions = new Map(options.navigation.flatMap((path, index) => path.map((_name, depth) => [JSON.stringify(path.slice(0, depth + 1)), index])).toReversed());
|
|
169
|
-
|
|
202
|
+
const children = Map.groupBy(options.docs, (doc) => doc.data.parent);
|
|
203
|
+
return toDocGroupBranches({ docs: options.docs, children, path: [], positions, unlistedPosition: options.navigation.length });
|
|
170
204
|
};
|
|
171
205
|
|
|
172
206
|
// src/component/widget/shell/_wg-shell-nav.tsx
|
|
@@ -972,7 +1006,7 @@ var createProcessor = (options) => {
|
|
|
972
1006
|
|
|
973
1007
|
// src/content/read-docs.ts
|
|
974
1008
|
import { readdir, readFile } from "node:fs/promises";
|
|
975
|
-
import { join, sep } from "node:path";
|
|
1009
|
+
import { join, posix as posix2, sep } from "node:path";
|
|
976
1010
|
import { VFile } from "vfile";
|
|
977
1011
|
import { parse as parseYaml } from "yaml";
|
|
978
1012
|
|
|
@@ -1008,6 +1042,10 @@ var docDataSchema = z2.object({
|
|
|
1008
1042
|
* 사이드바의 문서 묶음 경로 · 문자열도 배열로 정규화
|
|
1009
1043
|
*/
|
|
1010
1044
|
group: groupPathSchema,
|
|
1045
|
+
/**
|
|
1046
|
+
* 같은 묶음 안의 부모 문서 id · 확장자 제외, ./·../ 상대 경로도 허용
|
|
1047
|
+
*/
|
|
1048
|
+
parent: z2.string().trim().min(1).optional(),
|
|
1011
1049
|
/**
|
|
1012
1050
|
* 묶음 안의 읽는 순서 · 미지정 문서는 뒤에서 이름순
|
|
1013
1051
|
*/
|
|
@@ -1034,13 +1072,18 @@ var readDocs = async (options) => {
|
|
|
1034
1072
|
if (!parsed.success) {
|
|
1035
1073
|
throw new Error(`${copy_error_frontmatter}: ${path}: ${parsed.error.issues.map((issue) => `${issue.path.join(".")} ${issue.message}`).join(", ")}`);
|
|
1036
1074
|
}
|
|
1037
|
-
const
|
|
1075
|
+
const data = {
|
|
1076
|
+
...parsed.data,
|
|
1077
|
+
parent: parsed.data.parent !== void 0 && /^\.\.?\//.test(parsed.data.parent) ? posix2.join(posix2.dirname(id), parsed.data.parent) : parsed.data.parent
|
|
1078
|
+
};
|
|
1079
|
+
const fh = { frontmatter: data, headings: [], sections: [] };
|
|
1038
1080
|
file.data.fh = fh;
|
|
1039
1081
|
tree.children = tree.children.filter((node) => node.type !== "yaml");
|
|
1040
1082
|
const rendered = await options.processor.run(tree, file);
|
|
1041
1083
|
return {
|
|
1042
1084
|
id,
|
|
1043
|
-
data
|
|
1085
|
+
data,
|
|
1086
|
+
ancestors: [],
|
|
1044
1087
|
html: options.processor.stringify(rendered, file),
|
|
1045
1088
|
outline: { headings: fh.headings, sections: fh.sections }
|
|
1046
1089
|
};
|
|
@@ -1050,7 +1093,25 @@ var readDocs = async (options) => {
|
|
|
1050
1093
|
if (duplicateIds.length > 0) {
|
|
1051
1094
|
throw new Error(`${copy_error_doc_id_clash}: ${duplicateIds.map((entry) => entry[0]).join(", ")}`);
|
|
1052
1095
|
}
|
|
1053
|
-
|
|
1096
|
+
const docsById = new Map(docs.map((doc) => [doc.id, doc]));
|
|
1097
|
+
return docs.map((doc) => {
|
|
1098
|
+
const ancestors = /* @__PURE__ */ new Set([doc.id]);
|
|
1099
|
+
for (let parentId = doc.data.parent; parentId !== void 0; ) {
|
|
1100
|
+
const parent = docsById.get(parentId);
|
|
1101
|
+
if (parent === void 0) {
|
|
1102
|
+
throw new Error(`${copy_error_doc_parent_missing}: ${doc.id} \u2192 ${parentId}`);
|
|
1103
|
+
}
|
|
1104
|
+
if (ancestors.has(parent.id)) {
|
|
1105
|
+
throw new Error(`${copy_error_doc_parent_cycle}: ${[...ancestors, parent.id].join(" \u2192 ")}`);
|
|
1106
|
+
}
|
|
1107
|
+
if (JSON.stringify(parent.data.group) !== JSON.stringify(doc.data.group)) {
|
|
1108
|
+
throw new Error(`${copy_error_doc_parent_group}: ${doc.id} \u2192 ${parent.id}`);
|
|
1109
|
+
}
|
|
1110
|
+
ancestors.add(parent.id);
|
|
1111
|
+
parentId = parent.data.parent;
|
|
1112
|
+
}
|
|
1113
|
+
return { ...doc, ancestors: [...ancestors].slice(1).toReversed() };
|
|
1114
|
+
});
|
|
1054
1115
|
};
|
|
1055
1116
|
|
|
1056
1117
|
// src/content/read-home.ts
|
|
@@ -1172,10 +1233,10 @@ var toErrorMessage = (error) => {
|
|
|
1172
1233
|
// src/util/file/to-asset-path.ts
|
|
1173
1234
|
import { createHash } from "node:crypto";
|
|
1174
1235
|
import { readFileSync } from "node:fs";
|
|
1175
|
-
import { posix as
|
|
1236
|
+
import { posix as posix3 } from "node:path";
|
|
1176
1237
|
var toAssetPath = (options) => {
|
|
1177
|
-
const extension =
|
|
1178
|
-
return `${
|
|
1238
|
+
const extension = posix3.extname(options.path);
|
|
1239
|
+
return `${posix3.dirname(options.path)}/${posix3.basename(options.path, extension)}.${createHash("sha256").update(readFileSync(options.file)).digest("hex")}${extension}`;
|
|
1179
1240
|
};
|
|
1180
1241
|
|
|
1181
1242
|
// src/util/font/to-font-css.ts
|
package/package.json
CHANGED
|
@@ -13,8 +13,30 @@
|
|
|
13
13
|
display: block;
|
|
14
14
|
padding: var(--app-space-tree-row) 0 var(--app-space-tree-row) calc(var(--app-space-tree-indent) + var(--app-space-inline));
|
|
15
15
|
color: var(--app-color-text-muted);
|
|
16
|
-
font-size: var(--app-font-size-
|
|
16
|
+
font-size: var(--app-font-size-group);
|
|
17
|
+
line-height: calc(var(--app-font-size-dense) * var(--app-line-height-tight));
|
|
18
|
+
overflow-wrap: anywhere;
|
|
19
|
+
|
|
20
|
+
&::before {
|
|
21
|
+
position: absolute;
|
|
22
|
+
top: calc(0.5lh + var(--app-space-tree-row));
|
|
23
|
+
left: 0;
|
|
24
|
+
width: var(--app-space-tree-indent);
|
|
25
|
+
border-top: 1px solid var(--app-color-border-soft);
|
|
26
|
+
content: "";
|
|
27
|
+
pointer-events: none;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.wg_shellNavList__link {
|
|
32
|
+
/* 가로 가지는 링크 첫 줄 기준 · 여러 줄 제목에서도 연결 위치 유지 */
|
|
33
|
+
position: relative;
|
|
34
|
+
display: block;
|
|
35
|
+
padding: var(--app-space-tree-row) 0 var(--app-space-tree-row) calc(var(--app-space-tree-indent) + var(--app-space-inline));
|
|
36
|
+
color: var(--app-color-link);
|
|
37
|
+
font-size: var(--app-font-size-dense);
|
|
17
38
|
line-height: var(--app-line-height-tight);
|
|
39
|
+
text-decoration: none;
|
|
18
40
|
overflow-wrap: anywhere;
|
|
19
41
|
|
|
20
42
|
&::before {
|
|
@@ -26,6 +48,33 @@
|
|
|
26
48
|
content: "";
|
|
27
49
|
pointer-events: none;
|
|
28
50
|
}
|
|
51
|
+
|
|
52
|
+
&:where(:hover) {
|
|
53
|
+
text-decoration: underline;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
&:where(:hover)::before {
|
|
57
|
+
border-top-color: currentcolor;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* hover와 같은 밑줄·가지선으로 키보드 위치 표시 */
|
|
61
|
+
&:where(:focus-visible) {
|
|
62
|
+
outline: none;
|
|
63
|
+
text-decoration: underline;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
&:where(:focus-visible)::before {
|
|
67
|
+
border-top-color: currentcolor;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.wg_shellNavList__link--active {
|
|
72
|
+
color: var(--app-color-link);
|
|
73
|
+
font-weight: var(--app-font-weight-active);
|
|
74
|
+
|
|
75
|
+
&::before {
|
|
76
|
+
border-top-color: currentcolor;
|
|
77
|
+
}
|
|
29
78
|
}
|
|
30
79
|
|
|
31
80
|
.wg_shellNavList__item {
|
|
@@ -62,20 +111,20 @@
|
|
|
62
111
|
}
|
|
63
112
|
|
|
64
113
|
&:where(:hover)::after {
|
|
65
|
-
border-left-color: var(--app-color-
|
|
114
|
+
border-left-color: var(--app-color-link);
|
|
66
115
|
}
|
|
67
116
|
|
|
68
117
|
&:where(:has(:focus-visible))::after {
|
|
69
|
-
border-left-color: var(--app-color-
|
|
118
|
+
border-left-color: var(--app-color-link);
|
|
70
119
|
}
|
|
71
120
|
|
|
72
121
|
/* 뒤 항목까지의 세로 경로 · 같은 목록의 형제 안에서만 연결 */
|
|
73
122
|
&:where(:has(~ .wg_shellNavList__item:hover))::before {
|
|
74
|
-
border-left-color: var(--app-color-
|
|
123
|
+
border-left-color: var(--app-color-link);
|
|
75
124
|
}
|
|
76
125
|
|
|
77
126
|
&:where(:has(~ .wg_shellNavList__item :focus-visible))::before {
|
|
78
|
-
border-left-color: var(--app-color-
|
|
127
|
+
border-left-color: var(--app-color-link);
|
|
79
128
|
}
|
|
80
129
|
|
|
81
130
|
& > .wg_shellNavList__root {
|
|
@@ -84,75 +133,39 @@
|
|
|
84
133
|
|
|
85
134
|
/* 하위 문서를 탐색할 때 조상 묶음의 가지도 함께 연결 */
|
|
86
135
|
&:where(:hover) > .wg_shellNavList__branch::before {
|
|
87
|
-
border-top-color: var(--app-color-
|
|
136
|
+
border-top-color: var(--app-color-link);
|
|
88
137
|
}
|
|
89
138
|
|
|
90
139
|
&:where(:has(:focus-visible)) > .wg_shellNavList__branch::before {
|
|
91
|
-
border-top-color: var(--app-color-
|
|
140
|
+
border-top-color: var(--app-color-link);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* 자식 문서를 탐색할 때 클릭 가능한 부모 문서의 가지도 연결 */
|
|
144
|
+
&:where(:hover) > .wg_shellNavList__link::before {
|
|
145
|
+
border-top-color: var(--app-color-link);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
&:where(:has(:focus-visible)) > .wg_shellNavList__link::before {
|
|
149
|
+
border-top-color: var(--app-color-link);
|
|
92
150
|
}
|
|
93
151
|
}
|
|
94
152
|
|
|
95
153
|
.wg_shellNavList__item--trail {
|
|
96
154
|
&::before {
|
|
97
|
-
border-left-color: var(--app-color-
|
|
155
|
+
border-left-color: var(--app-color-link);
|
|
98
156
|
}
|
|
99
157
|
}
|
|
100
158
|
|
|
101
159
|
.wg_shellNavList__item--current {
|
|
102
160
|
&::after {
|
|
103
|
-
border-left-color: var(--app-color-
|
|
161
|
+
border-left-color: var(--app-color-link);
|
|
104
162
|
}
|
|
105
163
|
|
|
106
164
|
& > .wg_shellNavList__branch::before {
|
|
107
|
-
border-top-color: var(--app-color-
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
.wg_shellNavList__link {
|
|
112
|
-
/* 가로 가지는 링크 첫 줄 기준 · 여러 줄 제목에서도 연결 위치 유지 */
|
|
113
|
-
position: relative;
|
|
114
|
-
display: block;
|
|
115
|
-
padding: var(--app-space-tree-row) 0 var(--app-space-tree-row) calc(var(--app-space-tree-indent) + var(--app-space-inline));
|
|
116
|
-
color: var(--app-color-text);
|
|
117
|
-
font-size: var(--app-font-size-dense);
|
|
118
|
-
line-height: var(--app-line-height-tight);
|
|
119
|
-
text-decoration: none;
|
|
120
|
-
overflow-wrap: anywhere;
|
|
121
|
-
|
|
122
|
-
&::before {
|
|
123
|
-
position: absolute;
|
|
124
|
-
top: calc(0.5lh + var(--app-space-tree-row));
|
|
125
|
-
left: 0;
|
|
126
|
-
width: var(--app-space-tree-indent);
|
|
127
|
-
border-top: 1px solid var(--app-color-border-soft);
|
|
128
|
-
content: "";
|
|
129
|
-
pointer-events: none;
|
|
165
|
+
border-top-color: var(--app-color-link);
|
|
130
166
|
}
|
|
131
167
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
&:where(:hover)::before {
|
|
137
|
-
border-top-color: currentcolor;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/* hover와 같은 밑줄·가지선으로 키보드 위치 표시 */
|
|
141
|
-
&:where(:focus-visible) {
|
|
142
|
-
outline: none;
|
|
143
|
-
text-decoration: underline;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
&:where(:focus-visible)::before {
|
|
147
|
-
border-top-color: currentcolor;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
.wg_shellNavList__link--active {
|
|
152
|
-
color: var(--app-color-text-strong);
|
|
153
|
-
font-weight: var(--app-font-weight-active);
|
|
154
|
-
|
|
155
|
-
&::before {
|
|
156
|
-
border-top-color: currentcolor;
|
|
168
|
+
& > .wg_shellNavList__link::before {
|
|
169
|
+
border-top-color: var(--app-color-link);
|
|
157
170
|
}
|
|
158
171
|
}
|
|
@@ -19,8 +19,12 @@ export interface WgShellNavListProps {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export const WgShellNavList = (props: WgShellNavListProps) => {
|
|
22
|
+
const currentAncestors = new Set(props.currentDoc?.ancestors);
|
|
22
23
|
const entries = [
|
|
23
|
-
...props.group.docs.map((doc) =>
|
|
24
|
+
...props.group.docs.map((doc) => {
|
|
25
|
+
const active = doc.id === props.currentDoc?.id;
|
|
26
|
+
return {kind: "document" as const, key: `doc:${doc.id}`, doc, active, current: active || currentAncestors.has(doc.id)};
|
|
27
|
+
}),
|
|
24
28
|
...props.group.groups.map((group) => ({
|
|
25
29
|
kind: "group" as const,
|
|
26
30
|
key: `group:${JSON.stringify(group.path)}`,
|
|
@@ -44,17 +48,28 @@ export const WgShellNavList = (props: WgShellNavListProps) => {
|
|
|
44
48
|
key={entry.key}
|
|
45
49
|
>
|
|
46
50
|
{/**
|
|
47
|
-
*
|
|
51
|
+
* 문서 링크 · 부모도 독립 페이지, 현재 한 문서에만 페이지 상태 부여
|
|
48
52
|
*/}
|
|
49
53
|
{entry.kind === "document" && (
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
<Fragment>
|
|
55
|
+
<a
|
|
56
|
+
className={clsx("wg_shellNavList__link", {"wg_shellNavList__link--active": entry.active})}
|
|
57
|
+
href={`/${entry.doc.id}/`}
|
|
58
|
+
title={entry.doc.data.label}
|
|
59
|
+
aria-current={entry.active ? "page" : undefined}
|
|
60
|
+
>
|
|
61
|
+
{entry.doc.data.name}
|
|
62
|
+
</a>
|
|
63
|
+
{/**
|
|
64
|
+
* 하위 문서 · 부모 아래에서도 기존 목록의 들여쓰기와 키보드 이동 유지
|
|
65
|
+
*/}
|
|
66
|
+
{entry.doc.children !== undefined && (
|
|
67
|
+
<WgShellNavList
|
|
68
|
+
group={{name: entry.doc.data.name, path: entry.doc.data.group, docs: entry.doc.children, groups: []}}
|
|
69
|
+
currentDoc={props.currentDoc}
|
|
70
|
+
/>
|
|
71
|
+
)}
|
|
72
|
+
</Fragment>
|
|
58
73
|
)}
|
|
59
74
|
{/**
|
|
60
75
|
* 하위 묶음 · 같은 목록을 재귀로 이어 깊이에 따라 들여쓰기
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* 읽는 순서의 문서 목록 ·
|
|
2
|
+
* 읽는 순서의 문서 목록 · 작은 회색 묶음과 링크색 문서
|
|
3
3
|
*/
|
|
4
4
|
.wg_shellNav__root {
|
|
5
5
|
font-size: var(--app-font-size-dense);
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
.wg_shellNav__group {
|
|
16
16
|
margin: 0 0 4px;
|
|
17
17
|
color: var(--app-color-text-muted);
|
|
18
|
-
font-size: var(--app-font-size-
|
|
18
|
+
font-size: var(--app-font-size-group);
|
|
19
|
+
line-height: calc(var(--app-font-size-dense) * var(--app-line-height-tight));
|
|
19
20
|
text-box: trim-start cap alphabetic;
|
|
20
21
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* 문서 탐색 아래 목차 ·
|
|
2
|
+
* 문서 탐색 아래 목차 · 회색 제목·묶음과 본문 글색의 이동 대상 구분
|
|
3
3
|
*/
|
|
4
4
|
.wg_shellToc__root {
|
|
5
5
|
margin-top: var(--app-space-group);
|
|
@@ -13,14 +13,15 @@
|
|
|
13
13
|
margin-bottom: var(--app-space-inline);
|
|
14
14
|
color: var(--app-color-text-muted);
|
|
15
15
|
font-size: var(--app-font-size-label);
|
|
16
|
-
font-weight: var(--app-font-weight-
|
|
16
|
+
font-weight: var(--app-font-weight-strong);
|
|
17
17
|
letter-spacing: var(--app-letter-spacing-label);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
.wg_shellToc__group {
|
|
21
21
|
margin: var(--app-space-stack) 0 4px;
|
|
22
22
|
color: var(--app-color-text-muted);
|
|
23
|
-
font-size: var(--app-font-size-
|
|
23
|
+
font-size: var(--app-font-size-group);
|
|
24
|
+
line-height: calc(var(--app-font-size-dense) * var(--app-line-height-tight));
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
.wg_shellToc__list {
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
position: relative;
|
|
35
36
|
display: block;
|
|
36
37
|
padding: var(--app-space-tree-row) 0 var(--app-space-tree-row) calc(var(--app-space-tree-indent) + var(--app-space-inline));
|
|
37
|
-
color: var(--app-color-text
|
|
38
|
+
color: var(--app-color-text);
|
|
38
39
|
font-size: var(--app-font-size-dense);
|
|
39
40
|
line-height: var(--app-line-height-tight);
|
|
40
41
|
text-decoration: none;
|
|
@@ -222,7 +223,7 @@
|
|
|
222
223
|
position: relative;
|
|
223
224
|
display: block;
|
|
224
225
|
padding: var(--app-space-tree-row) 0 var(--app-space-tree-row) calc(var(--app-space-tree-indent) + var(--app-space-inline));
|
|
225
|
-
color: var(--app-color-text
|
|
226
|
+
color: var(--app-color-text);
|
|
226
227
|
font-size: var(--app-font-size-detail);
|
|
227
228
|
line-height: var(--app-line-height-tight);
|
|
228
229
|
text-decoration: none;
|
package/src/constant/copy.ts
CHANGED
|
@@ -35,7 +35,7 @@ export const copy_home_missing = "문서 폴더에 README.md를 추가하면 이
|
|
|
35
35
|
/**
|
|
36
36
|
* 현재 문서 목차 라벨
|
|
37
37
|
*/
|
|
38
|
-
export const copy_nav_toc_label = "
|
|
38
|
+
export const copy_nav_toc_label = "On this page";
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* 대소문자만 다른 README 중복 오류
|
|
@@ -82,6 +82,21 @@ export const copy_error_doc_id = "문서 id에 사용할 수 없는 경로";
|
|
|
82
82
|
*/
|
|
83
83
|
export const copy_error_doc_id_clash = "문서 id가 겹친다";
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* 누락되거나 홈을 가리키는 부모 문서 참조
|
|
87
|
+
*/
|
|
88
|
+
export const copy_error_doc_parent_missing = "부모 문서가 없다";
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 자기 참조 또는 순환하는 문서 부모 경로
|
|
92
|
+
*/
|
|
93
|
+
export const copy_error_doc_parent_cycle = "문서 parent가 순환한다";
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 분류 경로가 다른 문서 사이의 부모 참조
|
|
97
|
+
*/
|
|
98
|
+
export const copy_error_doc_parent_group = "부모 문서와 group이 다르다";
|
|
99
|
+
|
|
85
100
|
/**
|
|
86
101
|
* 흐름도 렌더링 실패
|
|
87
102
|
*/
|
|
@@ -160,6 +160,133 @@ test("nested groups follow metadata paths and keep sibling branches and document
|
|
|
160
160
|
assert.doesNotMatch(html, /<details\b/);
|
|
161
161
|
});
|
|
162
162
|
|
|
163
|
+
test("parent is an optional nonempty document ID", () => {
|
|
164
|
+
const data = {name: "Child", label: "Child", group: "Guide"};
|
|
165
|
+
|
|
166
|
+
assert.equal(docDataSchema.parse(data).parent, undefined);
|
|
167
|
+
assert.equal(docDataSchema.parse({...data, parent: " guide/settings "}).parent, "guide/settings");
|
|
168
|
+
for (const parent of ["", " ", null, 1, [], ["guide"]]) {
|
|
169
|
+
assert.equal(docDataSchema.safeParse({...data, parent}).success, false);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("parent documents contain ordered descendants with independent URLs and one current page", async (t) => {
|
|
174
|
+
const root = await mkdtemp(join(tmpdir(), "for-humanity-parents-"));
|
|
175
|
+
const site = siteConfigSchema.parse({navigation: [["Projects", "Atlas", "Guide"]]});
|
|
176
|
+
|
|
177
|
+
t.after(() => rm(root, {recursive: true, force: true}));
|
|
178
|
+
await mkdir(join(root, "guide"));
|
|
179
|
+
await Promise.all([
|
|
180
|
+
writeFile(join(root, "settings.md"), "---\nname: Settings\nlabel: Settings\ngroup: [Projects, Atlas, Guide]\norder: 10\n---\n\nSettings introduction.\n"),
|
|
181
|
+
writeFile(join(root, "guide/site.md"), "---\nname: Site\nlabel: Site\ngroup: [Projects, Atlas, Guide]\nparent: settings\norder: 20\n---\n\nSite introduction.\n"),
|
|
182
|
+
writeFile(join(root, "themes.md"), "---\nname: Themes\nlabel: Themes\ngroup: [Projects, Atlas, Guide]\nparent: settings\norder: 10\n---\n\nThemes introduction.\n"),
|
|
183
|
+
writeFile(join(root, "details.md"), "---\nname: Details\nlabel: Details\ngroup: [Projects, Atlas, Guide]\nparent: guide/site\n---\n\nDeep child.\n"),
|
|
184
|
+
writeFile(join(root, "writing.md"), "---\nname: Writing\nlabel: Writing\ngroup: [Projects, Atlas, Guide]\norder: 20\n---\n\nIndependent guide.\n"),
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
const docs = await readDocs({root, processor: createProcessor({site, root})});
|
|
188
|
+
const groups = toDocGroups({docs, navigation: site.navigation});
|
|
189
|
+
const guide = groups[0].groups[0].groups[0];
|
|
190
|
+
|
|
191
|
+
assert.deepEqual(
|
|
192
|
+
guide.docs.map((doc) => doc.id),
|
|
193
|
+
["settings", "writing"],
|
|
194
|
+
);
|
|
195
|
+
assert.ok(guide.docs[0].children);
|
|
196
|
+
assert.deepEqual(
|
|
197
|
+
guide.docs[0].children.map((doc) => doc.id),
|
|
198
|
+
["themes", "guide/site"],
|
|
199
|
+
);
|
|
200
|
+
assert.ok(guide.docs[0].children[1].children);
|
|
201
|
+
assert.deepEqual(
|
|
202
|
+
guide.docs[0].children[1].children.map((doc) => doc.id),
|
|
203
|
+
["details"],
|
|
204
|
+
);
|
|
205
|
+
assert.deepEqual(docs.find((doc) => doc.id === "details")?.ancestors, ["settings", "guide/site"]);
|
|
206
|
+
assert.deepEqual(toDocGroups({docs: docs.toReversed(), navigation: site.navigation}), groups);
|
|
207
|
+
|
|
208
|
+
const html = renderToStaticMarkup(WgShellNav({site, docs, current: "details"}));
|
|
209
|
+
assert.match(html, /href="\/settings\/"[^>]*>Settings<\/a><ul\b[^>]*aria-label="Settings"/);
|
|
210
|
+
assert.match(html, /href="\/guide\/site\/"[^>]*>Site<\/a><ul\b[^>]*aria-label="Site"/);
|
|
211
|
+
assert.match(html, /href="\/details\/"[^>]*aria-current="page"/);
|
|
212
|
+
assert.equal([...html.matchAll(/aria-current="page"/g)].length, 1);
|
|
213
|
+
assert.equal([...html.matchAll(/wg_shellNavList__link--active/g)].length, 1);
|
|
214
|
+
assert.equal([...html.matchAll(/wg_shellNavList__item--current/g)].length, 5);
|
|
215
|
+
for (const doc of docs) {
|
|
216
|
+
assert.equal(html.split(`href="/${doc.id}/"`).length - 1, 1);
|
|
217
|
+
}
|
|
218
|
+
assert.ok(html.indexOf('href="/themes/"') < html.indexOf('href="/guide/site/"'));
|
|
219
|
+
assert.doesNotMatch(renderToStaticMarkup(WgShellNav({site, docs})), /aria-current="page"|wg_shellNavList__item--current/);
|
|
220
|
+
|
|
221
|
+
const app = createApp({site, store: {docs}, assets: {stylePath: asset_style_path, clientPath: asset_client_path, fontCss: "", preload: [], reload: false}});
|
|
222
|
+
for (const doc of docs) {
|
|
223
|
+
assert.equal((await app.request(`/${doc.id}/`)).status, 200);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("relative parent IDs work with both the whole collection and a project document root", async (t) => {
|
|
228
|
+
const root = await mkdtemp(join(tmpdir(), "for-humanity-parents-"));
|
|
229
|
+
const site = siteConfigSchema.parse({});
|
|
230
|
+
t.after(() => rm(root, {recursive: true, force: true}));
|
|
231
|
+
|
|
232
|
+
await mkdir(join(root, "project/research"), {recursive: true});
|
|
233
|
+
await mkdir(join(root, "project/guide"));
|
|
234
|
+
await Promise.all([
|
|
235
|
+
writeFile(join(root, "project/research/README.md"), "---\nname: Research\nlabel: Research\ngroup: Guide\n---\n\nResearch introduction.\n"),
|
|
236
|
+
writeFile(join(root, "project/research/parts.md"), "---\nname: Parts\nlabel: Parts\ngroup: Guide\nparent: ./readme\n---\n\nText.\n"),
|
|
237
|
+
writeFile(join(root, "project/guide/authoring.md"), "---\nname: Authoring\nlabel: Authoring\ngroup: Guide\nparent: ../research/readme\n---\n\nText.\n"),
|
|
238
|
+
]);
|
|
239
|
+
for (const scope of [root, join(root, "project")]) {
|
|
240
|
+
const docs = await readDocs({root: scope, processor: createProcessor({site, root: scope})});
|
|
241
|
+
const parentId = scope === root ? "project/research/readme" : "research/readme";
|
|
242
|
+
const groups = toDocGroups({docs, navigation: site.navigation});
|
|
243
|
+
assert.deepEqual(
|
|
244
|
+
groups[0].docs.map((doc) => doc.id),
|
|
245
|
+
[parentId],
|
|
246
|
+
);
|
|
247
|
+
assert.ok(groups[0].docs[0].children);
|
|
248
|
+
assert.equal(groups[0].docs[0].children.length, 2);
|
|
249
|
+
assert.ok(groups[0].docs[0].children.every((doc) => doc.data.parent === parentId));
|
|
250
|
+
assert.ok(groups[0].docs[0].children.every((doc) => doc.ancestors.length === 1 && doc.ancestors[0] === parentId));
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("missing and home document parents fail when loading documents", async (t) => {
|
|
255
|
+
const root = await mkdtemp(join(tmpdir(), "for-humanity-parents-"));
|
|
256
|
+
const site = siteConfigSchema.parse({});
|
|
257
|
+
t.after(() => rm(root, {recursive: true, force: true}));
|
|
258
|
+
|
|
259
|
+
await writeFile(join(root, "README.md"), "# Home\n");
|
|
260
|
+
for (const parent of ["missing", "readme", "guide.md", "/guide/"]) {
|
|
261
|
+
await writeFile(join(root, "child.md"), `---\nname: Child\nlabel: Child\ngroup: Guide\nparent: ${parent}\n---\n\nText.\n`);
|
|
262
|
+
await assert.rejects(readDocs({root, processor: createProcessor({site, root})}), /부모 문서가 없다: child/);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("self references and cycles fail when loading documents", async (t) => {
|
|
267
|
+
const root = await mkdtemp(join(tmpdir(), "for-humanity-parents-"));
|
|
268
|
+
const site = siteConfigSchema.parse({});
|
|
269
|
+
t.after(() => rm(root, {recursive: true, force: true}));
|
|
270
|
+
|
|
271
|
+
await writeFile(join(root, "first.md"), "---\nname: First\nlabel: First\ngroup: Guide\nparent: first\n---\n\nText.\n");
|
|
272
|
+
await assert.rejects(readDocs({root, processor: createProcessor({site, root})}), /문서 parent가 순환한다/);
|
|
273
|
+
|
|
274
|
+
await writeFile(join(root, "first.md"), "---\nname: First\nlabel: First\ngroup: Guide\nparent: second\n---\n\nText.\n");
|
|
275
|
+
await writeFile(join(root, "second.md"), "---\nname: Second\nlabel: Second\ngroup: Guide\nparent: third\n---\n\nText.\n");
|
|
276
|
+
await writeFile(join(root, "third.md"), "---\nname: Third\nlabel: Third\ngroup: Guide\nparent: first\n---\n\nText.\n");
|
|
277
|
+
await assert.rejects(readDocs({root, processor: createProcessor({site, root})}), /문서 parent가 순환한다/);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("parent and child must use the same complete group path", async (t) => {
|
|
281
|
+
const root = await mkdtemp(join(tmpdir(), "for-humanity-parents-"));
|
|
282
|
+
const site = siteConfigSchema.parse({});
|
|
283
|
+
t.after(() => rm(root, {recursive: true, force: true}));
|
|
284
|
+
|
|
285
|
+
await writeFile(join(root, "parent.md"), "---\nname: Parent\nlabel: Parent\ngroup: [Projects, Atlas, Research]\n---\n\nText.\n");
|
|
286
|
+
await writeFile(join(root, "child.md"), "---\nname: Child\nlabel: Child\ngroup: [Projects, Beacon, Research]\nparent: parent\n---\n\nText.\n");
|
|
287
|
+
await assert.rejects(readDocs({root, processor: createProcessor({site, root})}), /부모 문서와 group이 다르다: child/);
|
|
288
|
+
});
|
|
289
|
+
|
|
163
290
|
test("frontmatter supports BOM, CRLF and a closing fence at EOF", async (t) => {
|
|
164
291
|
const root = await mkdtemp(join(tmpdir(), "for-humanity-docs-"));
|
|
165
292
|
const site = siteConfigSchema.parse({});
|
package/src/content/read-docs.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import {readdir, readFile} from "node:fs/promises";
|
|
2
|
-
import {join, sep} from "node:path";
|
|
2
|
+
import {join, posix, sep} from "node:path";
|
|
3
3
|
import {VFile} from "vfile";
|
|
4
4
|
import {parse as parseYaml} from "yaml";
|
|
5
5
|
import {asset_dir, asset_favicon_path} from "@/constant/asset";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
copy_error_doc_id,
|
|
8
|
+
copy_error_doc_id_clash,
|
|
9
|
+
copy_error_doc_parent_cycle,
|
|
10
|
+
copy_error_doc_parent_group,
|
|
11
|
+
copy_error_doc_parent_missing,
|
|
12
|
+
copy_error_frontmatter,
|
|
13
|
+
} from "@/constant/copy";
|
|
7
14
|
import type {Processor} from "@/content/create-processor";
|
|
8
15
|
import type {Doc} from "@/type/doc";
|
|
9
16
|
import {docDataSchema} from "@/type/doc-data";
|
|
@@ -49,8 +56,14 @@ export const readDocs = async (options: {root: string; processor: Processor}): P
|
|
|
49
56
|
throw new Error(`${copy_error_frontmatter}: ${path}: ${parsed.error.issues.map((issue) => `${issue.path.join(".")} ${issue.message}`).join(", ")}`);
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
// 상대 부모 id는 파일 위치 기준으로 한 번 해소 · 문서 루트를 바꿔도 같은 관계 유지
|
|
60
|
+
const data = {
|
|
61
|
+
...parsed.data,
|
|
62
|
+
parent: parsed.data.parent !== undefined && /^\.\.?\//.test(parsed.data.parent) ? posix.join(posix.dirname(id), parsed.data.parent) : parsed.data.parent,
|
|
63
|
+
};
|
|
64
|
+
|
|
52
65
|
// 플러그인의 제목·절 정보 공유
|
|
53
|
-
const fh: DocFileData = {frontmatter:
|
|
66
|
+
const fh: DocFileData = {frontmatter: data, headings: [], sections: []};
|
|
54
67
|
file.data.fh = fh;
|
|
55
68
|
|
|
56
69
|
// YAML 노드만 제외 · 본문 위치와 오류의 원본 줄·열·offset 유지
|
|
@@ -59,7 +72,8 @@ export const readDocs = async (options: {root: string; processor: Processor}): P
|
|
|
59
72
|
|
|
60
73
|
return {
|
|
61
74
|
id,
|
|
62
|
-
data
|
|
75
|
+
data,
|
|
76
|
+
ancestors: [],
|
|
63
77
|
html: options.processor.stringify(rendered, file),
|
|
64
78
|
outline: {headings: fh.headings, sections: fh.sections},
|
|
65
79
|
};
|
|
@@ -71,5 +85,25 @@ export const readDocs = async (options: {root: string; processor: Processor}): P
|
|
|
71
85
|
throw new Error(`${copy_error_doc_id_clash}: ${duplicateIds.map((entry) => entry[0]).join(", ")}`);
|
|
72
86
|
}
|
|
73
87
|
|
|
74
|
-
|
|
88
|
+
// 모든 파일을 수집한 뒤 부모 참조 검증 · 파일 읽기 순서와 무관한 문서 경로
|
|
89
|
+
const docsById = new Map(docs.map((doc) => [doc.id, doc]));
|
|
90
|
+
return docs.map((doc) => {
|
|
91
|
+
const ancestors = new Set([doc.id]);
|
|
92
|
+
for (let parentId = doc.data.parent; parentId !== undefined; ) {
|
|
93
|
+
const parent = docsById.get(parentId);
|
|
94
|
+
if (parent === undefined) {
|
|
95
|
+
throw new Error(`${copy_error_doc_parent_missing}: ${doc.id} → ${parentId}`);
|
|
96
|
+
}
|
|
97
|
+
if (ancestors.has(parent.id)) {
|
|
98
|
+
throw new Error(`${copy_error_doc_parent_cycle}: ${[...ancestors, parent.id].join(" → ")}`);
|
|
99
|
+
}
|
|
100
|
+
if (JSON.stringify(parent.data.group) !== JSON.stringify(doc.data.group)) {
|
|
101
|
+
throw new Error(`${copy_error_doc_parent_group}: ${doc.id} → ${parent.id}`);
|
|
102
|
+
}
|
|
103
|
+
ancestors.add(parent.id);
|
|
104
|
+
parentId = parent.data.parent;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {...doc, ancestors: [...ancestors].slice(1).toReversed()};
|
|
108
|
+
});
|
|
75
109
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {locale_doc_name} from "@/constant/locale";
|
|
2
|
+
import type {Doc} from "@/type/doc";
|
|
3
|
+
import type {DocBranch} from "@/type/doc-branch";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 검증된 부모 관계로 문서 가지를 재귀 구성 · 형제 순서와 URL 유지
|
|
7
|
+
*/
|
|
8
|
+
export const toDocBranches = (options: {docs: readonly Doc[]; children: ReadonlyMap<Doc["data"]["parent"], Doc[]>}): DocBranch[] => {
|
|
9
|
+
return options.docs
|
|
10
|
+
.toSorted((a, b) => a.data.order - b.data.order || a.data.name.localeCompare(b.data.name, locale_doc_name) || a.id.localeCompare(b.id, locale_doc_name))
|
|
11
|
+
.map((doc) => {
|
|
12
|
+
const children = options.children.get(doc.id);
|
|
13
|
+
return {...doc, children: children === undefined ? undefined : toDocBranches({docs: children, children: options.children})};
|
|
14
|
+
});
|
|
15
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {locale_doc_group
|
|
1
|
+
import {locale_doc_group} from "@/constant/locale";
|
|
2
|
+
import {toDocBranches} from "@/content/to-doc-groups/_to-doc-branches";
|
|
2
3
|
import type {Doc} from "@/type/doc";
|
|
3
4
|
import type {DocGroup} from "@/type/doc-group";
|
|
4
5
|
|
|
@@ -10,6 +11,10 @@ interface DocGroupBranchesOptions {
|
|
|
10
11
|
* 부모 경로에 속한 문서
|
|
11
12
|
*/
|
|
12
13
|
docs: readonly Doc[];
|
|
14
|
+
/**
|
|
15
|
+
* 검증된 부모 id별 문서 · 자손을 각 단계에서 다시 수집하지 않음
|
|
16
|
+
*/
|
|
17
|
+
children: ReadonlyMap<Doc["data"]["parent"], Doc[]>;
|
|
13
18
|
/**
|
|
14
19
|
* 부모까지의 경로 · 최상위는 빈 배열
|
|
15
20
|
*/
|
|
@@ -49,10 +54,17 @@ export const toDocGroupBranches = (options: DocGroupBranchesOptions): DocGroup[]
|
|
|
49
54
|
return {
|
|
50
55
|
name: group.name,
|
|
51
56
|
path: group.path,
|
|
52
|
-
docs:
|
|
53
|
-
.filter((doc) => doc.data.group.length === group.path.length)
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
docs: toDocBranches({
|
|
58
|
+
docs: group.docs.filter((doc) => doc.data.group.length === group.path.length && doc.data.parent === undefined),
|
|
59
|
+
children: options.children,
|
|
60
|
+
}),
|
|
61
|
+
groups: toDocGroupBranches({
|
|
62
|
+
docs: group.docs,
|
|
63
|
+
children: options.children,
|
|
64
|
+
path: group.path,
|
|
65
|
+
positions: options.positions,
|
|
66
|
+
unlistedPosition: options.unlistedPosition,
|
|
67
|
+
}),
|
|
56
68
|
};
|
|
57
69
|
});
|
|
58
70
|
};
|
|
@@ -10,5 +10,6 @@ export const toDocGroups = (options: {docs: readonly Doc[]; navigation: SiteConf
|
|
|
10
10
|
// 같은 조상을 지정한 경로는 최초 navigation 위치를 우선
|
|
11
11
|
const positions = new Map(options.navigation.flatMap((path, index) => path.map((_name, depth) => [JSON.stringify(path.slice(0, depth + 1)), index] as const)).toReversed());
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
const children = Map.groupBy(options.docs, (doc) => doc.data.parent);
|
|
14
|
+
return toDocGroupBranches({docs: options.docs, children, path: [], positions, unlistedPosition: options.navigation.length});
|
|
14
15
|
};
|
package/src/style/token.css
CHANGED
|
@@ -55,7 +55,8 @@
|
|
|
55
55
|
--app-font-mono: "JetBrains Mono Variable", var(--app-font-sans);
|
|
56
56
|
--app-font-brand: "Architects Daughter", var(--app-font-sans);
|
|
57
57
|
|
|
58
|
-
/* 글자 크기 · 12px 기본과 역할별 제목 크기 */
|
|
58
|
+
/* 글자 크기 · 11px 비상호작용 묶음 · 12px 기본과 역할별 제목 크기 */
|
|
59
|
+
--app-font-size-group: 11px;
|
|
59
60
|
--app-font-size-label: 12px;
|
|
60
61
|
--app-font-size-detail: 12px;
|
|
61
62
|
--app-font-size-dense: 12px;
|
|
@@ -65,9 +66,9 @@
|
|
|
65
66
|
--app-font-size-section: 14px;
|
|
66
67
|
--app-font-size-title: 16px;
|
|
67
68
|
|
|
68
|
-
/* 굵기 · 기본 400 · 현재
|
|
69
|
+
/* 굵기 · 기본 400 · 현재 위치·제목·강조 600 */
|
|
69
70
|
--app-font-weight-mark: 400;
|
|
70
|
-
--app-font-weight-active:
|
|
71
|
+
--app-font-weight-active: 600;
|
|
71
72
|
--app-font-weight-strong: 600;
|
|
72
73
|
--app-font-weight-title: 600;
|
|
73
74
|
|
package/src/type/doc-data.ts
CHANGED
|
@@ -22,6 +22,10 @@ export const docDataSchema = z.object({
|
|
|
22
22
|
* 사이드바의 문서 묶음 경로 · 문자열도 배열로 정규화
|
|
23
23
|
*/
|
|
24
24
|
group: groupPathSchema,
|
|
25
|
+
/**
|
|
26
|
+
* 같은 묶음 안의 부모 문서 id · 확장자 제외, ./·../ 상대 경로도 허용
|
|
27
|
+
*/
|
|
28
|
+
parent: z.string().trim().min(1).optional(),
|
|
25
29
|
/**
|
|
26
30
|
* 묶음 안의 읽는 순서 · 미지정 문서는 뒤에서 이름순
|
|
27
31
|
*/
|
package/src/type/doc-group.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {Doc} from "@/type/doc";
|
|
2
|
+
import type {DocBranch} from "@/type/doc-branch";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* 사이드바의 문서 묶음 · 같은 이름도 전체 경로로 구분
|
|
@@ -13,9 +14,9 @@ export interface DocGroup {
|
|
|
13
14
|
*/
|
|
14
15
|
path: Doc["data"]["group"];
|
|
15
16
|
/**
|
|
16
|
-
* 현재
|
|
17
|
+
* 현재 묶음의 부모 없는 문서 · 하위 문서는 각 문서 가지에 포함
|
|
17
18
|
*/
|
|
18
|
-
docs:
|
|
19
|
+
docs: DocBranch[];
|
|
19
20
|
/**
|
|
20
21
|
* 현재 묶음의 하위 묶음 · navigation·이름 순
|
|
21
22
|
*/
|