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