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
|
@@ -13,8 +13,30 @@
|
|
|
13
13
|
display: block;
|
|
14
14
|
padding: var(--app-space-tree-row) 0 var(--app-space-tree-row) calc(var(--app-space-tree-indent) + var(--app-space-inline));
|
|
15
15
|
color: var(--app-color-text-muted);
|
|
16
|
-
font-size: var(--app-font-size-
|
|
16
|
+
font-size: var(--app-font-size-group);
|
|
17
|
+
line-height: calc(var(--app-font-size-dense) * var(--app-line-height-tight));
|
|
18
|
+
overflow-wrap: anywhere;
|
|
19
|
+
|
|
20
|
+
&::before {
|
|
21
|
+
position: absolute;
|
|
22
|
+
top: calc(0.5lh + var(--app-space-tree-row));
|
|
23
|
+
left: 0;
|
|
24
|
+
width: var(--app-space-tree-indent);
|
|
25
|
+
border-top: 1px solid var(--app-color-border-soft);
|
|
26
|
+
content: "";
|
|
27
|
+
pointer-events: none;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.wg_shellNavList__link {
|
|
32
|
+
/* 가로 가지는 링크 첫 줄 기준 · 여러 줄 제목에서도 연결 위치 유지 */
|
|
33
|
+
position: relative;
|
|
34
|
+
display: block;
|
|
35
|
+
padding: var(--app-space-tree-row) 0 var(--app-space-tree-row) calc(var(--app-space-tree-indent) + var(--app-space-inline));
|
|
36
|
+
color: var(--app-color-link);
|
|
37
|
+
font-size: var(--app-font-size-dense);
|
|
17
38
|
line-height: var(--app-line-height-tight);
|
|
39
|
+
text-decoration: none;
|
|
18
40
|
overflow-wrap: anywhere;
|
|
19
41
|
|
|
20
42
|
&::before {
|
|
@@ -26,6 +48,33 @@
|
|
|
26
48
|
content: "";
|
|
27
49
|
pointer-events: none;
|
|
28
50
|
}
|
|
51
|
+
|
|
52
|
+
&:where(:hover) {
|
|
53
|
+
text-decoration: underline;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
&:where(:hover)::before {
|
|
57
|
+
border-top-color: currentcolor;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* hover와 같은 밑줄·가지선으로 키보드 위치 표시 */
|
|
61
|
+
&:where(:focus-visible) {
|
|
62
|
+
outline: none;
|
|
63
|
+
text-decoration: underline;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
&:where(:focus-visible)::before {
|
|
67
|
+
border-top-color: currentcolor;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.wg_shellNavList__link--active {
|
|
72
|
+
color: var(--app-color-link);
|
|
73
|
+
font-weight: var(--app-font-weight-active);
|
|
74
|
+
|
|
75
|
+
&::before {
|
|
76
|
+
border-top-color: currentcolor;
|
|
77
|
+
}
|
|
29
78
|
}
|
|
30
79
|
|
|
31
80
|
.wg_shellNavList__item {
|
|
@@ -62,20 +111,20 @@
|
|
|
62
111
|
}
|
|
63
112
|
|
|
64
113
|
&:where(:hover)::after {
|
|
65
|
-
border-left-color: var(--app-color-
|
|
114
|
+
border-left-color: var(--app-color-link);
|
|
66
115
|
}
|
|
67
116
|
|
|
68
117
|
&:where(:has(:focus-visible))::after {
|
|
69
|
-
border-left-color: var(--app-color-
|
|
118
|
+
border-left-color: var(--app-color-link);
|
|
70
119
|
}
|
|
71
120
|
|
|
72
121
|
/* 뒤 항목까지의 세로 경로 · 같은 목록의 형제 안에서만 연결 */
|
|
73
122
|
&:where(:has(~ .wg_shellNavList__item:hover))::before {
|
|
74
|
-
border-left-color: var(--app-color-
|
|
123
|
+
border-left-color: var(--app-color-link);
|
|
75
124
|
}
|
|
76
125
|
|
|
77
126
|
&:where(:has(~ .wg_shellNavList__item :focus-visible))::before {
|
|
78
|
-
border-left-color: var(--app-color-
|
|
127
|
+
border-left-color: var(--app-color-link);
|
|
79
128
|
}
|
|
80
129
|
|
|
81
130
|
& > .wg_shellNavList__root {
|
|
@@ -84,75 +133,39 @@
|
|
|
84
133
|
|
|
85
134
|
/* 하위 문서를 탐색할 때 조상 묶음의 가지도 함께 연결 */
|
|
86
135
|
&:where(:hover) > .wg_shellNavList__branch::before {
|
|
87
|
-
border-top-color: var(--app-color-
|
|
136
|
+
border-top-color: var(--app-color-link);
|
|
88
137
|
}
|
|
89
138
|
|
|
90
139
|
&:where(:has(:focus-visible)) > .wg_shellNavList__branch::before {
|
|
91
|
-
border-top-color: var(--app-color-
|
|
140
|
+
border-top-color: var(--app-color-link);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* 자식 문서를 탐색할 때 클릭 가능한 부모 문서의 가지도 연결 */
|
|
144
|
+
&:where(:hover) > .wg_shellNavList__link::before {
|
|
145
|
+
border-top-color: var(--app-color-link);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
&:where(:has(:focus-visible)) > .wg_shellNavList__link::before {
|
|
149
|
+
border-top-color: var(--app-color-link);
|
|
92
150
|
}
|
|
93
151
|
}
|
|
94
152
|
|
|
95
153
|
.wg_shellNavList__item--trail {
|
|
96
154
|
&::before {
|
|
97
|
-
border-left-color: var(--app-color-
|
|
155
|
+
border-left-color: var(--app-color-link);
|
|
98
156
|
}
|
|
99
157
|
}
|
|
100
158
|
|
|
101
159
|
.wg_shellNavList__item--current {
|
|
102
160
|
&::after {
|
|
103
|
-
border-left-color: var(--app-color-
|
|
161
|
+
border-left-color: var(--app-color-link);
|
|
104
162
|
}
|
|
105
163
|
|
|
106
164
|
& > .wg_shellNavList__branch::before {
|
|
107
|
-
border-top-color: var(--app-color-
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
.wg_shellNavList__link {
|
|
112
|
-
/* 가로 가지는 링크 첫 줄 기준 · 여러 줄 제목에서도 연결 위치 유지 */
|
|
113
|
-
position: relative;
|
|
114
|
-
display: block;
|
|
115
|
-
padding: var(--app-space-tree-row) 0 var(--app-space-tree-row) calc(var(--app-space-tree-indent) + var(--app-space-inline));
|
|
116
|
-
color: var(--app-color-text);
|
|
117
|
-
font-size: var(--app-font-size-dense);
|
|
118
|
-
line-height: var(--app-line-height-tight);
|
|
119
|
-
text-decoration: none;
|
|
120
|
-
overflow-wrap: anywhere;
|
|
121
|
-
|
|
122
|
-
&::before {
|
|
123
|
-
position: absolute;
|
|
124
|
-
top: calc(0.5lh + var(--app-space-tree-row));
|
|
125
|
-
left: 0;
|
|
126
|
-
width: var(--app-space-tree-indent);
|
|
127
|
-
border-top: 1px solid var(--app-color-border-soft);
|
|
128
|
-
content: "";
|
|
129
|
-
pointer-events: none;
|
|
165
|
+
border-top-color: var(--app-color-link);
|
|
130
166
|
}
|
|
131
167
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
&:where(:hover)::before {
|
|
137
|
-
border-top-color: currentcolor;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/* hover와 같은 밑줄·가지선으로 키보드 위치 표시 */
|
|
141
|
-
&:where(:focus-visible) {
|
|
142
|
-
outline: none;
|
|
143
|
-
text-decoration: underline;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
&:where(:focus-visible)::before {
|
|
147
|
-
border-top-color: currentcolor;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
.wg_shellNavList__link--active {
|
|
152
|
-
color: var(--app-color-text-strong);
|
|
153
|
-
font-weight: var(--app-font-weight-active);
|
|
154
|
-
|
|
155
|
-
&::before {
|
|
156
|
-
border-top-color: currentcolor;
|
|
168
|
+
& > .wg_shellNavList__link::before {
|
|
169
|
+
border-top-color: var(--app-color-link);
|
|
157
170
|
}
|
|
158
171
|
}
|
|
@@ -19,8 +19,12 @@ export interface WgShellNavListProps {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export const WgShellNavList = (props: WgShellNavListProps) => {
|
|
22
|
+
const currentAncestors = new Set(props.currentDoc?.ancestors);
|
|
22
23
|
const entries = [
|
|
23
|
-
...props.group.docs.map((doc) =>
|
|
24
|
+
...props.group.docs.map((doc) => {
|
|
25
|
+
const active = doc.id === props.currentDoc?.id;
|
|
26
|
+
return {kind: "document" as const, key: `doc:${doc.id}`, doc, active, current: active || currentAncestors.has(doc.id)};
|
|
27
|
+
}),
|
|
24
28
|
...props.group.groups.map((group) => ({
|
|
25
29
|
kind: "group" as const,
|
|
26
30
|
key: `group:${JSON.stringify(group.path)}`,
|
|
@@ -44,17 +48,28 @@ export const WgShellNavList = (props: WgShellNavListProps) => {
|
|
|
44
48
|
key={entry.key}
|
|
45
49
|
>
|
|
46
50
|
{/**
|
|
47
|
-
*
|
|
51
|
+
* 문서 링크 · 부모도 독립 페이지, 현재 한 문서에만 페이지 상태 부여
|
|
48
52
|
*/}
|
|
49
53
|
{entry.kind === "document" && (
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
<Fragment>
|
|
55
|
+
<a
|
|
56
|
+
className={clsx("wg_shellNavList__link", {"wg_shellNavList__link--active": entry.active})}
|
|
57
|
+
href={`/${entry.doc.id}/`}
|
|
58
|
+
title={entry.doc.data.label}
|
|
59
|
+
aria-current={entry.active ? "page" : undefined}
|
|
60
|
+
>
|
|
61
|
+
{entry.doc.data.name}
|
|
62
|
+
</a>
|
|
63
|
+
{/**
|
|
64
|
+
* 하위 문서 · 부모 아래에서도 기존 목록의 들여쓰기와 키보드 이동 유지
|
|
65
|
+
*/}
|
|
66
|
+
{entry.doc.children !== undefined && (
|
|
67
|
+
<WgShellNavList
|
|
68
|
+
group={{name: entry.doc.data.name, path: entry.doc.data.group, docs: entry.doc.children, groups: []}}
|
|
69
|
+
currentDoc={props.currentDoc}
|
|
70
|
+
/>
|
|
71
|
+
)}
|
|
72
|
+
</Fragment>
|
|
58
73
|
)}
|
|
59
74
|
{/**
|
|
60
75
|
* 하위 묶음 · 같은 목록을 재귀로 이어 깊이에 따라 들여쓰기
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* 읽는 순서의 문서 목록 ·
|
|
2
|
+
* 읽는 순서의 문서 목록 · 작은 회색 묶음과 링크색 문서
|
|
3
3
|
*/
|
|
4
4
|
.wg_shellNav__root {
|
|
5
5
|
font-size: var(--app-font-size-dense);
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
.wg_shellNav__group {
|
|
16
16
|
margin: 0 0 4px;
|
|
17
17
|
color: var(--app-color-text-muted);
|
|
18
|
-
font-size: var(--app-font-size-
|
|
18
|
+
font-size: var(--app-font-size-group);
|
|
19
|
+
line-height: calc(var(--app-font-size-dense) * var(--app-line-height-tight));
|
|
19
20
|
text-box: trim-start cap alphabetic;
|
|
20
21
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* 문서 탐색 아래 목차 ·
|
|
2
|
+
* 문서 탐색 아래 목차 · 회색 제목·묶음과 본문 글색의 이동 대상 구분
|
|
3
3
|
*/
|
|
4
4
|
.wg_shellToc__root {
|
|
5
5
|
margin-top: var(--app-space-group);
|
|
@@ -13,14 +13,15 @@
|
|
|
13
13
|
margin-bottom: var(--app-space-inline);
|
|
14
14
|
color: var(--app-color-text-muted);
|
|
15
15
|
font-size: var(--app-font-size-label);
|
|
16
|
-
font-weight: var(--app-font-weight-
|
|
16
|
+
font-weight: var(--app-font-weight-strong);
|
|
17
17
|
letter-spacing: var(--app-letter-spacing-label);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
.wg_shellToc__group {
|
|
21
21
|
margin: var(--app-space-stack) 0 4px;
|
|
22
22
|
color: var(--app-color-text-muted);
|
|
23
|
-
font-size: var(--app-font-size-
|
|
23
|
+
font-size: var(--app-font-size-group);
|
|
24
|
+
line-height: calc(var(--app-font-size-dense) * var(--app-line-height-tight));
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
.wg_shellToc__list {
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
position: relative;
|
|
35
36
|
display: block;
|
|
36
37
|
padding: var(--app-space-tree-row) 0 var(--app-space-tree-row) calc(var(--app-space-tree-indent) + var(--app-space-inline));
|
|
37
|
-
color: var(--app-color-text
|
|
38
|
+
color: var(--app-color-text);
|
|
38
39
|
font-size: var(--app-font-size-dense);
|
|
39
40
|
line-height: var(--app-line-height-tight);
|
|
40
41
|
text-decoration: none;
|
|
@@ -222,7 +223,7 @@
|
|
|
222
223
|
position: relative;
|
|
223
224
|
display: block;
|
|
224
225
|
padding: var(--app-space-tree-row) 0 var(--app-space-tree-row) calc(var(--app-space-tree-indent) + var(--app-space-inline));
|
|
225
|
-
color: var(--app-color-text
|
|
226
|
+
color: var(--app-color-text);
|
|
226
227
|
font-size: var(--app-font-size-detail);
|
|
227
228
|
line-height: var(--app-line-height-tight);
|
|
228
229
|
text-decoration: none;
|
package/src/constant/asset.ts
CHANGED
|
@@ -40,9 +40,29 @@ export const asset_content_types: Record<string, string> = {
|
|
|
40
40
|
".js": "text/javascript; charset=utf-8",
|
|
41
41
|
".woff2": "font/woff2",
|
|
42
42
|
".svg": "image/svg+xml",
|
|
43
|
+
".png": "image/png",
|
|
44
|
+
".jpg": "image/jpeg",
|
|
45
|
+
".jpeg": "image/jpeg",
|
|
46
|
+
".gif": "image/gif",
|
|
47
|
+
".webp": "image/webp",
|
|
48
|
+
".avif": "image/avif",
|
|
49
|
+
".pdf": "application/pdf",
|
|
50
|
+
".csv": "text/csv; charset=utf-8",
|
|
51
|
+
".txt": "text/plain; charset=utf-8",
|
|
52
|
+
".zip": "application/zip",
|
|
43
53
|
};
|
|
44
54
|
|
|
45
55
|
/**
|
|
46
56
|
* 미등록 확장자의 content-type
|
|
47
57
|
*/
|
|
48
58
|
export const asset_content_type_default = "application/octet-stream";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 문서에서 참조한 그림·첨부 자원의 예약 경로
|
|
62
|
+
*/
|
|
63
|
+
export const asset_media_dir = `/${asset_dir}/media`;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 명시적 로컬 참조로 배포할 수 있는 파일 형식 · 실행 파일·소스·설정 제외
|
|
67
|
+
*/
|
|
68
|
+
export const asset_media_extensions = new Set([".svg", ".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".pdf", ".csv", ".txt", ".zip"]);
|
package/src/constant/copy.ts
CHANGED
|
@@ -35,7 +35,7 @@ export const copy_home_missing = "문서 폴더에 README.md를 추가하면 이
|
|
|
35
35
|
/**
|
|
36
36
|
* 현재 문서 목차 라벨
|
|
37
37
|
*/
|
|
38
|
-
export const copy_nav_toc_label = "
|
|
38
|
+
export const copy_nav_toc_label = "On this page";
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* 대소문자만 다른 README 중복 오류
|
|
@@ -82,6 +82,21 @@ export const copy_error_doc_id = "문서 id에 사용할 수 없는 경로";
|
|
|
82
82
|
*/
|
|
83
83
|
export const copy_error_doc_id_clash = "문서 id가 겹친다";
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* 누락되거나 홈을 가리키는 부모 문서 참조
|
|
87
|
+
*/
|
|
88
|
+
export const copy_error_doc_parent_missing = "부모 문서가 없다";
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 자기 참조 또는 순환하는 문서 부모 경로
|
|
92
|
+
*/
|
|
93
|
+
export const copy_error_doc_parent_cycle = "문서 parent가 순환한다";
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 분류 경로가 다른 문서 사이의 부모 참조
|
|
97
|
+
*/
|
|
98
|
+
export const copy_error_doc_parent_group = "부모 문서와 group이 다르다";
|
|
99
|
+
|
|
85
100
|
/**
|
|
86
101
|
* 흐름도 렌더링 실패
|
|
87
102
|
*/
|
|
@@ -106,3 +121,8 @@ export const copy_warn_broken_link = "없는 문서로 건 링크다";
|
|
|
106
121
|
* 미등록 날짜 상태 문구 경고
|
|
107
122
|
*/
|
|
108
123
|
export const copy_warn_unknown_status = "설정에 없는 상태 문구다";
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 문서에서 참조한 로컬 자원의 경로·형식 오류
|
|
127
|
+
*/
|
|
128
|
+
export const copy_error_asset = "자원이 올바르지 않다";
|
package/src/constant/doc.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {remarkParts} from "@/component/widget/prose/_function/remark-parts";
|
|
|
19
19
|
import {remarkStatus} from "@/component/widget/prose/_function/remark-status";
|
|
20
20
|
import {remarkSwatch} from "@/component/widget/prose/_function/remark-swatch";
|
|
21
21
|
import {remarkUnknownDirectives} from "@/component/widget/prose/_function/remark-unknown-directives";
|
|
22
|
+
import {rehypeAssets} from "@/content/rehype-assets";
|
|
22
23
|
import {rehypeHeadingIds} from "@/content/rehype-heading-ids";
|
|
23
24
|
import {remarkReport} from "@/content/remark-report";
|
|
24
25
|
import type {SiteConfig} from "@/type/site-config";
|
|
@@ -28,7 +29,7 @@ import type {SiteConfig} from "@/type/site-config";
|
|
|
28
29
|
* 플러그인 HTML 조각은 마지막 rehype-raw에서 해석
|
|
29
30
|
* Shiki inline style: token.css의 --app-code-* 참조
|
|
30
31
|
*/
|
|
31
|
-
export const createProcessor = (options: {site: SiteConfig; root: string}) => {
|
|
32
|
+
export const createProcessor = (options: {site: SiteConfig; root: string; files?: Map<string, string>}) => {
|
|
32
33
|
return unified()
|
|
33
34
|
.use(remarkParse)
|
|
34
35
|
.use(remarkFrontmatter, ["yaml"])
|
|
@@ -50,6 +51,7 @@ export const createProcessor = (options: {site: SiteConfig; root: string}) => {
|
|
|
50
51
|
.use(rehypeSections)
|
|
51
52
|
.use(rehypeTables)
|
|
52
53
|
.use(rehypeRaw)
|
|
54
|
+
.use(rehypeAssets, {root: options.root, files: options.files ?? new Map<string, string>()})
|
|
53
55
|
.use(rehypeStringify, {allowDangerousHtml: true});
|
|
54
56
|
};
|
|
55
57
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import {mkdir, mkdtemp, rm, writeFile} from "node:fs/promises";
|
|
2
|
+
import {mkdir, mkdtemp, rm, symlink, writeFile} from "node:fs/promises";
|
|
3
3
|
import {tmpdir} from "node:os";
|
|
4
4
|
import {join} from "node:path";
|
|
5
5
|
import {test} from "node:test";
|
|
@@ -160,6 +160,133 @@ test("nested groups follow metadata paths and keep sibling branches and document
|
|
|
160
160
|
assert.doesNotMatch(html, /<details\b/);
|
|
161
161
|
});
|
|
162
162
|
|
|
163
|
+
test("parent is an optional nonempty document ID", () => {
|
|
164
|
+
const data = {name: "Child", label: "Child", group: "Guide"};
|
|
165
|
+
|
|
166
|
+
assert.equal(docDataSchema.parse(data).parent, undefined);
|
|
167
|
+
assert.equal(docDataSchema.parse({...data, parent: " guide/settings "}).parent, "guide/settings");
|
|
168
|
+
for (const parent of ["", " ", null, 1, [], ["guide"]]) {
|
|
169
|
+
assert.equal(docDataSchema.safeParse({...data, parent}).success, false);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("parent documents contain ordered descendants with independent URLs and one current page", async (t) => {
|
|
174
|
+
const root = await mkdtemp(join(tmpdir(), "for-humanity-parents-"));
|
|
175
|
+
const site = siteConfigSchema.parse({navigation: [["Projects", "Atlas", "Guide"]]});
|
|
176
|
+
|
|
177
|
+
t.after(() => rm(root, {recursive: true, force: true}));
|
|
178
|
+
await mkdir(join(root, "guide"));
|
|
179
|
+
await Promise.all([
|
|
180
|
+
writeFile(join(root, "settings.md"), "---\nname: Settings\nlabel: Settings\ngroup: [Projects, Atlas, Guide]\norder: 10\n---\n\nSettings introduction.\n"),
|
|
181
|
+
writeFile(join(root, "guide/site.md"), "---\nname: Site\nlabel: Site\ngroup: [Projects, Atlas, Guide]\nparent: settings\norder: 20\n---\n\nSite introduction.\n"),
|
|
182
|
+
writeFile(join(root, "themes.md"), "---\nname: Themes\nlabel: Themes\ngroup: [Projects, Atlas, Guide]\nparent: settings\norder: 10\n---\n\nThemes introduction.\n"),
|
|
183
|
+
writeFile(join(root, "details.md"), "---\nname: Details\nlabel: Details\ngroup: [Projects, Atlas, Guide]\nparent: guide/site\n---\n\nDeep child.\n"),
|
|
184
|
+
writeFile(join(root, "writing.md"), "---\nname: Writing\nlabel: Writing\ngroup: [Projects, Atlas, Guide]\norder: 20\n---\n\nIndependent guide.\n"),
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
const docs = await readDocs({root, processor: createProcessor({site, root})});
|
|
188
|
+
const groups = toDocGroups({docs, navigation: site.navigation});
|
|
189
|
+
const guide = groups[0].groups[0].groups[0];
|
|
190
|
+
|
|
191
|
+
assert.deepEqual(
|
|
192
|
+
guide.docs.map((doc) => doc.id),
|
|
193
|
+
["settings", "writing"],
|
|
194
|
+
);
|
|
195
|
+
assert.ok(guide.docs[0].children);
|
|
196
|
+
assert.deepEqual(
|
|
197
|
+
guide.docs[0].children.map((doc) => doc.id),
|
|
198
|
+
["themes", "guide/site"],
|
|
199
|
+
);
|
|
200
|
+
assert.ok(guide.docs[0].children[1].children);
|
|
201
|
+
assert.deepEqual(
|
|
202
|
+
guide.docs[0].children[1].children.map((doc) => doc.id),
|
|
203
|
+
["details"],
|
|
204
|
+
);
|
|
205
|
+
assert.deepEqual(docs.find((doc) => doc.id === "details")?.ancestors, ["settings", "guide/site"]);
|
|
206
|
+
assert.deepEqual(toDocGroups({docs: docs.toReversed(), navigation: site.navigation}), groups);
|
|
207
|
+
|
|
208
|
+
const html = renderToStaticMarkup(WgShellNav({site, docs, current: "details"}));
|
|
209
|
+
assert.match(html, /href="\/settings\/"[^>]*>Settings<\/a><ul\b[^>]*aria-label="Settings"/);
|
|
210
|
+
assert.match(html, /href="\/guide\/site\/"[^>]*>Site<\/a><ul\b[^>]*aria-label="Site"/);
|
|
211
|
+
assert.match(html, /href="\/details\/"[^>]*aria-current="page"/);
|
|
212
|
+
assert.equal([...html.matchAll(/aria-current="page"/g)].length, 1);
|
|
213
|
+
assert.equal([...html.matchAll(/wg_shellNavList__link--active/g)].length, 1);
|
|
214
|
+
assert.equal([...html.matchAll(/wg_shellNavList__item--current/g)].length, 5);
|
|
215
|
+
for (const doc of docs) {
|
|
216
|
+
assert.equal(html.split(`href="/${doc.id}/"`).length - 1, 1);
|
|
217
|
+
}
|
|
218
|
+
assert.ok(html.indexOf('href="/themes/"') < html.indexOf('href="/guide/site/"'));
|
|
219
|
+
assert.doesNotMatch(renderToStaticMarkup(WgShellNav({site, docs})), /aria-current="page"|wg_shellNavList__item--current/);
|
|
220
|
+
|
|
221
|
+
const app = createApp({site, store: {docs}, assets: {stylePath: asset_style_path, clientPath: asset_client_path, fontCss: "", preload: [], reload: false}});
|
|
222
|
+
for (const doc of docs) {
|
|
223
|
+
assert.equal((await app.request(`/${doc.id}/`)).status, 200);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("relative parent IDs work with both the whole collection and a project document root", async (t) => {
|
|
228
|
+
const root = await mkdtemp(join(tmpdir(), "for-humanity-parents-"));
|
|
229
|
+
const site = siteConfigSchema.parse({});
|
|
230
|
+
t.after(() => rm(root, {recursive: true, force: true}));
|
|
231
|
+
|
|
232
|
+
await mkdir(join(root, "project/research"), {recursive: true});
|
|
233
|
+
await mkdir(join(root, "project/guide"));
|
|
234
|
+
await Promise.all([
|
|
235
|
+
writeFile(join(root, "project/research/README.md"), "---\nname: Research\nlabel: Research\ngroup: Guide\n---\n\nResearch introduction.\n"),
|
|
236
|
+
writeFile(join(root, "project/research/parts.md"), "---\nname: Parts\nlabel: Parts\ngroup: Guide\nparent: ./readme\n---\n\nText.\n"),
|
|
237
|
+
writeFile(join(root, "project/guide/authoring.md"), "---\nname: Authoring\nlabel: Authoring\ngroup: Guide\nparent: ../research/readme\n---\n\nText.\n"),
|
|
238
|
+
]);
|
|
239
|
+
for (const scope of [root, join(root, "project")]) {
|
|
240
|
+
const docs = await readDocs({root: scope, processor: createProcessor({site, root: scope})});
|
|
241
|
+
const parentId = scope === root ? "project/research/readme" : "research/readme";
|
|
242
|
+
const groups = toDocGroups({docs, navigation: site.navigation});
|
|
243
|
+
assert.deepEqual(
|
|
244
|
+
groups[0].docs.map((doc) => doc.id),
|
|
245
|
+
[parentId],
|
|
246
|
+
);
|
|
247
|
+
assert.ok(groups[0].docs[0].children);
|
|
248
|
+
assert.equal(groups[0].docs[0].children.length, 2);
|
|
249
|
+
assert.ok(groups[0].docs[0].children.every((doc) => doc.data.parent === parentId));
|
|
250
|
+
assert.ok(groups[0].docs[0].children.every((doc) => doc.ancestors.length === 1 && doc.ancestors[0] === parentId));
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("missing and home document parents fail when loading documents", async (t) => {
|
|
255
|
+
const root = await mkdtemp(join(tmpdir(), "for-humanity-parents-"));
|
|
256
|
+
const site = siteConfigSchema.parse({});
|
|
257
|
+
t.after(() => rm(root, {recursive: true, force: true}));
|
|
258
|
+
|
|
259
|
+
await writeFile(join(root, "README.md"), "# Home\n");
|
|
260
|
+
for (const parent of ["missing", "readme", "guide.md", "/guide/"]) {
|
|
261
|
+
await writeFile(join(root, "child.md"), `---\nname: Child\nlabel: Child\ngroup: Guide\nparent: ${parent}\n---\n\nText.\n`);
|
|
262
|
+
await assert.rejects(readDocs({root, processor: createProcessor({site, root})}), /부모 문서가 없다: child/);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("self references and cycles fail when loading documents", async (t) => {
|
|
267
|
+
const root = await mkdtemp(join(tmpdir(), "for-humanity-parents-"));
|
|
268
|
+
const site = siteConfigSchema.parse({});
|
|
269
|
+
t.after(() => rm(root, {recursive: true, force: true}));
|
|
270
|
+
|
|
271
|
+
await writeFile(join(root, "first.md"), "---\nname: First\nlabel: First\ngroup: Guide\nparent: first\n---\n\nText.\n");
|
|
272
|
+
await assert.rejects(readDocs({root, processor: createProcessor({site, root})}), /문서 parent가 순환한다/);
|
|
273
|
+
|
|
274
|
+
await writeFile(join(root, "first.md"), "---\nname: First\nlabel: First\ngroup: Guide\nparent: second\n---\n\nText.\n");
|
|
275
|
+
await writeFile(join(root, "second.md"), "---\nname: Second\nlabel: Second\ngroup: Guide\nparent: third\n---\n\nText.\n");
|
|
276
|
+
await writeFile(join(root, "third.md"), "---\nname: Third\nlabel: Third\ngroup: Guide\nparent: first\n---\n\nText.\n");
|
|
277
|
+
await assert.rejects(readDocs({root, processor: createProcessor({site, root})}), /문서 parent가 순환한다/);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("parent and child must use the same complete group path", async (t) => {
|
|
281
|
+
const root = await mkdtemp(join(tmpdir(), "for-humanity-parents-"));
|
|
282
|
+
const site = siteConfigSchema.parse({});
|
|
283
|
+
t.after(() => rm(root, {recursive: true, force: true}));
|
|
284
|
+
|
|
285
|
+
await writeFile(join(root, "parent.md"), "---\nname: Parent\nlabel: Parent\ngroup: [Projects, Atlas, Research]\n---\n\nText.\n");
|
|
286
|
+
await writeFile(join(root, "child.md"), "---\nname: Child\nlabel: Child\ngroup: [Projects, Beacon, Research]\nparent: parent\n---\n\nText.\n");
|
|
287
|
+
await assert.rejects(readDocs({root, processor: createProcessor({site, root})}), /부모 문서와 group이 다르다: child/);
|
|
288
|
+
});
|
|
289
|
+
|
|
163
290
|
test("frontmatter supports BOM, CRLF and a closing fence at EOF", async (t) => {
|
|
164
291
|
const root = await mkdtemp(join(tmpdir(), "for-humanity-docs-"));
|
|
165
292
|
const site = siteConfigSchema.parse({});
|
|
@@ -289,7 +416,7 @@ test("relative Markdown links preserve query strings and fragments", async (t) =
|
|
|
289
416
|
"[Fragment](../api.mdx#details?literal=yes)",
|
|
290
417
|
"[External](https://example.com/api.md?mode=compact#details)",
|
|
291
418
|
"[Absolute](/api.md?mode=compact#details)",
|
|
292
|
-
"[Asset](diagram.svg?version=2#label)",
|
|
419
|
+
"[Asset](/diagram.svg?version=2#label)",
|
|
293
420
|
"[Local](#details?literal=yes)",
|
|
294
421
|
].join("\n\n"),
|
|
295
422
|
),
|
|
@@ -306,7 +433,7 @@ test("relative Markdown links preserve query strings and fragments", async (t) =
|
|
|
306
433
|
assert.match(setup.html, /href="\/api\/#details\?literal=yes"/);
|
|
307
434
|
assert.match(setup.html, /href="https:\/\/example\.com\/api\.md\?mode=compact#details"/);
|
|
308
435
|
assert.match(setup.html, /href="\/api\.md\?mode=compact#details"/);
|
|
309
|
-
assert.match(setup.html, /href="diagram\.svg\?version=2#label"/);
|
|
436
|
+
assert.match(setup.html, /href="\/diagram\.svg\?version=2#label"/);
|
|
310
437
|
assert.match(setup.html, /href="#details\?literal=yes"/);
|
|
311
438
|
});
|
|
312
439
|
|
|
@@ -333,3 +460,53 @@ test("headings and contents preserve authored text without automatic numbering",
|
|
|
333
460
|
assert.match(html, /wg_shellToc__group">Setup<\/div>/);
|
|
334
461
|
assert.doesNotMatch(html, /wg_prose__num|wg_shellToc__mark/);
|
|
335
462
|
});
|
|
463
|
+
|
|
464
|
+
test("local Markdown and raw HTML images share fingerprinted assets with attachments", async (t) => {
|
|
465
|
+
const root = await mkdtemp(join(tmpdir(), "for-humanity-media-"));
|
|
466
|
+
t.after(() => rm(root, {recursive: true, force: true}));
|
|
467
|
+
await mkdir(join(root, "guide"));
|
|
468
|
+
await mkdir(join(root, "img"));
|
|
469
|
+
await writeFile(join(root, "img", "sample.svg"), '<svg xmlns="http://www.w3.org/2000/svg"/>');
|
|
470
|
+
await writeFile(join(root, "guide", "report.csv"), "name,value\nsample,1\n");
|
|
471
|
+
await writeFile(
|
|
472
|
+
join(root, "guide", "page.md"),
|
|
473
|
+
'---\nname: Media\nlabel: Media\ngroup: Guide\n---\n\n\n\n![Reference][shot]\n\n[shot]: ../img/sample.svg\n\n<img src="../img/sample.svg" alt="Raw" />\n\n[Download](report.csv?download=1#rows)\n\n\n',
|
|
474
|
+
);
|
|
475
|
+
const files = new Map<string, string>();
|
|
476
|
+
const docs = await readDocs({root, processor: createProcessor({site: siteConfigSchema.parse({}), root, files})});
|
|
477
|
+
assert.equal(files.size, 2);
|
|
478
|
+
assert.equal(docs[0].html.match(/src="\/_fh\/media\/asset\.[a-f0-9]+\.svg"/g)?.length, 3);
|
|
479
|
+
assert.match(docs[0].html, /href="\/_fh\/media\/asset\.[a-f0-9]+\.csv\?download=1#rows"/);
|
|
480
|
+
assert.match(docs[0].html, /src="https:\/\/example.com\/image.png"/);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("missing local images and paths outside the document root fail instead of producing broken output", async (t) => {
|
|
484
|
+
const root = await mkdtemp(join(tmpdir(), "for-humanity-media-"));
|
|
485
|
+
t.after(() => rm(root, {recursive: true, force: true}));
|
|
486
|
+
await writeFile(join(root, "page.md"), "---\nname: Media\nlabel: Media\ngroup: Guide\n---\n\n\n");
|
|
487
|
+
await assert.rejects(readDocs({root, processor: createProcessor({site: siteConfigSchema.parse({}), root})}), /자원/);
|
|
488
|
+
await writeFile(join(root, "page.md"), "---\nname: Media\nlabel: Media\ngroup: Guide\n---\n\n\n");
|
|
489
|
+
await assert.rejects(readDocs({root, processor: createProcessor({site: siteConfigSchema.parse({}), root})}), /문서 폴더 밖/);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test("home images use the chosen root and symlink escapes cannot publish outside files", async (t) => {
|
|
493
|
+
const root = await mkdtemp(join(tmpdir(), "for-humanity-media-"));
|
|
494
|
+
const outside = await mkdtemp(join(tmpdir(), "for-humanity-outside-"));
|
|
495
|
+
t.after(() => Promise.all([rm(root, {recursive: true, force: true}), rm(outside, {recursive: true, force: true})]));
|
|
496
|
+
await writeFile(join(root, "local image.png"), "first");
|
|
497
|
+
await writeFile(join(root, "README.md"), "# Home\n\n\n");
|
|
498
|
+
const files = new Map<string, string>();
|
|
499
|
+
const site = siteConfigSchema.parse({});
|
|
500
|
+
const home = await readHome({root, processor: createProcessor({site, root, files})});
|
|
501
|
+
assert.equal(files.size, 1);
|
|
502
|
+
const before = [...files.keys()][0];
|
|
503
|
+
assert.ok(home?.html.includes(before));
|
|
504
|
+
await writeFile(join(root, "local image.png"), "changed");
|
|
505
|
+
const updated = new Map<string, string>();
|
|
506
|
+
await readHome({root, processor: createProcessor({site, root, files: updated})});
|
|
507
|
+
assert.notEqual([...updated.keys()][0], before);
|
|
508
|
+
await writeFile(join(outside, "private.png"), "private");
|
|
509
|
+
await symlink(join(outside, "private.png"), join(root, "linked.png"));
|
|
510
|
+
await writeFile(join(root, "README.md"), "# Home\n\n\n");
|
|
511
|
+
await assert.rejects(readHome({root, processor: createProcessor({site, root})}), /문서 폴더 밖/);
|
|
512
|
+
});
|