for-humanity 0.2.1 → 0.4.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 +20 -0
- package/README.md +6 -5
- package/dist/cli.css +66 -54
- package/dist/cli.js +183 -55
- package/dist/client.js +2 -2
- package/package.json +1 -1
- package/src/cli.ts +18 -7
- package/src/client.ts +12 -1
- package/src/component/widget/shell/_wg-shell-nav-list.css +70 -57
- package/src/component/widget/shell/_wg-shell-nav-list.tsx +25 -10
- package/src/component/widget/shell/_wg-shell-nav.css +3 -2
- package/src/component/widget/shell/_wg-shell-toc.css +6 -5
- package/src/constant/asset.ts +20 -0
- package/src/constant/copy.ts +21 -1
- package/src/constant/doc.ts +2 -2
- package/src/content/create-processor.ts +3 -1
- package/src/content/read-docs.test.ts +180 -3
- package/src/content/read-docs.ts +39 -5
- package/src/content/rehype-assets.ts +70 -0
- package/src/content/to-doc-groups/_to-doc-branches.ts +15 -0
- package/src/content/to-doc-groups/_to-doc-group-branches.ts +17 -5
- package/src/content/to-doc-groups/to-doc-groups.ts +2 -1
- package/src/dev.ts +1 -1
- package/src/style/token.css +4 -3
- package/src/type/doc-branch.ts +11 -0
- package/src/type/doc-data.ts +5 -1
- package/src/type/doc-group.ts +3 -2
- package/src/type/doc.ts +4 -0
package/src/content/read-docs.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import {readdir, readFile} from "node:fs/promises";
|
|
2
|
-
import {join, sep} from "node:path";
|
|
2
|
+
import {join, posix, sep} from "node:path";
|
|
3
3
|
import {VFile} from "vfile";
|
|
4
4
|
import {parse as parseYaml} from "yaml";
|
|
5
5
|
import {asset_dir, asset_favicon_path} from "@/constant/asset";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
copy_error_doc_id,
|
|
8
|
+
copy_error_doc_id_clash,
|
|
9
|
+
copy_error_doc_parent_cycle,
|
|
10
|
+
copy_error_doc_parent_group,
|
|
11
|
+
copy_error_doc_parent_missing,
|
|
12
|
+
copy_error_frontmatter,
|
|
13
|
+
} from "@/constant/copy";
|
|
7
14
|
import type {Processor} from "@/content/create-processor";
|
|
8
15
|
import type {Doc} from "@/type/doc";
|
|
9
16
|
import {docDataSchema} from "@/type/doc-data";
|
|
@@ -49,8 +56,14 @@ export const readDocs = async (options: {root: string; processor: Processor}): P
|
|
|
49
56
|
throw new Error(`${copy_error_frontmatter}: ${path}: ${parsed.error.issues.map((issue) => `${issue.path.join(".")} ${issue.message}`).join(", ")}`);
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
// 상대 부모 id는 파일 위치 기준으로 한 번 해소 · 문서 루트를 바꿔도 같은 관계 유지
|
|
60
|
+
const data = {
|
|
61
|
+
...parsed.data,
|
|
62
|
+
parent: parsed.data.parent !== undefined && /^\.\.?\//.test(parsed.data.parent) ? posix.join(posix.dirname(id), parsed.data.parent) : parsed.data.parent,
|
|
63
|
+
};
|
|
64
|
+
|
|
52
65
|
// 플러그인의 제목·절 정보 공유
|
|
53
|
-
const fh: DocFileData = {frontmatter:
|
|
66
|
+
const fh: DocFileData = {frontmatter: data, headings: [], sections: []};
|
|
54
67
|
file.data.fh = fh;
|
|
55
68
|
|
|
56
69
|
// YAML 노드만 제외 · 본문 위치와 오류의 원본 줄·열·offset 유지
|
|
@@ -59,7 +72,8 @@ export const readDocs = async (options: {root: string; processor: Processor}): P
|
|
|
59
72
|
|
|
60
73
|
return {
|
|
61
74
|
id,
|
|
62
|
-
data
|
|
75
|
+
data,
|
|
76
|
+
ancestors: [],
|
|
63
77
|
html: options.processor.stringify(rendered, file),
|
|
64
78
|
outline: {headings: fh.headings, sections: fh.sections},
|
|
65
79
|
};
|
|
@@ -71,5 +85,25 @@ export const readDocs = async (options: {root: string; processor: Processor}): P
|
|
|
71
85
|
throw new Error(`${copy_error_doc_id_clash}: ${duplicateIds.map((entry) => entry[0]).join(", ")}`);
|
|
72
86
|
}
|
|
73
87
|
|
|
74
|
-
|
|
88
|
+
// 모든 파일을 수집한 뒤 부모 참조 검증 · 파일 읽기 순서와 무관한 문서 경로
|
|
89
|
+
const docsById = new Map(docs.map((doc) => [doc.id, doc]));
|
|
90
|
+
return docs.map((doc) => {
|
|
91
|
+
const ancestors = new Set([doc.id]);
|
|
92
|
+
for (let parentId = doc.data.parent; parentId !== undefined; ) {
|
|
93
|
+
const parent = docsById.get(parentId);
|
|
94
|
+
if (parent === undefined) {
|
|
95
|
+
throw new Error(`${copy_error_doc_parent_missing}: ${doc.id} → ${parentId}`);
|
|
96
|
+
}
|
|
97
|
+
if (ancestors.has(parent.id)) {
|
|
98
|
+
throw new Error(`${copy_error_doc_parent_cycle}: ${[...ancestors, parent.id].join(" → ")}`);
|
|
99
|
+
}
|
|
100
|
+
if (JSON.stringify(parent.data.group) !== JSON.stringify(doc.data.group)) {
|
|
101
|
+
throw new Error(`${copy_error_doc_parent_group}: ${doc.id} → ${parent.id}`);
|
|
102
|
+
}
|
|
103
|
+
ancestors.add(parent.id);
|
|
104
|
+
parentId = parent.data.parent;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {...doc, ancestors: [...ancestors].slice(1).toReversed()};
|
|
108
|
+
});
|
|
75
109
|
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {existsSync, realpathSync, statSync} from "node:fs";
|
|
2
|
+
import {dirname, extname, isAbsolute, relative, resolve, sep} from "node:path";
|
|
3
|
+
import type {Root} from "hast";
|
|
4
|
+
import {visit} from "unist-util-visit";
|
|
5
|
+
import type {VFile} from "vfile";
|
|
6
|
+
import {asset_media_dir, asset_media_extensions} from "@/constant/asset";
|
|
7
|
+
import {copy_error_asset} from "@/constant/copy";
|
|
8
|
+
import {toAssetPath} from "@/util/file/to-asset-path";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 명시적으로 참조한 로컬 이미지·첨부의 수집 계약
|
|
12
|
+
*/
|
|
13
|
+
export interface AssetOptions {
|
|
14
|
+
/**
|
|
15
|
+
* 자원을 읽을 수 있는 문서 루트
|
|
16
|
+
*/
|
|
17
|
+
root: string;
|
|
18
|
+
/**
|
|
19
|
+
* dev 응답과 정적 복사에서 공유할 URL → 파일 목록
|
|
20
|
+
*/
|
|
21
|
+
files: Map<string, string>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 파일 위치 기준 이미지·첨부 → 내용 지문 URL · 원시 HTML도 같은 규칙
|
|
26
|
+
* 없는 파일, 폴더 밖 경로와 symlink, 지원하지 않는 이미지 형식은 빌드 실패
|
|
27
|
+
* 절대 URL·외부 URL은 작성자가 제공하며 수집하지 않음
|
|
28
|
+
*/
|
|
29
|
+
export const rehypeAssets = (options: AssetOptions) => (tree: Root, file: VFile) => {
|
|
30
|
+
visit(tree, "element", (node) => {
|
|
31
|
+
const property = node.tagName === "img" ? "src" : "href";
|
|
32
|
+
const url = node.properties[property];
|
|
33
|
+
|
|
34
|
+
if ((node.tagName !== "img" && node.tagName !== "a") || typeof url !== "string" || /^(?:[a-z][a-z\d+.-]*:|\/|#)/i.test(url)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const match = /^([^?#]+)([?#].*)?$/.exec(url);
|
|
39
|
+
if (match === null) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const path = decodeURIComponent(match[1]);
|
|
44
|
+
const extension = extname(path).toLowerCase();
|
|
45
|
+
if (!asset_media_extensions.has(extension)) {
|
|
46
|
+
if (node.tagName === "img") {
|
|
47
|
+
file.fail(`${copy_error_asset}: 지원하지 않는 이미지 형식 · ${url}`, node);
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const target = resolve(dirname(file.path), path);
|
|
53
|
+
const location = relative(options.root, target);
|
|
54
|
+
if (location === ".." || location.startsWith(`..${sep}`) || isAbsolute(location)) {
|
|
55
|
+
file.fail(`${copy_error_asset}: 문서 폴더 밖 · ${url}`, node);
|
|
56
|
+
}
|
|
57
|
+
if (!existsSync(target)) {
|
|
58
|
+
file.fail(`${copy_error_asset}: 파일이 없다 · ${url}`, node);
|
|
59
|
+
}
|
|
60
|
+
const actual = realpathSync(target);
|
|
61
|
+
const actualLocation = relative(realpathSync(options.root), actual);
|
|
62
|
+
if (actualLocation === ".." || actualLocation.startsWith(`..${sep}`) || isAbsolute(actualLocation) || !statSync(actual).isFile()) {
|
|
63
|
+
file.fail(`${copy_error_asset}: 문서 폴더 밖이거나 일반 파일이 아니다 · ${url}`, node);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const assetUrl = toAssetPath({path: `${asset_media_dir}/asset${extension}`, file: actual});
|
|
67
|
+
options.files.set(assetUrl, actual);
|
|
68
|
+
node.properties[property] = match[2] === undefined ? assetUrl : `${assetUrl}${match[2]}`;
|
|
69
|
+
});
|
|
70
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {locale_doc_name} from "@/constant/locale";
|
|
2
|
+
import type {Doc} from "@/type/doc";
|
|
3
|
+
import type {DocBranch} from "@/type/doc-branch";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 검증된 부모 관계로 문서 가지를 재귀 구성 · 형제 순서와 URL 유지
|
|
7
|
+
*/
|
|
8
|
+
export const toDocBranches = (options: {docs: readonly Doc[]; children: ReadonlyMap<Doc["data"]["parent"], Doc[]>}): DocBranch[] => {
|
|
9
|
+
return options.docs
|
|
10
|
+
.toSorted((a, b) => a.data.order - b.data.order || a.data.name.localeCompare(b.data.name, locale_doc_name) || a.id.localeCompare(b.id, locale_doc_name))
|
|
11
|
+
.map((doc) => {
|
|
12
|
+
const children = options.children.get(doc.id);
|
|
13
|
+
return {...doc, children: children === undefined ? undefined : toDocBranches({docs: children, children: options.children})};
|
|
14
|
+
});
|
|
15
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {locale_doc_group
|
|
1
|
+
import {locale_doc_group} from "@/constant/locale";
|
|
2
|
+
import {toDocBranches} from "@/content/to-doc-groups/_to-doc-branches";
|
|
2
3
|
import type {Doc} from "@/type/doc";
|
|
3
4
|
import type {DocGroup} from "@/type/doc-group";
|
|
4
5
|
|
|
@@ -10,6 +11,10 @@ interface DocGroupBranchesOptions {
|
|
|
10
11
|
* 부모 경로에 속한 문서
|
|
11
12
|
*/
|
|
12
13
|
docs: readonly Doc[];
|
|
14
|
+
/**
|
|
15
|
+
* 검증된 부모 id별 문서 · 자손을 각 단계에서 다시 수집하지 않음
|
|
16
|
+
*/
|
|
17
|
+
children: ReadonlyMap<Doc["data"]["parent"], Doc[]>;
|
|
13
18
|
/**
|
|
14
19
|
* 부모까지의 경로 · 최상위는 빈 배열
|
|
15
20
|
*/
|
|
@@ -49,10 +54,17 @@ export const toDocGroupBranches = (options: DocGroupBranchesOptions): DocGroup[]
|
|
|
49
54
|
return {
|
|
50
55
|
name: group.name,
|
|
51
56
|
path: group.path,
|
|
52
|
-
docs:
|
|
53
|
-
.filter((doc) => doc.data.group.length === group.path.length)
|
|
54
|
-
|
|
55
|
-
|
|
57
|
+
docs: toDocBranches({
|
|
58
|
+
docs: group.docs.filter((doc) => doc.data.group.length === group.path.length && doc.data.parent === undefined),
|
|
59
|
+
children: options.children,
|
|
60
|
+
}),
|
|
61
|
+
groups: toDocGroupBranches({
|
|
62
|
+
docs: group.docs,
|
|
63
|
+
children: options.children,
|
|
64
|
+
path: group.path,
|
|
65
|
+
positions: options.positions,
|
|
66
|
+
unlistedPosition: options.unlistedPosition,
|
|
67
|
+
}),
|
|
56
68
|
};
|
|
57
69
|
});
|
|
58
70
|
};
|
|
@@ -10,5 +10,6 @@ export const toDocGroups = (options: {docs: readonly Doc[]; navigation: SiteConf
|
|
|
10
10
|
// 같은 조상을 지정한 경로는 최초 navigation 위치를 우선
|
|
11
11
|
const positions = new Map(options.navigation.flatMap((path, index) => path.map((_name, depth) => [JSON.stringify(path.slice(0, depth + 1)), index] as const)).toReversed());
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
const children = Map.groupBy(options.docs, (doc) => doc.data.parent);
|
|
14
|
+
return toDocGroupBranches({docs: options.docs, children, path: [], positions, unlistedPosition: options.navigation.length});
|
|
14
15
|
};
|
package/src/dev.ts
CHANGED
|
@@ -41,7 +41,7 @@ export const createDevApp = (options: DevOptions) => {
|
|
|
41
41
|
return c.notFound();
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
c.header("content-type", asset_content_types[extname(file)] ?? asset_content_type_default);
|
|
44
|
+
c.header("content-type", asset_content_types[extname(file).toLowerCase()] ?? asset_content_type_default);
|
|
45
45
|
|
|
46
46
|
if (extname(file) === ".woff2") {
|
|
47
47
|
c.header("Cache-Control", font_cache_control);
|
package/src/style/token.css
CHANGED
|
@@ -55,7 +55,8 @@
|
|
|
55
55
|
--app-font-mono: "JetBrains Mono Variable", var(--app-font-sans);
|
|
56
56
|
--app-font-brand: "Architects Daughter", var(--app-font-sans);
|
|
57
57
|
|
|
58
|
-
/* 글자 크기 · 12px 기본과 역할별 제목 크기 */
|
|
58
|
+
/* 글자 크기 · 11px 비상호작용 묶음 · 12px 기본과 역할별 제목 크기 */
|
|
59
|
+
--app-font-size-group: 11px;
|
|
59
60
|
--app-font-size-label: 12px;
|
|
60
61
|
--app-font-size-detail: 12px;
|
|
61
62
|
--app-font-size-dense: 12px;
|
|
@@ -65,9 +66,9 @@
|
|
|
65
66
|
--app-font-size-section: 14px;
|
|
66
67
|
--app-font-size-title: 16px;
|
|
67
68
|
|
|
68
|
-
/* 굵기 · 기본 400 · 현재
|
|
69
|
+
/* 굵기 · 기본 400 · 현재 위치·제목·강조 600 */
|
|
69
70
|
--app-font-weight-mark: 400;
|
|
70
|
-
--app-font-weight-active:
|
|
71
|
+
--app-font-weight-active: 600;
|
|
71
72
|
--app-font-weight-strong: 600;
|
|
72
73
|
--app-font-weight-title: 600;
|
|
73
74
|
|
package/src/type/doc-data.ts
CHANGED
|
@@ -15,13 +15,17 @@ export const docDataSchema = z.object({
|
|
|
15
15
|
*/
|
|
16
16
|
label: z.string(),
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
18
|
+
* 기존 type 값 호환 · 생략 권장, 렌더링 차이 없음
|
|
19
19
|
*/
|
|
20
20
|
type: z.enum(doc_type).default(doc_type.document),
|
|
21
21
|
/**
|
|
22
22
|
* 사이드바의 문서 묶음 경로 · 문자열도 배열로 정규화
|
|
23
23
|
*/
|
|
24
24
|
group: groupPathSchema,
|
|
25
|
+
/**
|
|
26
|
+
* 같은 묶음 안의 부모 문서 id · 확장자 제외, ./·../ 상대 경로도 허용
|
|
27
|
+
*/
|
|
28
|
+
parent: z.string().trim().min(1).optional(),
|
|
25
29
|
/**
|
|
26
30
|
* 묶음 안의 읽는 순서 · 미지정 문서는 뒤에서 이름순
|
|
27
31
|
*/
|
package/src/type/doc-group.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {Doc} from "@/type/doc";
|
|
2
|
+
import type {DocBranch} from "@/type/doc-branch";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* 사이드바의 문서 묶음 · 같은 이름도 전체 경로로 구분
|
|
@@ -13,9 +14,9 @@ export interface DocGroup {
|
|
|
13
14
|
*/
|
|
14
15
|
path: Doc["data"]["group"];
|
|
15
16
|
/**
|
|
16
|
-
* 현재
|
|
17
|
+
* 현재 묶음의 부모 없는 문서 · 하위 문서는 각 문서 가지에 포함
|
|
17
18
|
*/
|
|
18
|
-
docs:
|
|
19
|
+
docs: DocBranch[];
|
|
19
20
|
/**
|
|
20
21
|
* 현재 묶음의 하위 묶음 · navigation·이름 순
|
|
21
22
|
*/
|