for-humanity 0.1.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 +14 -0
- package/LICENSE +21 -0
- package/README.md +118 -0
- package/dist/cli.css +962 -0
- package/dist/cli.js +1241 -0
- package/dist/client.js +5 -0
- package/package.json +92 -0
- package/src/app.tsx +57 -0
- package/src/asset/favicon.svg +5 -0
- package/src/asset/font/architects-daughter/LICENSE.txt +93 -0
- package/src/asset/font/architects-daughter/architects-daughter-latin-400-normal.woff2 +0 -0
- package/src/asset/font/architects-daughter/architects-daughter.css +10 -0
- package/src/asset/font/jetbrains-mono/LICENSE +93 -0
- package/src/asset/font/jetbrains-mono/jetbrains-mono-latin-wght-normal.woff2 +0 -0
- package/src/asset/font/jetbrains-mono/jetbrains-mono.css +11 -0
- package/src/asset/font/pretendard/LICENSE.txt +94 -0
- package/src/asset/font/pretendard/pretendardvariable-dynamic-subset.css +1008 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.0.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.1.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.10.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.11.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.12.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.13.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.14.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.15.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.16.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.17.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.18.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.19.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.2.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.20.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.21.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.22.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.23.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.24.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.25.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.26.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.27.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.28.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.29.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.3.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.30.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.31.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.32.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.33.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.34.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.35.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.36.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.37.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.38.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.39.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.4.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.40.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.41.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.42.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.43.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.44.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.45.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.46.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.47.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.48.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.49.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.5.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.50.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.51.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.52.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.53.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.54.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.55.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.56.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.57.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.58.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.59.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.6.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.60.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.61.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.62.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.63.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.64.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.65.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.66.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.67.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.68.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.69.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.7.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.70.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.71.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.72.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.73.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.74.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.75.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.76.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.77.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.78.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.79.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.8.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.80.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.81.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.82.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.83.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.84.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.85.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.86.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.87.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.88.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.89.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.9.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.90.woff2 +0 -0
- package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.91.woff2 +0 -0
- package/src/cli.ts +162 -0
- package/src/client.ts +322 -0
- package/src/component/widget/prose/_constant/heading-highlight.ts +4 -0
- package/src/component/widget/prose/_constant/section.ts +7 -0
- package/src/component/widget/prose/_constant/status.ts +9 -0
- package/src/component/widget/prose/_function/has-block-content.ts +30 -0
- package/src/component/widget/prose/_function/rehype-head.ts +31 -0
- package/src/component/widget/prose/_function/rehype-sections/_to-section-headings.ts +20 -0
- package/src/component/widget/prose/_function/rehype-sections/rehype-sections.ts +21 -0
- package/src/component/widget/prose/_function/rehype-tables.ts +22 -0
- package/src/component/widget/prose/_function/remark-blocks.ts +112 -0
- package/src/component/widget/prose/_function/remark-flow.ts +26 -0
- package/src/component/widget/prose/_function/remark-links.ts +33 -0
- package/src/component/widget/prose/_function/remark-parts.ts +26 -0
- package/src/component/widget/prose/_function/remark-status.ts +59 -0
- package/src/component/widget/prose/_function/remark-swatch.ts +16 -0
- package/src/component/widget/prose/_function/remark-unknown-directives.ts +25 -0
- package/src/component/widget/prose/_type/doc-section.ts +9 -0
- package/src/component/widget/prose/wg-prose.css +381 -0
- package/src/component/widget/prose/wg-prose.tsx +43 -0
- package/src/component/widget/shell/_constant/cursor-face.ts +9 -0
- package/src/component/widget/shell/_constant/navigation.ts +4 -0
- package/src/component/widget/shell/_constant/reading-line.ts +4 -0
- package/src/component/widget/shell/_function/bind-mobile-navigation.ts +140 -0
- package/src/component/widget/shell/_function/to-toc-groups.ts +38 -0
- package/src/component/widget/shell/_type/doc-outline.ts +16 -0
- package/src/component/widget/shell/_type/toc-group.ts +39 -0
- package/src/component/widget/shell/_wg-shell-nav.css +134 -0
- package/src/component/widget/shell/_wg-shell-nav.tsx +75 -0
- package/src/component/widget/shell/_wg-shell-toc.css +268 -0
- package/src/component/widget/shell/_wg-shell-toc.tsx +57 -0
- package/src/component/widget/shell/wg-shell.css +261 -0
- package/src/component/widget/shell/wg-shell.tsx +147 -0
- package/src/constant/asset.ts +48 -0
- package/src/constant/cli.ts +19 -0
- package/src/constant/copy.ts +108 -0
- package/src/constant/doc.ts +13 -0
- package/src/constant/font.ts +39 -0
- package/src/constant/locale.ts +9 -0
- package/src/constant/site.ts +9 -0
- package/src/constant/status.ts +15 -0
- package/src/content/create-processor.ts +59 -0
- package/src/content/read-docs.test.ts +242 -0
- package/src/content/read-docs.ts +75 -0
- package/src/content/read-home.ts +32 -0
- package/src/content/rehype-heading-ids.ts +34 -0
- package/src/content/remark-report.ts +15 -0
- package/src/content/to-doc-groups.ts +18 -0
- package/src/dev.ts +79 -0
- package/src/env.d.ts +6 -0
- package/src/page/doc/pg-doc.tsx +35 -0
- package/src/page/home/pg-home.tsx +40 -0
- package/src/style/base.css +28 -0
- package/src/style/token.css +162 -0
- package/src/type/doc-content.ts +15 -0
- package/src/type/doc-data.ts +33 -0
- package/src/type/doc-file-data.ts +21 -0
- package/src/type/doc-heading.ts +17 -0
- package/src/type/doc.ts +16 -0
- package/src/type/site-assets.ts +17 -0
- package/src/type/site-config.ts +55 -0
- package/src/type/vfile-data.d.ts +10 -0
- package/src/util/dom/find-hash-target.ts +18 -0
- package/src/util/dom/reveal-hash-target.ts +15 -0
- package/src/util/error/to-error-message.ts +10 -0
- package/src/util/font/to-font-css.test.ts +41 -0
- package/src/util/font/to-font-css.ts +28 -0
- package/src/util/mermaid/render-flow.js +321 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 브라우저 동작 · 모바일 탐색, 커서 장식, 읽는 절, hash 위치 보정
|
|
3
|
+
* 서버 HTML의 data-* 연결 · React hydration 없음
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {heading_highlight_hold_ms} from "@/component/widget/prose/_constant/heading-highlight";
|
|
7
|
+
import {cursor_face_offset_px, cursor_face_storage_key} from "@/component/widget/shell/_constant/cursor-face";
|
|
8
|
+
import {reading_line_slack_px} from "@/component/widget/shell/_constant/reading-line";
|
|
9
|
+
import {bindMobileNavigation} from "@/component/widget/shell/_function/bind-mobile-navigation";
|
|
10
|
+
import {findHashTarget} from "@/util/dom/find-hash-target";
|
|
11
|
+
import {revealHashTarget} from "@/util/dom/reveal-hash-target";
|
|
12
|
+
|
|
13
|
+
bindMobileNavigation();
|
|
14
|
+
|
|
15
|
+
const cursorFace = document.querySelector<HTMLImageElement>("[data-cursor-face]");
|
|
16
|
+
|
|
17
|
+
if (cursorFace) {
|
|
18
|
+
const cursorMedia = matchMedia("(hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference)");
|
|
19
|
+
const position = {x: 0, y: 0, frame: 0};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 터치 전환과 화면 이탈 시 예약된 이동도 취소해 장식이 다시 나타나지 않게 함
|
|
23
|
+
*/
|
|
24
|
+
const handleCursorHide = () => {
|
|
25
|
+
cancelAnimationFrame(position.frame);
|
|
26
|
+
position.frame = 0;
|
|
27
|
+
cursorFace.classList.remove("wg_shell__cursorFace--visible");
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 비활성 전환에서만 숨김 · 활성화 알림이 새 마우스 이동을 취소하지 않도록 함
|
|
32
|
+
*/
|
|
33
|
+
const handleCursorMediaChange = () => {
|
|
34
|
+
if (!cursorMedia.matches) {
|
|
35
|
+
handleCursorHide();
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 이동·화면 크기 변경·문서 진입에 같은 위치 계산 적용 · 화면 갱신마다 한 번만 반영
|
|
41
|
+
*/
|
|
42
|
+
const renderCursorFace = () => {
|
|
43
|
+
if (position.frame !== 0) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
position.frame = requestAnimationFrame(() => {
|
|
48
|
+
position.frame = 0;
|
|
49
|
+
|
|
50
|
+
// 포인터 좌표는 실행 중에만 결정 · transform으로 문서 재배치 방지
|
|
51
|
+
cursorFace.style.transform = `translate3d(
|
|
52
|
+
${Math.max(0, Math.min(position.x + cursor_face_offset_px, document.documentElement.clientWidth - cursorFace.width - cursor_face_offset_px))}px,
|
|
53
|
+
${Math.max(0, Math.min(position.y + cursor_face_offset_px, document.documentElement.clientHeight - cursorFace.height - cursor_face_offset_px))}px,
|
|
54
|
+
0
|
|
55
|
+
)`;
|
|
56
|
+
cursorFace.classList.add("wg_shell__cursorFace--visible");
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 누른 상태도 마우스 입력으로 처리 · 클릭·텍스트 선택 중에도 얼굴 유지
|
|
62
|
+
*/
|
|
63
|
+
const handleDocumentPointer = (event: PointerEvent) => {
|
|
64
|
+
const isMouse = cursorMedia.matches && event.pointerType === "mouse";
|
|
65
|
+
|
|
66
|
+
if (!isMouse) {
|
|
67
|
+
handleCursorHide();
|
|
68
|
+
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
position.x = event.clientX;
|
|
73
|
+
position.y = event.clientY;
|
|
74
|
+
renderCursorFace();
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 이미 보이는 얼굴을 새 화면 크기에 맞춰 이동 · 추가 마우스 이동 불필요
|
|
79
|
+
*/
|
|
80
|
+
const handleCursorResize = () => {
|
|
81
|
+
if (cursorFace.classList.contains("wg_shell__cursorFace--visible")) {
|
|
82
|
+
renderCursorFace();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 전체 페이지 이동에서도 마지막 좌표 유지 · 저장 실패는 기본 마우스 동작에 영향 없음
|
|
88
|
+
*/
|
|
89
|
+
const handlePageHide = () => {
|
|
90
|
+
if (!cursorFace.classList.contains("wg_shell__cursorFace--visible")) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
sessionStorage.setItem(cursor_face_storage_key, JSON.stringify({x: position.x, y: position.y}));
|
|
96
|
+
} catch {
|
|
97
|
+
// 저장소가 막힌 환경에서도 클릭·선택과 현재 페이지의 장식 유지
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 첫 진입과 BFCache 복귀에서 좌표를 한 번 소비 · 이전 문서의 위치가 남지 않도록 삭제
|
|
103
|
+
*/
|
|
104
|
+
const handlePageShow = () => {
|
|
105
|
+
try {
|
|
106
|
+
const stored = sessionStorage.getItem(cursor_face_storage_key);
|
|
107
|
+
|
|
108
|
+
sessionStorage.removeItem(cursor_face_storage_key);
|
|
109
|
+
|
|
110
|
+
if (stored !== null && cursorMedia.matches) {
|
|
111
|
+
const point: unknown = JSON.parse(stored);
|
|
112
|
+
// 속성 타입의 좁히기를 유지하도록 저장소 검증과 사용을 같은 분기에 둠
|
|
113
|
+
if (
|
|
114
|
+
typeof point === "object" &&
|
|
115
|
+
point !== null &&
|
|
116
|
+
"x" in point &&
|
|
117
|
+
"y" in point &&
|
|
118
|
+
typeof point.x === "number" &&
|
|
119
|
+
typeof point.y === "number" &&
|
|
120
|
+
Number.isFinite(point.x) &&
|
|
121
|
+
Number.isFinite(point.y)
|
|
122
|
+
) {
|
|
123
|
+
position.x = point.x;
|
|
124
|
+
position.y = point.y;
|
|
125
|
+
renderCursorFace();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
// 저장 실패나 잘못된 좌표는 무시하고 다음 실제 마우스 입력에서 표시
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
handlePageShow();
|
|
134
|
+
|
|
135
|
+
document.addEventListener("pointermove", handleDocumentPointer, {passive: true});
|
|
136
|
+
document.addEventListener("pointerdown", handleDocumentPointer, {passive: true});
|
|
137
|
+
document.documentElement.addEventListener("pointerleave", handleCursorHide);
|
|
138
|
+
addEventListener("blur", handleCursorHide);
|
|
139
|
+
addEventListener("resize", handleCursorResize);
|
|
140
|
+
addEventListener("pagehide", handlePageHide);
|
|
141
|
+
addEventListener("pageshow", handlePageShow);
|
|
142
|
+
cursorMedia.addEventListener("change", handleCursorMediaChange);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const hashTarget = revealHashTarget(location.hash);
|
|
146
|
+
|
|
147
|
+
// 값 조립이 아닌 현재 강조의 수명 상태 · 새 이동은 이전 타이머를 취소
|
|
148
|
+
let highlightedHeading: HTMLElement | null = null;
|
|
149
|
+
let highlightTimer = 0;
|
|
150
|
+
let highlightScroll: AbortController | null = null;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 강조 해제와 도착 감시 정리 · 다음 앵커의 오래된 타이머 개입 방지
|
|
154
|
+
*/
|
|
155
|
+
const clearHeadingHighlight = () => {
|
|
156
|
+
clearTimeout(highlightTimer);
|
|
157
|
+
highlightScroll?.abort();
|
|
158
|
+
highlightScroll = null;
|
|
159
|
+
highlightedHeading?.classList.remove("wg_prose__headingText--highlighted");
|
|
160
|
+
highlightedHeading = null;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 도착한 제목만 잠시 강조 · 같은 대상 재클릭도 유지 시간 재시작
|
|
165
|
+
*/
|
|
166
|
+
const highlightHeading = (target: HTMLElement | null) => {
|
|
167
|
+
clearHeadingHighlight();
|
|
168
|
+
highlightedHeading = target === null ? null : target.querySelector<HTMLElement>(".wg_prose__headingText");
|
|
169
|
+
|
|
170
|
+
if (highlightedHeading === null) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
highlightedHeading.classList.add("wg_prose__headingText--highlighted");
|
|
175
|
+
highlightTimer = window.setTimeout(clearHeadingHighlight, heading_highlight_hold_ms);
|
|
176
|
+
highlightScroll = new AbortController();
|
|
177
|
+
|
|
178
|
+
// 부드러운 스크롤이 끝난 뒤에도 3초 확보 · 움직이지 않거나 scrollend가 없으면 위 타이머 사용
|
|
179
|
+
document.addEventListener("scrollend", handleHighlightArrival, {signal: highlightScroll.signal});
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 본문 스크롤의 첫 도착에서 유지 시간 갱신 · 사이드바 스크롤은 제외
|
|
184
|
+
*/
|
|
185
|
+
const handleHighlightArrival = (event: Event) => {
|
|
186
|
+
if (event.target !== document) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
clearTimeout(highlightTimer);
|
|
191
|
+
highlightScroll?.abort();
|
|
192
|
+
highlightScroll = null;
|
|
193
|
+
highlightTimer = window.setTimeout(clearHeadingHighlight, heading_highlight_hold_ms);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* 같은 hash를 다시 눌러도 접힌 대상 공개 · 기본 링크 이동 유지
|
|
198
|
+
*/
|
|
199
|
+
const handleHashClick = (event: MouseEvent) => {
|
|
200
|
+
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (
|
|
205
|
+
event.currentTarget instanceof HTMLAnchorElement &&
|
|
206
|
+
(event.currentTarget.target === "" || event.currentTarget.target === "_self") &&
|
|
207
|
+
!event.currentTarget.hasAttribute("download") &&
|
|
208
|
+
event.currentTarget.origin === location.origin &&
|
|
209
|
+
event.currentTarget.pathname === location.pathname &&
|
|
210
|
+
event.currentTarget.search === location.search &&
|
|
211
|
+
event.currentTarget.hash
|
|
212
|
+
) {
|
|
213
|
+
const target = revealHashTarget(event.currentTarget.hash);
|
|
214
|
+
|
|
215
|
+
if (event.currentTarget.hash === location.hash) {
|
|
216
|
+
highlightHeading(target);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* hash 변경의 숨은 대상 공개와 위치 보정
|
|
223
|
+
*/
|
|
224
|
+
const handleHashChange = () => {
|
|
225
|
+
const target = revealHashTarget(location.hash);
|
|
226
|
+
|
|
227
|
+
target?.scrollIntoView();
|
|
228
|
+
highlightHeading(target);
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
for (const link of document.querySelectorAll<HTMLAnchorElement>("a[href]")) {
|
|
232
|
+
link.addEventListener("click", handleHashClick);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
addEventListener("hashchange", handleHashChange);
|
|
236
|
+
|
|
237
|
+
// 목차 링크와 실제 제목 연결
|
|
238
|
+
const tocSections = [...document.querySelectorAll<HTMLAnchorElement>("[data-toc-link]")].flatMap((link) => {
|
|
239
|
+
const heading = findHashTarget(link.hash);
|
|
240
|
+
const item = link.parentElement;
|
|
241
|
+
|
|
242
|
+
if (!heading || !item) {
|
|
243
|
+
return [];
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return [
|
|
247
|
+
{
|
|
248
|
+
heading,
|
|
249
|
+
link,
|
|
250
|
+
item,
|
|
251
|
+
list: item.parentElement,
|
|
252
|
+
subs: [...item.querySelectorAll<HTMLAnchorElement>("[data-toc-sub-link]")].flatMap((sub) => {
|
|
253
|
+
const subHeading = findHashTarget(sub.hash);
|
|
254
|
+
const subItem = sub.parentElement;
|
|
255
|
+
|
|
256
|
+
return subHeading && subItem ? [{heading: subHeading, link: sub, item: subItem}] : [];
|
|
257
|
+
}),
|
|
258
|
+
},
|
|
259
|
+
];
|
|
260
|
+
});
|
|
261
|
+
const firstSection = tocSections.at(0);
|
|
262
|
+
|
|
263
|
+
if (firstSection) {
|
|
264
|
+
/**
|
|
265
|
+
* 읽는 선 기준 절·소제목 표시
|
|
266
|
+
* 기준: 제목의 scroll-margin-top + reading_line_slack_px
|
|
267
|
+
*/
|
|
268
|
+
const markActive = () => {
|
|
269
|
+
// scroll-margin-top 계산값: CSS px
|
|
270
|
+
const line = Number.parseFloat(getComputedStyle(firstSection.heading).scrollMarginTop) + reading_line_slack_px;
|
|
271
|
+
// 접힌 Details 안 제목은 좌표가 남아도 현재 위치에서 제외
|
|
272
|
+
const current = tocSections.findLast((section) => section.heading.checkVisibility() && section.heading.getBoundingClientRect().top <= line) ?? firstSection;
|
|
273
|
+
const sub = current.subs.findLast((item) => item.heading.checkVisibility() && item.heading.getBoundingClientRect().top <= line);
|
|
274
|
+
const currentIndex = tocSections.indexOf(current);
|
|
275
|
+
const currentSubIndex = sub === undefined ? -1 : current.subs.indexOf(sub);
|
|
276
|
+
|
|
277
|
+
for (const [index, section] of tocSections.entries()) {
|
|
278
|
+
section.link.classList.toggle("wg_shellToc__link--active", section === current);
|
|
279
|
+
section.item.classList.toggle("wg_shellToc__item--trail", section.list === current.list && index < currentIndex);
|
|
280
|
+
section.item.classList.toggle("wg_shellToc__item--current", section === current);
|
|
281
|
+
|
|
282
|
+
for (const [subIndex, item] of section.subs.entries()) {
|
|
283
|
+
item.link.classList.toggle("wg_shellToc__subLink--active", item === sub);
|
|
284
|
+
item.item.classList.toggle("wg_shellToc__subItem--trail", section === current && subIndex < currentSubIndex);
|
|
285
|
+
item.item.classList.toggle("wg_shellToc__subItem--current", item === sub);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
addEventListener("scroll", markActive, {passive: true});
|
|
291
|
+
addEventListener("resize", markActive);
|
|
292
|
+
addEventListener("toggle", markActive, true);
|
|
293
|
+
markActive();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const userScroll = new AbortController();
|
|
297
|
+
|
|
298
|
+
for (const type of ["wheel", "touchmove", "keydown", "mousedown"]) {
|
|
299
|
+
addEventListener(type, () => userScroll.abort(), {once: true, passive: true});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* 글꼴 로드 후 hash 위치 보정 · 사용자 입력 시 자동 이동 취소
|
|
304
|
+
* 초기 smooth scroll과 보정의 충돌 방지 · 보정 후 CSS 동작 복원
|
|
305
|
+
*/
|
|
306
|
+
const settleHashScroll = async () => {
|
|
307
|
+
await document.fonts.ready;
|
|
308
|
+
|
|
309
|
+
if (hashTarget && !userScroll.signal.aborted) {
|
|
310
|
+
hashTarget.scrollIntoView({behavior: "instant"});
|
|
311
|
+
highlightHeading(hashTarget);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
document.documentElement.style.removeProperty("scroll-behavior");
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// 글꼴 요청 완료 시점 보장 · load 후 fonts.ready
|
|
318
|
+
if (document.readyState === "complete") {
|
|
319
|
+
settleHashScroll();
|
|
320
|
+
} else {
|
|
321
|
+
addEventListener("load", settleHashScroll, {once: true});
|
|
322
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 상태 문구 뒤 날짜 패턴 · YYYY-MM-DD
|
|
3
|
+
*/
|
|
4
|
+
export const status_date_pattern = String.raw`\d{4}-\d{2}-\d{2}`;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 미등록 날짜 상태 후보 · 등록 문구 치환 뒤 남은 괄호 문구
|
|
8
|
+
*/
|
|
9
|
+
export const status_candidate_pattern = new RegExp(String.raw`\([^()\n]*${status_date_pattern}\)`, "g");
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type {Nodes} from "mdast";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 부품 본문의 내용 판정 · 참조 정의·주석·빈 텍스트와 빈 구조 제외
|
|
5
|
+
* 이미지·표·구분선·주석 외 원시 HTML은 본문 요소
|
|
6
|
+
*/
|
|
7
|
+
export const hasBlockContent = (node: Nodes): boolean => {
|
|
8
|
+
if (node.type === "definition") {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (node.type === "html") {
|
|
13
|
+
// HTML의 빈 주석·대체 닫힘·미종결 주석 포함
|
|
14
|
+
return node.value.replace(/<!--(?:>|->|[\s\S]*?(?:--!?>|$))/g, "").trim() !== "";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if ("value" in node) {
|
|
18
|
+
return node.value.trim() !== "";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (node.type === "table" || node.type === "textDirective") {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if ("children" in node) {
|
|
26
|
+
return node.children.some(hasBlockContent);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return true;
|
|
30
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type {Root} from "hast";
|
|
2
|
+
import type {VFile} from "vfile";
|
|
3
|
+
import {section_heading_depth} from "@/component/widget/prose/_constant/section";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* name 제목과 첫 절 앞 본문의 공통 머리 · README는 원문의 h1 사용
|
|
7
|
+
*/
|
|
8
|
+
export const rehypeHead = () => (tree: Root, file: VFile) => {
|
|
9
|
+
const frontmatter = file.data.fh?.frontmatter;
|
|
10
|
+
|
|
11
|
+
const firstSection = tree.children.findIndex((node) => node.type === "element" && node.tagName === `h${section_heading_depth.section}`);
|
|
12
|
+
const leadEnd = firstSection === -1 ? tree.children.length : firstSection;
|
|
13
|
+
|
|
14
|
+
if (frontmatter === undefined && leadEnd === 0) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
tree.children = [
|
|
19
|
+
{
|
|
20
|
+
type: "element",
|
|
21
|
+
tagName: "header",
|
|
22
|
+
properties: {className: ["wg_prose__head"]},
|
|
23
|
+
children: [...tree.children.slice(0, leadEnd).filter((node) => node.type !== "doctype")],
|
|
24
|
+
},
|
|
25
|
+
...tree.children.slice(leadEnd),
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
if (frontmatter !== undefined && tree.children[0].type === "element") {
|
|
29
|
+
tree.children[0].children.unshift({type: "element", tagName: "h1", properties: {}, children: [{type: "text", value: frontmatter.name}]});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type {Element, Root} from "hast";
|
|
2
|
+
import {section_heading_depth} from "@/component/widget/prose/_constant/section";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 목차 대상 h2·h3 · 중첩 블록까지 문서 순서 수집
|
|
6
|
+
* rehype-heading-ids의 순회 순서와 일치
|
|
7
|
+
*/
|
|
8
|
+
export const toSectionHeadings = (node: Root | Element): Element[] => {
|
|
9
|
+
return node.children.flatMap((child) => {
|
|
10
|
+
if (child.type !== "element") {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (child.tagName === `h${section_heading_depth.section}` || child.tagName === `h${section_heading_depth.sub}`) {
|
|
15
|
+
return [child];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return toSectionHeadings(child);
|
|
19
|
+
});
|
|
20
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type {Root} from "hast";
|
|
2
|
+
import type {VFile} from "vfile";
|
|
3
|
+
import {toSectionHeadings} from "@/component/widget/prose/_function/rehype-sections/_to-section-headings";
|
|
4
|
+
import type {DocSection} from "@/component/widget/prose/_type/doc-section";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* h2·h3의 가름 메타데이터 수집 · 작성한 제목은 그대로 유지
|
|
8
|
+
* id·목차 텍스트는 앞 단계의 rehype-heading-ids 소유
|
|
9
|
+
* file.data.fh.sections는 headings의 h2·h3 순서와 일치
|
|
10
|
+
*/
|
|
11
|
+
export const rehypeSections = () => (tree: Root, file: VFile) => {
|
|
12
|
+
if (file.data.fh === undefined) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
file.data.fh.sections = toSectionHeadings(tree).map(
|
|
17
|
+
(heading): DocSection => ({
|
|
18
|
+
part: typeof heading.properties.dataPart === "string" ? heading.properties.dataPart : undefined,
|
|
19
|
+
}),
|
|
20
|
+
);
|
|
21
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type {Root} from "hast";
|
|
2
|
+
import {SKIP, visit} from "unist-util-visit";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 넓은 표의 내부 가로 스크롤 래퍼
|
|
6
|
+
*/
|
|
7
|
+
export const rehypeTables = () => (tree: Root) => {
|
|
8
|
+
visit(tree, "element", (node, index, parent) => {
|
|
9
|
+
if (node.tagName !== "table" || parent === undefined || index === undefined) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
parent.children[index] = {
|
|
14
|
+
type: "element",
|
|
15
|
+
tagName: "div",
|
|
16
|
+
properties: {className: ["wg_prose__table"]},
|
|
17
|
+
children: [node],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return [SKIP, index + 1];
|
|
21
|
+
});
|
|
22
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type {Root} from "mdast";
|
|
2
|
+
import {toString as toPlainText} from "mdast-util-to-string";
|
|
3
|
+
import {visit} from "unist-util-visit";
|
|
4
|
+
import type {VFile} from "vfile";
|
|
5
|
+
import {section_heading_depth} from "@/component/widget/prose/_constant/section";
|
|
6
|
+
import {hasBlockContent} from "@/component/widget/prose/_function/has-block-content";
|
|
7
|
+
import {copy_error_directive_syntax} from "@/constant/copy";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Note·Details의 제목·본문·속성·닫힘 계약
|
|
11
|
+
* label은 첫 자식 · summary는 Details의 첫 요소 · 원문 위치는 오류 기준
|
|
12
|
+
*/
|
|
13
|
+
export const remarkBlocks = () => (tree: Root, file: VFile) => {
|
|
14
|
+
const source = String(file.value);
|
|
15
|
+
|
|
16
|
+
visit(tree, (node, _index, parent) => {
|
|
17
|
+
if (node.type === "leafDirective" && node.name === "part" && parent?.type !== "root") {
|
|
18
|
+
file.fail(`${copy_error_directive_syntax}: part · 문서 최상위에 작성`, node);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (node.type !== "containerDirective" && node.type !== "leafDirective" && node.type !== "textDirective") {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (node.name !== "note" && node.name !== "details") {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (node.type !== "containerDirective") {
|
|
30
|
+
file.fail(`${copy_error_directive_syntax}: ${node.name} · ::: 블록으로 작성`, node);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const label = node.children.at(0);
|
|
34
|
+
|
|
35
|
+
if (label?.type !== "paragraph" || label.data?.directiveLabel !== true || label.children.some((child) => child.type !== "text")) {
|
|
36
|
+
file.fail(`${copy_error_directive_syntax}: ${node.name} · 일반 텍스트 제목 필수`, node);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const title = toPlainText(label).trim();
|
|
40
|
+
|
|
41
|
+
if (title === "") {
|
|
42
|
+
file.fail(`${copy_error_directive_syntax}: ${node.name} · 비어 있지 않은 제목 필수`, node);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!node.children.slice(1).some(hasBlockContent)) {
|
|
46
|
+
file.fail(`${copy_error_directive_syntax}: ${node.name} · 본문 필수`, node);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const unsupported = node.attributes ? Object.keys(node.attributes).find((key) => node.name !== "details" || key !== "open") : undefined;
|
|
50
|
+
|
|
51
|
+
if (unsupported !== undefined) {
|
|
52
|
+
file.fail(`${copy_error_directive_syntax}: ${node.name} · 미지원 속성 ${unsupported}`, node);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const open = node.attributes?.open;
|
|
56
|
+
const attributeSource = source.slice(label.position?.end.offset, node.position?.end.offset).split(/\r?\n/, 1)[0].trim();
|
|
57
|
+
|
|
58
|
+
// 속성 정규화 이전의 표기 · 중복 open과 open="" 거부
|
|
59
|
+
if (attributeSource !== "" && (node.name !== "details" || !/^\{[ \t]*open[ \t]*\}$/.test(attributeSource))) {
|
|
60
|
+
file.fail(`${copy_error_directive_syntax}: ${node.name} · 허용 속성은 Details의 {open}, 나머지는 생략`, node);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const original = source.slice(node.position?.start.offset, node.position?.end.offset);
|
|
64
|
+
const opening = /^(:{3,})/.exec(original);
|
|
65
|
+
const closing = /(?:^|\n)[ \t>]*(:{3,})[ \t]*\r?$/.exec(original);
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
opening === null ||
|
|
69
|
+
closing === null ||
|
|
70
|
+
closing.index + closing[0].length !== original.length ||
|
|
71
|
+
closing[1].length < opening[1].length ||
|
|
72
|
+
node.position?.end.offset === node.children.at(-1)?.position?.end.offset
|
|
73
|
+
) {
|
|
74
|
+
file.fail(`${copy_error_directive_syntax}: ${node.name} · 블록을 닫는 fence 필수`, node);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
visit(node, "heading", (heading) => {
|
|
78
|
+
if (heading.depth <= section_heading_depth.section) {
|
|
79
|
+
file.fail(`${copy_error_directive_syntax}: ${node.name} · #·## 제목은 부품 밖에 작성`, heading);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// 코드 안의 :::가 container를 먼저 닫은 경우 · 더 긴 바깥 fence로 구분
|
|
84
|
+
visit(node, "code", (code) => {
|
|
85
|
+
const originalCode = source.slice(code.position?.start.offset, code.position?.end.offset);
|
|
86
|
+
const codeOpening = /^ {0,3}(`{3,}|~{3,})/.exec(originalCode);
|
|
87
|
+
const codeClosing = /(?:^|\n)[ \t>]*(`{3,}|~{3,})[ \t]*\r?$/.exec(originalCode);
|
|
88
|
+
|
|
89
|
+
if (
|
|
90
|
+
codeOpening !== null &&
|
|
91
|
+
(codeClosing === null ||
|
|
92
|
+
codeClosing.index + codeClosing[0].length !== originalCode.length ||
|
|
93
|
+
codeClosing[1][0] !== codeOpening[1][0] ||
|
|
94
|
+
codeClosing[1].length < codeOpening[1].length)
|
|
95
|
+
) {
|
|
96
|
+
file.fail(`${copy_error_directive_syntax}: ${node.name} · 코드 fence를 닫고, 코드 안의 :보다 긴 바깥 fence 사용`, code);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
label.children = [{type: "text", value: title}];
|
|
101
|
+
label.data = {
|
|
102
|
+
...label.data,
|
|
103
|
+
hName: node.name === "note" ? "strong" : "summary",
|
|
104
|
+
hProperties: {className: [node.name === "note" ? "wg_prose__noteTitle" : "wg_prose__detailsSummary"]},
|
|
105
|
+
};
|
|
106
|
+
node.data = {
|
|
107
|
+
...node.data,
|
|
108
|
+
hName: node.name === "note" ? "aside" : "details",
|
|
109
|
+
hProperties: node.name === "note" ? {className: ["wg_prose__note"], ariaLabel: title} : {className: ["wg_prose__details"], open: open === ""},
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type {Root} from "mdast";
|
|
2
|
+
import {visit} from "unist-util-visit";
|
|
3
|
+
import type {VFile} from "vfile";
|
|
4
|
+
import {copy_error_flow} from "@/constant/copy";
|
|
5
|
+
import {renderFlow} from "@/util/mermaid/render-flow";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Mermaid → 격자 SVG · beautiful-mermaid 기반
|
|
9
|
+
* 실패 시 파일·줄을 포함한 빌드 오류
|
|
10
|
+
*/
|
|
11
|
+
export const remarkFlow = () => (tree: Root, file: VFile) => {
|
|
12
|
+
visit(tree, "code", (node, index, parent) => {
|
|
13
|
+
if (node.lang !== "mermaid" || parent === undefined || index === undefined) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
parent.children[index] = {
|
|
19
|
+
type: "html",
|
|
20
|
+
value: `<div class="wg_prose__flow">${renderFlow(node.value)}</div>`,
|
|
21
|
+
};
|
|
22
|
+
} catch (error) {
|
|
23
|
+
file.fail(`${copy_error_flow}: ${error instanceof Error ? error.message : String(error)}`, node);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {existsSync} from "node:fs";
|
|
2
|
+
import {dirname, posix, relative, resolve} from "node:path";
|
|
3
|
+
import type {Root} from "mdast";
|
|
4
|
+
import {visit} from "unist-util-visit";
|
|
5
|
+
import type {VFile} from "vfile";
|
|
6
|
+
import {copy_warn_broken_link} from "@/constant/copy";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 상대 Markdown 링크 → 사이트 URL · query·hash 원문 유지
|
|
10
|
+
* 외부·절대 URL과 같은 페이지 hash 유지 · 없는 파일은 터미널 경고
|
|
11
|
+
*/
|
|
12
|
+
export const remarkLinks = (options: {root: string}) => (tree: Root, file: VFile) => {
|
|
13
|
+
const here = dirname(relative(options.root, file.path));
|
|
14
|
+
|
|
15
|
+
visit(tree, (node) => {
|
|
16
|
+
if (node.type !== "link" && node.type !== "definition") {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// 셋째 capture: query·hash 접미사 · 파일 확인과 경로 정규화에서 제외
|
|
21
|
+
const match = /^(?![a-z]+:|\/|#)([^#?]+)(\.mdx?)([?#].*|)$/i.exec(node.url);
|
|
22
|
+
|
|
23
|
+
if (match) {
|
|
24
|
+
if (!existsSync(resolve(dirname(file.path), `${match[1]}${match[2]}`))) {
|
|
25
|
+
file.message(`${copy_warn_broken_link}: ${node.url}`, node);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const target = posix.join(here, `${match[1]}${match[2]}`).toLowerCase();
|
|
29
|
+
|
|
30
|
+
node.url = target === "readme.md" ? `/${match[3]}` : `/${posix.join(here, match[1]).toLowerCase()}/${match[3]}`;
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type {Root} from "mdast";
|
|
2
|
+
import {toString as toPlainText} from "mdast-util-to-string";
|
|
3
|
+
import {section_heading_depth} from "@/component/widget/prose/_constant/section";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ::part[이름]의 본문 가름과 다음 h2의 data-part
|
|
7
|
+
* rehype-sections에서 같은 지점의 목차 묶음 생성
|
|
8
|
+
*/
|
|
9
|
+
export const remarkParts = () => (tree: Root) => {
|
|
10
|
+
for (const [index, node] of tree.children.entries()) {
|
|
11
|
+
if (node.type !== "leafDirective" || node.name !== "part") {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const section = tree.children.slice(index + 1).find((next) => next.type === "heading" && next.depth === section_heading_depth.section);
|
|
16
|
+
|
|
17
|
+
node.data = {hName: "div", hProperties: {className: ["wg_prose__part"]}};
|
|
18
|
+
|
|
19
|
+
if (section) {
|
|
20
|
+
section.data = {
|
|
21
|
+
...section.data,
|
|
22
|
+
hProperties: {...section.data?.hProperties, dataPart: toPlainText(node).trim()},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|