for-humanity 0.2.0 → 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 CHANGED
@@ -1,5 +1,21 @@
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
+
14
+ ## 0.2.1 — 2026-10-04
15
+
16
+ - Fingerprint stylesheet and browser-script URLs so new pages cannot reuse assets cached from an older deployment.
17
+ - Cache fingerprinted CSS, scripts and fonts with immutable headers on Cloudflare Pages.
18
+
3
19
  ## 0.2.0 — 2026-10-04
4
20
 
5
21
  - Support nested navigation with `group` name arrays while keeping single-name groups compatible.
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.2.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.2.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).
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.2.0` runs checks, publishes the package to npm, then creates a GitHub Release with the same package file.
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-label);
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-text-strong);
387
+ border-left-color: var(--app-color-link);
349
388
  }
350
389
  &:where(:has(:focus-visible))::after {
351
- border-left-color: var(--app-color-text-strong);
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-text-strong);
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-text-strong);
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-text-strong);
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-text-strong);
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-text-strong);
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-text-strong);
421
+ border-left-color: var(--app-color-link);
377
422
  }
378
423
  & > .wg_shellNavList__branch::before {
379
- border-top-color: var(--app-color-text-strong);
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
- &:where(:hover)::before {
404
- border-top-color: currentcolor;
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-label);
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-mark);
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-label);
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-muted);
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-muted);
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: 500;
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) => ({ kind: "document", key: `doc:${doc.id}`, doc, current: doc.id === props.currentDoc?.id })),
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__ */ jsx2(
95
- "a",
96
- {
97
- className: clsx2("wg_shellNavList__link", { "wg_shellNavList__link--active": entry.current }),
98
- href: `/${entry.doc.id}/`,
99
- title: entry.doc.data.label,
100
- "aria-current": entry.current ? "page" : void 0,
101
- children: entry.doc.data.name
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 = "Contents";
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: group.docs.filter((doc) => doc.data.group.length === group.path.length).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)),
161
- groups: toDocGroupBranches({ docs: group.docs, path: group.path, positions: options.positions, unlistedPosition: options.unlistedPosition })
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
- return toDocGroupBranches({ docs: options.docs, path: [], positions, unlistedPosition: options.navigation.length });
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
@@ -227,7 +261,7 @@ var WgShell = (props) => {
227
261
  /* @__PURE__ */ jsx5("style", { dangerouslySetInnerHTML: { __html: props.assets.fontCss } }),
228
262
  props.assets.preload.map((href) => /* @__PURE__ */ jsx5("link", { rel: "preload", href, as: "font", type: "font/woff2", crossOrigin: "" }, href)),
229
263
  /* @__PURE__ */ jsx5("script", { dangerouslySetInnerHTML: { __html: 'document.documentElement.style.scrollBehavior="auto"' } }),
230
- /* @__PURE__ */ jsx5("link", { rel: "stylesheet", href: asset_style_path }),
264
+ /* @__PURE__ */ jsx5("link", { rel: "stylesheet", href: props.assets.stylePath }),
231
265
  props.assets.reload && /* @__PURE__ */ jsx5("script", { dangerouslySetInnerHTML: { __html: reloadScript } })
232
266
  ] }),
233
267
  /* @__PURE__ */ jsxs5("body", { className: clsx5("wg_shell__root"), children: [
@@ -261,7 +295,7 @@ var WgShell = (props) => {
261
295
  /* @__PURE__ */ jsx5("div", { className: clsx5("wg_shell__drawerContent"), "data-navigation-content": "" })
262
296
  ] }),
263
297
  /* @__PURE__ */ jsx5("img", { className: clsx5("wg_shell__cursorFace"), src: asset_favicon_path, width: "24", height: "24", alt: "", "aria-hidden": "true", draggable: false, "data-cursor-face": "" }),
264
- /* @__PURE__ */ jsx5("script", { type: "module", src: asset_client_path })
298
+ /* @__PURE__ */ jsx5("script", { type: "module", src: props.assets.clientPath })
265
299
  ] })
266
300
  ] });
267
301
  };
@@ -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 fh = { frontmatter: parsed.data, headings: [], sections: [] };
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: parsed.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
- return docs;
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
@@ -1169,17 +1230,26 @@ var toErrorMessage = (error) => {
1169
1230
  return error instanceof Error ? error.message : String(error);
1170
1231
  };
1171
1232
 
1172
- // src/util/font/to-font-css.ts
1233
+ // src/util/file/to-asset-path.ts
1173
1234
  import { createHash } from "node:crypto";
1174
1235
  import { readFileSync } from "node:fs";
1236
+ import { posix as posix3 } from "node:path";
1237
+ var toAssetPath = (options) => {
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}`;
1240
+ };
1241
+
1242
+ // src/util/font/to-font-css.ts
1243
+ import { createHash as createHash2 } from "node:crypto";
1244
+ import { readFileSync as readFileSync2 } from "node:fs";
1175
1245
  import { basename, dirname as dirname2, extname as extname2, join as join3 } from "node:path";
1176
1246
  var toFontCss = (options) => {
1177
1247
  const files = /* @__PURE__ */ new Map();
1178
- const css = [...readFileSync(options.css, "utf8").matchAll(/@font-face\s*\{[^}]*\}/g)].map(
1248
+ const css = [...readFileSync2(options.css, "utf8").matchAll(/@font-face\s*\{[^}]*\}/g)].map(
1179
1249
  (match) => match[0].replace(/font-display:\s*[^;]+;/g, `font-display: ${font_display_strategy};`).replace(/url\(([^)]+)\)/g, (_match, relativeUrl) => {
1180
1250
  const file = join3(dirname2(options.css), relativeUrl);
1181
1251
  const extension = extname2(file);
1182
- const hash = createHash("sha256").update(readFileSync(file)).digest("hex").slice(0, font_hash_length);
1252
+ const hash = createHash2("sha256").update(readFileSync2(file)).digest("hex").slice(0, font_hash_length);
1183
1253
  const url = `${options.fontDir}/${basename(file, extension)}.${hash}${extension}`;
1184
1254
  files.set(url, file);
1185
1255
  return `url(${url})`;
@@ -1228,9 +1298,11 @@ var main = async () => {
1228
1298
  throw new Error(`${copy_error_font_preload}: ${font_sans_preload_file}`);
1229
1299
  }
1230
1300
  const fontCss = [sans.css, mono.css, brand.css].join("\n");
1301
+ const stylePath = toAssetPath({ path: asset_style_path, file: join4(kitRoot, "dist/cli.css") });
1302
+ const clientPath = toAssetPath({ path: asset_client_path, file: join4(kitRoot, "dist/client.js") });
1231
1303
  const files = new Map([
1232
- [asset_style_path, join4(kitRoot, "dist/cli.css")],
1233
- [asset_client_path, join4(kitRoot, "dist/client.js")],
1304
+ [stylePath, join4(kitRoot, "dist/cli.css")],
1305
+ [clientPath, join4(kitRoot, "dist/client.js")],
1234
1306
  [asset_favicon_path, join4(kitRoot, "src/asset/favicon.svg")],
1235
1307
  ...sans.files,
1236
1308
  ...mono.files,
@@ -1242,7 +1314,7 @@ var main = async () => {
1242
1314
  const app = createApp({
1243
1315
  site: siteConfig,
1244
1316
  store,
1245
- assets: { fontCss, preload: [...brand.files.keys(), sansPreload[0], ...mono.files.keys()], reload: command === "dev" }
1317
+ assets: { stylePath, clientPath, fontCss, preload: [...brand.files.keys(), sansPreload[0], ...mono.files.keys()], reload: command === "dev" }
1246
1318
  });
1247
1319
  if (command === "build") {
1248
1320
  await rm(outDir, { recursive: true, force: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "for-humanity",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Documents written for humans to read: a docs kit with a sidebar, parts, sections and flowcharts, built on Hono and React",
5
5
  "keywords": [
6
6
  "docs",
package/src/cli.ts CHANGED
@@ -25,6 +25,7 @@ import {readHome} from "@/content/read-home";
25
25
  import {createDevApp} from "@/dev";
26
26
  import {siteConfigSchema} from "@/type/site-config";
27
27
  import {toErrorMessage} from "@/util/error/to-error-message";
28
+ import {toAssetPath} from "@/util/file/to-asset-path";
28
29
  import {toFontCss} from "@/util/font/to-font-css";
29
30
 
30
31
  const commands = ["dev", "build", "preview"];
@@ -82,9 +83,11 @@ const main = async () => {
82
83
  }
83
84
 
84
85
  const fontCss = [sans.css, mono.css, brand.css].join("\n");
86
+ const stylePath = toAssetPath({path: asset_style_path, file: join(kitRoot, "dist/cli.css")});
87
+ const clientPath = toAssetPath({path: asset_client_path, file: join(kitRoot, "dist/client.js")});
85
88
  const files = new Map([
86
- [asset_style_path, join(kitRoot, "dist/cli.css")],
87
- [asset_client_path, join(kitRoot, "dist/client.js")],
89
+ [stylePath, join(kitRoot, "dist/cli.css")],
90
+ [clientPath, join(kitRoot, "dist/client.js")],
88
91
  [asset_favicon_path, join(kitRoot, "src/asset/favicon.svg")],
89
92
  ...sans.files,
90
93
  ...mono.files,
@@ -96,7 +99,7 @@ const main = async () => {
96
99
  const app = createApp({
97
100
  site: siteConfig,
98
101
  store,
99
- assets: {fontCss, preload: [...brand.files.keys(), sansPreload[0], ...mono.files.keys()], reload: command === "dev"},
102
+ assets: {stylePath, clientPath, fontCss, preload: [...brand.files.keys(), sansPreload[0], ...mono.files.keys()], reload: command === "dev"},
100
103
  });
101
104
 
102
105
  if (command === "build") {