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