for-humanity 0.9.1 → 0.11.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 +11 -0
- package/README.md +3 -3
- package/dist/cli.css +54 -25
- package/dist/cli.js +170 -61
- package/package.json +2 -1
- package/src/component/widget/prose/wg-prose.css +59 -23
- package/src/constant/copy.ts +5 -0
- package/src/entry/cli/_constant/snippet.ts +21 -0
- package/src/entry/cli/_function/create-processor/_remark-snippets.ts +117 -0
- package/src/entry/cli/_function/create-processor/_shiki-transformers.ts +55 -0
- package/src/entry/cli/_function/create-processor/create-processor.ts +6 -5
- package/src/entry/cli/cli.test.ts +62 -9
- package/src/entry/cli/cli.ts +15 -4
- package/src/style/token.css +10 -6
- package/src/component/widget/prose/_function/remark-code-labels.ts +0 -47
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.11.0 — 2026-10-13
|
|
6
|
+
|
|
7
|
+
- Add `<<< ./file.ext#region meta` code snippet imports: a file inside the document root becomes a code block, with optional `#region` extraction, dedent, fence meta passthrough and dev reloads when the file changes.
|
|
8
|
+
- Add `bad` and `good` fence meta that tint the whole code block, with line notation drawn one step darker on top.
|
|
9
|
+
|
|
10
|
+
## 0.10.0 — 2026-10-13
|
|
11
|
+
|
|
12
|
+
- Add VitePress-style code notation through Shiki transformers: `// [!code highlight|error|warning|++|--]`, `// [!code word:name]`, meta `{2,4-6}` and a `line-numbers` meta that numbers lines with CSS counters.
|
|
13
|
+
- Remove the `bad`/`good` code labels added in 0.9.0; mark the lines instead.
|
|
14
|
+
- Draw tables and flowchart boxes with the same border color as code blocks.
|
|
15
|
+
|
|
5
16
|
## 0.9.1 — 2026-10-13
|
|
6
17
|
|
|
7
18
|
- Draw tables with an outer border and cell borders, box flowcharts with a border, and separate sections with a solid rule and 64px of space.
|
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ Write Markdown. Keep the context. Share a static site.
|
|
|
33
33
|
Node.js 22 or later and pnpm. Install the CLI in your document project:
|
|
34
34
|
|
|
35
35
|
```sh
|
|
36
|
-
pnpm add -D --save-exact for-humanity@0.
|
|
36
|
+
pnpm add -D --save-exact for-humanity@0.11.0
|
|
37
37
|
mkdir docs
|
|
38
38
|
```
|
|
39
39
|
|
|
@@ -52,7 +52,7 @@ pnpm exec for-humanity preview docs
|
|
|
52
52
|
|
|
53
53
|
If your Markdown files are in the project root, use `.` instead of `docs`. The root `README.md` becomes the home page; root `AGENTS.md` and `CLAUDE.md` stay out of the site.
|
|
54
54
|
|
|
55
|
-
Version `0.
|
|
55
|
+
Version `0.11.0` supports `dev`, `build` and `preview`. `init` is planned; see the [Roadmap](https://github.com/minu-ha/for-humanity/blob/main/templates/document/roadmap.md) and [Changelog](https://github.com/minu-ha/for-humanity/blob/main/CHANGELOG.md).
|
|
56
56
|
Cloudflare Pages can host the generated site. See [Deployment](https://github.com/minu-ha/for-humanity/blob/main/templates/document/deployment.md) for the build settings.
|
|
57
57
|
Set an optional public root `url` in the site configuration to generate `robots.txt` and `sitemap.xml`; projects without a URL emit neither file. See [Crawling](https://github.com/minu-ha/for-humanity/blob/main/templates/document/crawling.md).
|
|
58
58
|
|
|
@@ -75,7 +75,7 @@ pnpm preview # Preview the static site
|
|
|
75
75
|
pnpm build:pages # Add Cloudflare Pages headers and a 404 page
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
Changes pushed to `main` update the documentation site. A stable version tag such as `v0.
|
|
78
|
+
Changes pushed to `main` update the documentation site. A stable version tag such as `v0.11.0` runs checks, publishes the package to npm, then creates a GitHub Release with the same package file.
|
|
79
79
|
The npm README updates with each package release. See [Maintenance](https://github.com/minu-ha/for-humanity/blob/main/templates/document/maintenance.md#packaging-and-release) for the release steps.
|
|
80
80
|
|
|
81
81
|
## Make it yours
|
package/dist/cli.css
CHANGED
|
@@ -138,15 +138,14 @@
|
|
|
138
138
|
}
|
|
139
139
|
& table {
|
|
140
140
|
width: 100%;
|
|
141
|
-
border: 1px solid var(--app-color-border
|
|
141
|
+
border: 1px solid var(--app-color-border);
|
|
142
142
|
border-collapse: collapse;
|
|
143
143
|
font-size: var(--app-font-size-dense);
|
|
144
144
|
font-variant-numeric: tabular-nums;
|
|
145
145
|
}
|
|
146
146
|
& th {
|
|
147
147
|
padding: var(--app-space-inline) var(--app-space-stack);
|
|
148
|
-
border: 1px solid var(--app-color-border
|
|
149
|
-
border-bottom-color: var(--app-color-border);
|
|
148
|
+
border: 1px solid var(--app-color-border);
|
|
150
149
|
background: var(--app-color-code);
|
|
151
150
|
color: var(--app-color-text-strong);
|
|
152
151
|
font-weight: var(--app-font-weight-strong);
|
|
@@ -156,7 +155,7 @@
|
|
|
156
155
|
}
|
|
157
156
|
& td {
|
|
158
157
|
padding: var(--app-space-inline) var(--app-space-stack);
|
|
159
|
-
border: 1px solid var(--app-color-border
|
|
158
|
+
border: 1px solid var(--app-color-border);
|
|
160
159
|
text-align: left;
|
|
161
160
|
vertical-align: top;
|
|
162
161
|
}
|
|
@@ -168,28 +167,54 @@
|
|
|
168
167
|
border: 0;
|
|
169
168
|
border-top: 1px dotted var(--app-color-border);
|
|
170
169
|
}
|
|
171
|
-
& .
|
|
172
|
-
|
|
173
|
-
|
|
170
|
+
& pre code .line {
|
|
171
|
+
display: inline-block;
|
|
172
|
+
width: calc(100% + 2 * var(--app-space-block));
|
|
173
|
+
margin: 0 calc(-1 * var(--app-space-block));
|
|
174
|
+
padding: 0 var(--app-space-block);
|
|
174
175
|
}
|
|
175
|
-
& .
|
|
176
|
-
|
|
176
|
+
& pre code .line.highlighted {
|
|
177
|
+
background: var(--app-color-accent-soft);
|
|
177
178
|
}
|
|
178
|
-
& .
|
|
179
|
-
padding: var(--app-space-inline) var(--app-space-block);
|
|
179
|
+
& pre code .line.highlighted.error {
|
|
180
180
|
background: var(--app-color-bad-soft);
|
|
181
|
-
color: var(--app-color-bad);
|
|
182
|
-
font-size: var(--app-font-size-dense);
|
|
183
|
-
font-weight: var(--app-font-weight-strong);
|
|
184
181
|
}
|
|
185
|
-
&
|
|
182
|
+
& pre code .line.highlighted.warning {
|
|
183
|
+
background: var(--app-color-warn-soft);
|
|
184
|
+
}
|
|
185
|
+
& pre code .line.diff.add {
|
|
186
186
|
background: var(--app-color-good-soft);
|
|
187
|
-
color: var(--app-color-good);
|
|
188
187
|
}
|
|
189
|
-
& .
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
188
|
+
& pre code .line.diff.remove {
|
|
189
|
+
background: var(--app-color-bad-soft);
|
|
190
|
+
opacity: 0.8;
|
|
191
|
+
}
|
|
192
|
+
& pre.wg_prose__code--bad code .line.highlighted,
|
|
193
|
+
& pre.wg_prose__code--bad code .line.highlighted.error,
|
|
194
|
+
& pre.wg_prose__code--bad code .line.diff.remove {
|
|
195
|
+
background: var(--app-color-bad-mark);
|
|
196
|
+
}
|
|
197
|
+
& pre.wg_prose__code--good code .line.highlighted,
|
|
198
|
+
& pre.wg_prose__code--good code .line.diff.add {
|
|
199
|
+
background: var(--app-color-good-mark);
|
|
200
|
+
}
|
|
201
|
+
& pre code .highlighted-word {
|
|
202
|
+
padding: 0 2px;
|
|
203
|
+
border-radius: var(--app-radius-inline);
|
|
204
|
+
background: var(--app-color-accent-soft);
|
|
205
|
+
}
|
|
206
|
+
& pre.wg_prose__lineNumbers code {
|
|
207
|
+
counter-reset: line;
|
|
208
|
+
}
|
|
209
|
+
& pre.wg_prose__lineNumbers code .line::before {
|
|
210
|
+
display: inline-block;
|
|
211
|
+
width: 2em;
|
|
212
|
+
margin-right: var(--app-space-stack);
|
|
213
|
+
color: var(--app-color-text-muted);
|
|
214
|
+
text-align: right;
|
|
215
|
+
content: counter(line);
|
|
216
|
+
counter-increment: line;
|
|
217
|
+
user-select: none;
|
|
193
218
|
}
|
|
194
219
|
}
|
|
195
220
|
.wg_prose__head {
|
|
@@ -272,7 +297,7 @@
|
|
|
272
297
|
overflow-x: auto;
|
|
273
298
|
margin: var(--app-space-block) 0;
|
|
274
299
|
padding: var(--app-space-block);
|
|
275
|
-
border: 1px solid var(--app-color-border
|
|
300
|
+
border: 1px solid var(--app-color-border);
|
|
276
301
|
border-radius: var(--app-radius-box);
|
|
277
302
|
letter-spacing: 0;
|
|
278
303
|
& svg {
|
|
@@ -763,9 +788,13 @@
|
|
|
763
788
|
--app-color-accent: light-dark(#555, #b5b5b5);
|
|
764
789
|
--app-color-accent-soft: light-dark(#f5f5f2, #222);
|
|
765
790
|
--app-color-bad: light-dark(#b2412e, #ff9b8a);
|
|
766
|
-
--app-color-bad-soft: light-dark(#
|
|
791
|
+
--app-color-bad-soft: light-dark(#f9ebe8, #3a2522);
|
|
792
|
+
--app-color-bad-mark: light-dark(#f1cdc6, #5b322c);
|
|
767
793
|
--app-color-good: light-dark(#2f7a47, #8bd3a3);
|
|
768
|
-
--app-color-good-soft: light-dark(#
|
|
794
|
+
--app-color-good-soft: light-dark(#e4f3ed, #1c342c);
|
|
795
|
+
--app-color-good-mark: light-dark(#c4e4d4, #27503f);
|
|
796
|
+
--app-color-warn: light-dark(#8a6400, #e6c46a);
|
|
797
|
+
--app-color-warn-soft: light-dark(#fbf3d9, #3d3418);
|
|
769
798
|
--app-color-link: light-dark(#11a, #9db8ff);
|
|
770
799
|
--app-color-link-visited: light-dark(#51a, #c5a5ed);
|
|
771
800
|
--app-color-link-hover: light-dark(#44d, #c1d1ff);
|
|
@@ -857,10 +886,10 @@
|
|
|
857
886
|
--app-space-gutter: 16px;
|
|
858
887
|
}
|
|
859
888
|
}
|
|
860
|
-
.
|
|
889
|
+
.wg_prose__code--bad {
|
|
861
890
|
--app-code-background: var(--app-color-bad-soft);
|
|
862
891
|
}
|
|
863
|
-
.
|
|
892
|
+
.wg_prose__code--good {
|
|
864
893
|
--app-code-background: var(--app-color-good-soft);
|
|
865
894
|
}
|
|
866
895
|
|
package/dist/cli.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// src/entry/cli/cli.ts
|
|
4
4
|
import { EventEmitter } from "node:events";
|
|
5
|
-
import { existsSync as
|
|
5
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, watch } from "node:fs";
|
|
6
6
|
import { copyFile, mkdir, rm, writeFile } from "node:fs/promises";
|
|
7
|
-
import { dirname as
|
|
7
|
+
import { dirname as dirname5, extname as extname5, join as join4, relative as relative5, resolve as resolve4, sep as sep4 } from "node:path";
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
9
|
import { serve } from "@hono/node-server";
|
|
10
10
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
@@ -64,6 +64,7 @@ var copy_error_directive_syntax = "\uBD80\uD488 \uBB38\uBC95\uC774 \uC62C\uBC14\
|
|
|
64
64
|
var copy_warn_broken_link = "\uC5C6\uB294 \uBB38\uC11C\uB85C \uAC74 \uB9C1\uD06C\uB2E4";
|
|
65
65
|
var copy_warn_unknown_status = "\uC124\uC815\uC5D0 \uC5C6\uB294 \uC0C1\uD0DC \uBB38\uAD6C\uB2E4";
|
|
66
66
|
var copy_error_asset = "\uC790\uC6D0\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uB2E4";
|
|
67
|
+
var copy_error_snippet = "\uCF54\uB4DC \uC2A4\uB2C8\uD3AB\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uB2E4";
|
|
67
68
|
|
|
68
69
|
// src/constant/font.ts
|
|
69
70
|
var font_mono_css = "src/asset/font/monoplex-kr/monoplex-kr.css";
|
|
@@ -1231,33 +1232,8 @@ var remarkBlocks = () => (tree, file) => {
|
|
|
1231
1232
|
});
|
|
1232
1233
|
};
|
|
1233
1234
|
|
|
1234
|
-
// src/component/widget/prose/_function/remark-code-labels.ts
|
|
1235
|
-
import { visit as visit3 } from "unist-util-visit";
|
|
1236
|
-
var code_label_pattern = /^(bad|good)(?:\s+(.+?))?\s*$/;
|
|
1237
|
-
var inserted_node_count = 3;
|
|
1238
|
-
var escapeHtml = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1239
|
-
var remarkCodeLabels = () => (tree) => {
|
|
1240
|
-
visit3(tree, "code", (node, index, parent) => {
|
|
1241
|
-
if (typeof node.meta !== "string" || parent === void 0 || index === void 0) {
|
|
1242
|
-
return;
|
|
1243
|
-
}
|
|
1244
|
-
const match = code_label_pattern.exec(node.meta);
|
|
1245
|
-
if (match === null) {
|
|
1246
|
-
return;
|
|
1247
|
-
}
|
|
1248
|
-
const [, tone, title] = match;
|
|
1249
|
-
const caption = title === void 0 ? "" : `<figcaption class="wg_prose__codeLabelTitle">${escapeHtml(title)}</figcaption>`;
|
|
1250
|
-
node.meta = null;
|
|
1251
|
-
parent.children.splice(index, 1, { type: "html", value: `<figure class="wg_prose__codeLabel wg_prose__codeLabel--${tone}">${caption}` }, node, {
|
|
1252
|
-
type: "html",
|
|
1253
|
-
value: "</figure>"
|
|
1254
|
-
});
|
|
1255
|
-
return index + inserted_node_count;
|
|
1256
|
-
});
|
|
1257
|
-
};
|
|
1258
|
-
|
|
1259
1235
|
// src/component/widget/prose/_function/remark-flow.ts
|
|
1260
|
-
import { visit as
|
|
1236
|
+
import { visit as visit3 } from "unist-util-visit";
|
|
1261
1237
|
|
|
1262
1238
|
// src/util/mermaid/render-flow.js
|
|
1263
1239
|
import { renderMermaidASCII } from "beautiful-mermaid";
|
|
@@ -1571,7 +1547,7 @@ var renderFlow = (source) => gridToSvg(renderMermaidASCII(widenCjk(source), ASCI
|
|
|
1571
1547
|
|
|
1572
1548
|
// src/component/widget/prose/_function/remark-flow.ts
|
|
1573
1549
|
var remarkFlow = () => (tree, file) => {
|
|
1574
|
-
|
|
1550
|
+
visit3(tree, "code", (node, index, parent) => {
|
|
1575
1551
|
if (node.lang !== "mermaid" || parent === void 0 || index === void 0) {
|
|
1576
1552
|
return;
|
|
1577
1553
|
}
|
|
@@ -1589,10 +1565,10 @@ var remarkFlow = () => (tree, file) => {
|
|
|
1589
1565
|
// src/component/widget/prose/_function/remark-links.ts
|
|
1590
1566
|
import { existsSync } from "node:fs";
|
|
1591
1567
|
import { dirname, posix, relative, resolve } from "node:path";
|
|
1592
|
-
import { visit as
|
|
1568
|
+
import { visit as visit4 } from "unist-util-visit";
|
|
1593
1569
|
var remarkLinks = (options) => (tree, file) => {
|
|
1594
1570
|
const here = dirname(relative(options.root, file.path));
|
|
1595
|
-
|
|
1571
|
+
visit4(tree, (node) => {
|
|
1596
1572
|
if (node.type !== "link" && node.type !== "definition") {
|
|
1597
1573
|
return;
|
|
1598
1574
|
}
|
|
@@ -1626,7 +1602,7 @@ var remarkParts = () => (tree) => {
|
|
|
1626
1602
|
};
|
|
1627
1603
|
|
|
1628
1604
|
// src/component/widget/prose/_function/remark-status.ts
|
|
1629
|
-
import { escape as
|
|
1605
|
+
import { escape as escapeHtml, escapeRegExp } from "es-toolkit/string";
|
|
1630
1606
|
import { findAndReplace } from "mdast-util-find-and-replace";
|
|
1631
1607
|
|
|
1632
1608
|
// src/component/widget/prose/_constant/status.ts
|
|
@@ -1652,7 +1628,7 @@ var remarkStatus = (options) => {
|
|
|
1652
1628
|
}
|
|
1653
1629
|
return {
|
|
1654
1630
|
type: "html",
|
|
1655
|
-
value: `<span class="wg_prose__pill wg_prose__pill--${item.kind}">${
|
|
1631
|
+
value: `<span class="wg_prose__pill wg_prose__pill--${item.kind}">${escapeHtml(text)}</span>`
|
|
1656
1632
|
};
|
|
1657
1633
|
}
|
|
1658
1634
|
],
|
|
@@ -1676,9 +1652,9 @@ var remarkStatus = (options) => {
|
|
|
1676
1652
|
};
|
|
1677
1653
|
|
|
1678
1654
|
// src/component/widget/prose/_function/remark-swatch.ts
|
|
1679
|
-
import { visit as
|
|
1655
|
+
import { visit as visit5 } from "unist-util-visit";
|
|
1680
1656
|
var remarkSwatch = () => (tree) => {
|
|
1681
|
-
|
|
1657
|
+
visit5(tree, "inlineCode", (node) => {
|
|
1682
1658
|
if (/^#[0-9a-f]{3,8}$/i.test(node.value.trim())) {
|
|
1683
1659
|
node.data = {
|
|
1684
1660
|
hProperties: { className: ["wg_prose__color"], style: `--wg-prose-chip-color: ${node.value.trim()}` }
|
|
@@ -1688,9 +1664,9 @@ var remarkSwatch = () => (tree) => {
|
|
|
1688
1664
|
};
|
|
1689
1665
|
|
|
1690
1666
|
// src/component/widget/prose/_function/remark-unknown-directives.ts
|
|
1691
|
-
import { SKIP as SKIP2, visit as
|
|
1667
|
+
import { SKIP as SKIP2, visit as visit6 } from "unist-util-visit";
|
|
1692
1668
|
var remarkUnknownDirectives = () => (tree, file) => {
|
|
1693
|
-
|
|
1669
|
+
visit6(tree, (node, index, parent) => {
|
|
1694
1670
|
if (node.type === "textDirective" && parent !== void 0 && index !== void 0) {
|
|
1695
1671
|
parent.children[index] = {
|
|
1696
1672
|
type: "text",
|
|
@@ -1707,7 +1683,7 @@ var remarkUnknownDirectives = () => (tree, file) => {
|
|
|
1707
1683
|
// src/entry/cli/_function/create-processor/_rehype-assets.ts
|
|
1708
1684
|
import { existsSync as existsSync2, realpathSync, statSync } from "node:fs";
|
|
1709
1685
|
import { dirname as dirname2, extname as extname2, isAbsolute, relative as relative2, resolve as resolve2, sep } from "node:path";
|
|
1710
|
-
import { visit as
|
|
1686
|
+
import { visit as visit7 } from "unist-util-visit";
|
|
1711
1687
|
|
|
1712
1688
|
// src/util/file/to-asset-path.ts
|
|
1713
1689
|
import { createHash } from "node:crypto";
|
|
@@ -1720,7 +1696,7 @@ var toAssetPath = (options) => {
|
|
|
1720
1696
|
|
|
1721
1697
|
// src/entry/cli/_function/create-processor/_rehype-assets.ts
|
|
1722
1698
|
var rehypeAssets = (options) => (tree, file) => {
|
|
1723
|
-
|
|
1699
|
+
visit7(tree, "element", (node) => {
|
|
1724
1700
|
const property = node.tagName === "img" ? "src" : "href";
|
|
1725
1701
|
const url = node.properties[property];
|
|
1726
1702
|
if (node.tagName !== "img" && node.tagName !== "a" || typeof url !== "string" || /^(?:[a-z][a-z\d+.-]*:|\/|#)/i.test(url)) {
|
|
@@ -1760,11 +1736,11 @@ var rehypeAssets = (options) => (tree, file) => {
|
|
|
1760
1736
|
// src/entry/cli/_function/create-processor/_rehype-heading-ids.ts
|
|
1761
1737
|
import Slugger from "github-slugger";
|
|
1762
1738
|
import { toString as toPlainText3 } from "hast-util-to-string";
|
|
1763
|
-
import { visit as
|
|
1739
|
+
import { visit as visit8 } from "unist-util-visit";
|
|
1764
1740
|
var rehypeHeadingIds = () => (tree, file) => {
|
|
1765
1741
|
const slugger = new Slugger();
|
|
1766
1742
|
const headings = [];
|
|
1767
|
-
|
|
1743
|
+
visit8(tree, "element", (node) => {
|
|
1768
1744
|
const depth = /^h([1-6])$/.exec(node.tagName)?.[1];
|
|
1769
1745
|
if (depth === void 0) {
|
|
1770
1746
|
return;
|
|
@@ -1788,14 +1764,140 @@ var remarkReport = (options) => (_tree, file) => {
|
|
|
1788
1764
|
}
|
|
1789
1765
|
};
|
|
1790
1766
|
|
|
1767
|
+
// src/entry/cli/_function/create-processor/_remark-snippets.ts
|
|
1768
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, realpathSync as realpathSync2, statSync as statSync2 } from "node:fs";
|
|
1769
|
+
import { dirname as dirname3, extname as extname3, isAbsolute as isAbsolute2, relative as relative4, resolve as resolve3, sep as sep2 } from "node:path";
|
|
1770
|
+
import { visit as visit9 } from "unist-util-visit";
|
|
1771
|
+
|
|
1772
|
+
// src/entry/cli/_constant/snippet.ts
|
|
1773
|
+
var snippet_pattern = /^<<<\s+(\S+?)(?:#([\w-]+))?(?:\s+(.+?))?\s*$/;
|
|
1774
|
+
var snippet_region_pattern = /^\s*\S*\s*#(end)?region\s+([\w-]+)/;
|
|
1775
|
+
var snippet_language_by_extension = {
|
|
1776
|
+
".cjs": "js",
|
|
1777
|
+
".md": "markdown",
|
|
1778
|
+
".mjs": "js",
|
|
1779
|
+
".mts": "ts",
|
|
1780
|
+
".txt": "text",
|
|
1781
|
+
".yml": "yaml"
|
|
1782
|
+
};
|
|
1783
|
+
|
|
1784
|
+
// src/entry/cli/_function/create-processor/_remark-snippets.ts
|
|
1785
|
+
var toSnippetBody = (source, region) => {
|
|
1786
|
+
const lines = source.replace(/\r\n/g, "\n").split("\n");
|
|
1787
|
+
let inside = region === void 0;
|
|
1788
|
+
let found = region === void 0;
|
|
1789
|
+
const kept = [];
|
|
1790
|
+
for (const line of lines) {
|
|
1791
|
+
const marker = snippet_region_pattern.exec(line);
|
|
1792
|
+
if (marker !== null) {
|
|
1793
|
+
if (region !== void 0 && marker[2] === region) {
|
|
1794
|
+
inside = marker[1] === void 0;
|
|
1795
|
+
found = true;
|
|
1796
|
+
}
|
|
1797
|
+
continue;
|
|
1798
|
+
}
|
|
1799
|
+
if (inside) {
|
|
1800
|
+
kept.push(line);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
if (!found) {
|
|
1804
|
+
return void 0;
|
|
1805
|
+
}
|
|
1806
|
+
while (kept.length > 0 && kept[0]?.trim() === "") {
|
|
1807
|
+
kept.shift();
|
|
1808
|
+
}
|
|
1809
|
+
while (kept.length > 0 && kept.at(-1)?.trim() === "") {
|
|
1810
|
+
kept.pop();
|
|
1811
|
+
}
|
|
1812
|
+
const indents = kept.filter((line) => line.trim() !== "").map((line) => line.length - line.trimStart().length);
|
|
1813
|
+
const indent = indents.length === 0 ? 0 : Math.min(...indents);
|
|
1814
|
+
return kept.map((line) => line.slice(Math.min(indent, line.length - line.trimStart().length))).join("\n");
|
|
1815
|
+
};
|
|
1816
|
+
var remarkSnippets = (options) => (tree, file) => {
|
|
1817
|
+
visit9(tree, "paragraph", (node, index, parent) => {
|
|
1818
|
+
const [child] = node.children;
|
|
1819
|
+
if (node.children.length !== 1 || child === void 0 || child.type !== "text" || parent === void 0 || index === void 0) {
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
const match = snippet_pattern.exec(child.value.trim());
|
|
1823
|
+
if (match === null) {
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
const [, path = "", region, meta] = match;
|
|
1827
|
+
const target = resolve3(dirname3(file.path), path);
|
|
1828
|
+
const location2 = relative4(options.root, target);
|
|
1829
|
+
if (location2 === ".." || location2.startsWith(`..${sep2}`) || isAbsolute2(location2)) {
|
|
1830
|
+
file.fail(`${copy_error_snippet}: \uBB38\uC11C \uD3F4\uB354 \uBC16 \xB7 ${path}`, node);
|
|
1831
|
+
}
|
|
1832
|
+
if (!existsSync3(target)) {
|
|
1833
|
+
file.fail(`${copy_error_snippet}: \uD30C\uC77C\uC774 \uC5C6\uB2E4 \xB7 ${path}`, node);
|
|
1834
|
+
}
|
|
1835
|
+
const actual = realpathSync2(target);
|
|
1836
|
+
const actualLocation = relative4(realpathSync2(options.root), actual);
|
|
1837
|
+
if (actualLocation === ".." || actualLocation.startsWith(`..${sep2}`) || isAbsolute2(actualLocation) || !statSync2(actual).isFile()) {
|
|
1838
|
+
file.fail(`${copy_error_snippet}: \uBB38\uC11C \uD3F4\uB354 \uBC16\uC774\uAC70\uB098 \uC77C\uBC18 \uD30C\uC77C\uC774 \uC544\uB2C8\uB2E4 \xB7 ${path}`, node);
|
|
1839
|
+
}
|
|
1840
|
+
const body = toSnippetBody(readFileSync2(actual, "utf8"), region);
|
|
1841
|
+
if (body === void 0) {
|
|
1842
|
+
file.fail(`${copy_error_snippet}: region \uC774 \uC5C6\uB2E4 \xB7 ${path}#${region}`, node);
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
const extension = extname3(actual).toLowerCase();
|
|
1846
|
+
const lang = snippet_language_by_extension[extension] === void 0 ? extension.slice(1) : snippet_language_by_extension[extension];
|
|
1847
|
+
options.snippets.add(actual);
|
|
1848
|
+
parent.children.splice(index, 1, { type: "code", lang, meta: meta === void 0 ? null : meta, value: body });
|
|
1849
|
+
});
|
|
1850
|
+
};
|
|
1851
|
+
|
|
1852
|
+
// src/entry/cli/_function/create-processor/_shiki-transformers.ts
|
|
1853
|
+
import {
|
|
1854
|
+
transformerMetaHighlight,
|
|
1855
|
+
transformerNotationDiff,
|
|
1856
|
+
transformerNotationErrorLevel,
|
|
1857
|
+
transformerNotationHighlight,
|
|
1858
|
+
transformerNotationWordHighlight
|
|
1859
|
+
} from "@shikijs/transformers";
|
|
1860
|
+
var lineNumbers = {
|
|
1861
|
+
name: "for-humanity:line-numbers",
|
|
1862
|
+
pre(node) {
|
|
1863
|
+
const meta = this.options.meta?.__raw;
|
|
1864
|
+
if (typeof meta === "string" && /\bline-numbers\b/.test(meta)) {
|
|
1865
|
+
this.addClassToHast(node, "wg_prose__lineNumbers");
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
};
|
|
1869
|
+
var tone = {
|
|
1870
|
+
name: "for-humanity:code-tone",
|
|
1871
|
+
pre(node) {
|
|
1872
|
+
const meta = this.options.meta?.__raw;
|
|
1873
|
+
if (typeof meta !== "string") {
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
for (const word of ["bad", "good"]) {
|
|
1877
|
+
if (new RegExp(`\\b${word}\\b`).test(meta)) {
|
|
1878
|
+
this.addClassToHast(node, `wg_prose__code--${word}`);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
};
|
|
1883
|
+
var shikiTransformers = [
|
|
1884
|
+
transformerNotationHighlight({ matchAlgorithm: "v3" }),
|
|
1885
|
+
transformerNotationErrorLevel({ matchAlgorithm: "v3" }),
|
|
1886
|
+
transformerNotationDiff({ matchAlgorithm: "v3" }),
|
|
1887
|
+
transformerNotationWordHighlight({ matchAlgorithm: "v3" }),
|
|
1888
|
+
transformerMetaHighlight(),
|
|
1889
|
+
lineNumbers,
|
|
1890
|
+
tone
|
|
1891
|
+
];
|
|
1892
|
+
|
|
1791
1893
|
// src/entry/cli/_function/create-processor/create-processor.ts
|
|
1792
1894
|
var createProcessor = (options) => {
|
|
1793
|
-
return unified().use(remarkParse).use(remarkFrontmatter, ["yaml"]).use(remarkGfm).use(remarkSmartypants).use(remarkDirective).use(remarkBlocks).use(remarkParts).use(remarkFlow).use(
|
|
1895
|
+
return unified().use(remarkParse).use(remarkFrontmatter, ["yaml"]).use(remarkGfm).use(remarkSnippets, { root: options.root, snippets: options.snippets ?? /* @__PURE__ */ new Set() }).use(remarkSmartypants).use(remarkDirective).use(remarkBlocks).use(remarkParts).use(remarkFlow).use(remarkStatus, { status: options.site.status }).use(remarkSwatch).use(remarkLinks, { root: options.root }).use(remarkUnknownDirectives).use(remarkReport, { root: options.root }).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeShiki, { theme: createCssVariablesTheme({ name: "for-humanity", variablePrefix: "--app-code-" }), transformers: shikiTransformers }).use(rehypeHead).use(rehypeHeadingIds).use(rehypeSections).use(rehypeTables).use(rehypeRaw).use(rehypeAssets, { root: options.root, files: options.files ?? /* @__PURE__ */ new Map() }).use(rehypeStringify, { allowDangerousHtml: true });
|
|
1794
1896
|
};
|
|
1795
1897
|
|
|
1796
1898
|
// src/entry/cli/_function/read-docs.ts
|
|
1797
1899
|
import { readdir, readFile as readFile2 } from "node:fs/promises";
|
|
1798
|
-
import { join, posix as posix3, sep as
|
|
1900
|
+
import { join, posix as posix3, sep as sep3 } from "node:path";
|
|
1799
1901
|
import { VFile } from "vfile";
|
|
1800
1902
|
import { parse as parseYaml } from "yaml";
|
|
1801
1903
|
|
|
@@ -1844,11 +1946,11 @@ var docDataSchema = z2.object({
|
|
|
1844
1946
|
// src/entry/cli/_function/read-docs.ts
|
|
1845
1947
|
var readDocs = async (options) => {
|
|
1846
1948
|
const paths = (await readdir(options.root, { recursive: true })).filter(
|
|
1847
|
-
(path) => /\.mdx?$/i.test(path) && !/^(readme|agents|claude)\.md$/i.test(path) && !path.split(
|
|
1949
|
+
(path) => /\.mdx?$/i.test(path) && !/^(readme|agents|claude)\.md$/i.test(path) && !path.split(sep3).includes("node_modules") && !path.startsWith(`dist${sep3}`)
|
|
1848
1950
|
);
|
|
1849
1951
|
const docs = await Promise.all(
|
|
1850
1952
|
paths.map(async (path) => {
|
|
1851
|
-
const id = path.replace(/\.mdx?$/i, "").split(
|
|
1953
|
+
const id = path.replace(/\.mdx?$/i, "").split(sep3).join("/").toLowerCase();
|
|
1852
1954
|
const segments = id.split("/");
|
|
1853
1955
|
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] === asset_dir || segments[0] === asset_favicon_path.slice(1)) {
|
|
1854
1956
|
throw new Error(`${copy_error_doc_id}: ${path}`);
|
|
@@ -2042,15 +2144,15 @@ var toErrorMessage = (error) => {
|
|
|
2042
2144
|
|
|
2043
2145
|
// src/util/font/to-font-css.ts
|
|
2044
2146
|
import { createHash as createHash2 } from "node:crypto";
|
|
2045
|
-
import { readFileSync as
|
|
2046
|
-
import { basename, dirname as
|
|
2147
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
2148
|
+
import { basename, dirname as dirname4, extname as extname4, join as join3 } from "node:path";
|
|
2047
2149
|
var toFontCss = (options) => {
|
|
2048
2150
|
const files = /* @__PURE__ */ new Map();
|
|
2049
|
-
const css = [...
|
|
2151
|
+
const css = [...readFileSync3(options.css, "utf8").matchAll(/@font-face\s*\{[^}]*\}/g)].map(
|
|
2050
2152
|
(match) => match[0].replace(/font-display:\s*[^;]+;/g, `font-display: ${font_display_strategy};`).replace(/url\(([^)]+)\)/g, (_match, relativeUrl) => {
|
|
2051
|
-
const file = join3(
|
|
2052
|
-
const extension =
|
|
2053
|
-
const hash = createHash2("sha256").update(
|
|
2153
|
+
const file = join3(dirname4(options.css), relativeUrl);
|
|
2154
|
+
const extension = extname4(file);
|
|
2155
|
+
const hash = createHash2("sha256").update(readFileSync3(file)).digest("hex").slice(0, font_hash_length);
|
|
2054
2156
|
const url = `${options.fontDir}/${basename(file, extension)}.${hash}${extension}`;
|
|
2055
2157
|
files.set(url, file);
|
|
2056
2158
|
return `url(${url})`;
|
|
@@ -2067,10 +2169,10 @@ var main = async () => {
|
|
|
2067
2169
|
throw new Error(`${copy_error_unknown_command}: ${command} (${commands.join(", ")})`);
|
|
2068
2170
|
}
|
|
2069
2171
|
const kitRoot = fileURLToPath(new URL("../", import.meta.url));
|
|
2070
|
-
const docsRoot =
|
|
2172
|
+
const docsRoot = resolve4(docsDir);
|
|
2071
2173
|
const outDir = join4(docsRoot, "dist");
|
|
2072
2174
|
const configFile = join4(docsRoot, cli_config_file_name);
|
|
2073
|
-
const parsedConfig = siteConfigSchema.safeParse(
|
|
2175
|
+
const parsedConfig = siteConfigSchema.safeParse(existsSync4(configFile) ? (await import(pathToFileURL(configFile).href)).default : site_config_absent);
|
|
2074
2176
|
if (!parsedConfig.success) {
|
|
2075
2177
|
throw new Error(`${copy_error_config}: ${parsedConfig.error.issues.map((issue) => `${issue.path.join(".")} ${issue.message}`).join(", ")}`);
|
|
2076
2178
|
}
|
|
@@ -2080,7 +2182,7 @@ var main = async () => {
|
|
|
2080
2182
|
app2.use(
|
|
2081
2183
|
"/*",
|
|
2082
2184
|
serveStatic({
|
|
2083
|
-
root:
|
|
2185
|
+
root: relative5(process.cwd(), outDir),
|
|
2084
2186
|
onFound: (_path, c) => {
|
|
2085
2187
|
if (c.req.path.startsWith(`${asset_font_dir}/`)) {
|
|
2086
2188
|
c.header("Cache-Control", font_cache_control);
|
|
@@ -2096,7 +2198,7 @@ var main = async () => {
|
|
|
2096
2198
|
const brand = toFontCss({ css: join4(kitRoot, font_brand_css), fontDir: asset_font_dir });
|
|
2097
2199
|
const fontCss = [mono.css, body.css, brand.css].join("\n");
|
|
2098
2200
|
const stylePath = toAssetPath({ path: asset_style_path, file: join4(kitRoot, "dist/cli.css") });
|
|
2099
|
-
const browserScript =
|
|
2201
|
+
const browserScript = readFileSync4(join4(kitRoot, "dist/browser.js"), "utf8");
|
|
2100
2202
|
const kitFiles = new Map([
|
|
2101
2203
|
[stylePath, join4(kitRoot, "dist/cli.css")],
|
|
2102
2204
|
[asset_favicon_path, join4(kitRoot, "src/asset/favicon.svg")],
|
|
@@ -2105,7 +2207,8 @@ var main = async () => {
|
|
|
2105
2207
|
...brand.files
|
|
2106
2208
|
]);
|
|
2107
2209
|
const files = new Map(kitFiles);
|
|
2108
|
-
const
|
|
2210
|
+
const snippets = /* @__PURE__ */ new Set();
|
|
2211
|
+
const processor = createProcessor({ site: siteConfig, root: docsRoot, files, snippets });
|
|
2109
2212
|
const [docs, home] = await Promise.all([
|
|
2110
2213
|
readDocs({ root: docsRoot, processor, reserved: siteConfig.url === void 0 ? void 0 : crawling_reserved_roots }),
|
|
2111
2214
|
readHome({ root: docsRoot, processor })
|
|
@@ -2128,11 +2231,11 @@ var main = async () => {
|
|
|
2128
2231
|
throw crawlerResult.error;
|
|
2129
2232
|
}
|
|
2130
2233
|
for (const [url, file] of files) {
|
|
2131
|
-
await mkdir(
|
|
2234
|
+
await mkdir(dirname5(join4(outDir, url)), { recursive: true });
|
|
2132
2235
|
await copyFile(file, join4(outDir, url));
|
|
2133
2236
|
}
|
|
2134
2237
|
console.log(
|
|
2135
|
-
`${crawlerResult === void 0 ? result.files.length : result.files.length + crawlerResult.files.length} files, ${files.size} assets \u2192 ${
|
|
2238
|
+
`${crawlerResult === void 0 ? result.files.length : result.files.length + crawlerResult.files.length} files, ${files.size} assets \u2192 ${relative5(process.cwd(), outDir)}`
|
|
2136
2239
|
);
|
|
2137
2240
|
return;
|
|
2138
2241
|
}
|
|
@@ -2144,7 +2247,8 @@ var main = async () => {
|
|
|
2144
2247
|
run: async () => {
|
|
2145
2248
|
const currentGeneration = generation;
|
|
2146
2249
|
const nextFiles = new Map(kitFiles);
|
|
2147
|
-
const
|
|
2250
|
+
const nextSnippets = /* @__PURE__ */ new Set();
|
|
2251
|
+
const nextProcessor = createProcessor({ site: siteConfig, root: docsRoot, files: nextFiles, snippets: nextSnippets });
|
|
2148
2252
|
const [docs2, home2] = await Promise.all([
|
|
2149
2253
|
readDocs({ root: docsRoot, processor: nextProcessor, reserved: siteConfig.url === void 0 ? void 0 : crawling_reserved_roots }),
|
|
2150
2254
|
readHome({ root: docsRoot, processor: nextProcessor })
|
|
@@ -2156,6 +2260,10 @@ var main = async () => {
|
|
|
2156
2260
|
for (const entry of nextFiles) {
|
|
2157
2261
|
files.set(...entry);
|
|
2158
2262
|
}
|
|
2263
|
+
snippets.clear();
|
|
2264
|
+
for (const entry of nextSnippets) {
|
|
2265
|
+
snippets.add(entry);
|
|
2266
|
+
}
|
|
2159
2267
|
store.docs = docs2;
|
|
2160
2268
|
store.home = home2;
|
|
2161
2269
|
reload.emit("change");
|
|
@@ -2166,8 +2274,9 @@ var main = async () => {
|
|
|
2166
2274
|
);
|
|
2167
2275
|
const handleDocsChange = (_event, filename) => {
|
|
2168
2276
|
if (filename !== null) {
|
|
2169
|
-
const segments = filename.split(
|
|
2170
|
-
|
|
2277
|
+
const segments = filename.split(sep4);
|
|
2278
|
+
const isSnippet = snippets.has(join4(docsRoot, filename));
|
|
2279
|
+
if (segments.includes("dist") || segments.includes("node_modules") || !isSnippet && !/\.mdx?$/i.test(filename) && !asset_media_extensions.has(extname5(filename).toLowerCase())) {
|
|
2171
2280
|
return;
|
|
2172
2281
|
}
|
|
2173
2282
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "for-humanity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Documents written for humans to read: a docs kit with a sidebar, parts, sections and flowcharts, built on Hono JSX",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"docs",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@hono/node-server": "^2.1.3",
|
|
53
53
|
"@shikijs/rehype": "^4.4.3",
|
|
54
|
+
"@shikijs/transformers": "4.4.3",
|
|
54
55
|
"beautiful-mermaid": "^1.1.3",
|
|
55
56
|
"clsx": "^2.1.1",
|
|
56
57
|
"es-toolkit": "^1.52.0",
|
|
@@ -176,10 +176,10 @@
|
|
|
176
176
|
margin-bottom: 0;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
/* 표 · 코드
|
|
179
|
+
/* 표 · 코드 상자와 같은 선 색으로 바깥 선과 칸 선 · 머리 행은 바탕 */
|
|
180
180
|
& table {
|
|
181
181
|
width: 100%;
|
|
182
|
-
border: 1px solid var(--app-color-border
|
|
182
|
+
border: 1px solid var(--app-color-border);
|
|
183
183
|
border-collapse: collapse;
|
|
184
184
|
font-size: var(--app-font-size-dense);
|
|
185
185
|
font-variant-numeric: tabular-nums;
|
|
@@ -187,8 +187,7 @@
|
|
|
187
187
|
|
|
188
188
|
& th {
|
|
189
189
|
padding: var(--app-space-inline) var(--app-space-stack);
|
|
190
|
-
border: 1px solid var(--app-color-border
|
|
191
|
-
border-bottom-color: var(--app-color-border);
|
|
190
|
+
border: 1px solid var(--app-color-border);
|
|
192
191
|
background: var(--app-color-code);
|
|
193
192
|
color: var(--app-color-text-strong);
|
|
194
193
|
font-weight: var(--app-font-weight-strong);
|
|
@@ -199,7 +198,7 @@
|
|
|
199
198
|
|
|
200
199
|
& td {
|
|
201
200
|
padding: var(--app-space-inline) var(--app-space-stack);
|
|
202
|
-
border: 1px solid var(--app-color-border
|
|
201
|
+
border: 1px solid var(--app-color-border);
|
|
203
202
|
text-align: left;
|
|
204
203
|
vertical-align: top;
|
|
205
204
|
}
|
|
@@ -215,34 +214,71 @@
|
|
|
215
214
|
border-top: 1px dotted var(--app-color-border);
|
|
216
215
|
}
|
|
217
216
|
|
|
218
|
-
/*
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
217
|
+
/* stylelint-disable selector-class-pattern -- Shiki 가 만드는 line·highlighted·diff 클래스 */
|
|
218
|
+
|
|
219
|
+
/* 코드 줄 · 바탕을 상자 가로 전체로 · 표기 없는 줄은 보이는 차이 없음 */
|
|
220
|
+
& pre code .line {
|
|
221
|
+
display: inline-block;
|
|
222
|
+
width: calc(100% + 2 * var(--app-space-block));
|
|
223
|
+
margin: 0 calc(-1 * var(--app-space-block));
|
|
224
|
+
padding: 0 var(--app-space-block);
|
|
222
225
|
}
|
|
223
226
|
|
|
224
|
-
& .
|
|
225
|
-
|
|
227
|
+
& pre code .line.highlighted {
|
|
228
|
+
background: var(--app-color-accent-soft);
|
|
226
229
|
}
|
|
227
230
|
|
|
228
|
-
& .
|
|
229
|
-
padding: var(--app-space-inline) var(--app-space-block);
|
|
231
|
+
& pre code .line.highlighted.error {
|
|
230
232
|
background: var(--app-color-bad-soft);
|
|
231
|
-
color: var(--app-color-bad);
|
|
232
|
-
font-size: var(--app-font-size-dense);
|
|
233
|
-
font-weight: var(--app-font-weight-strong);
|
|
234
233
|
}
|
|
235
234
|
|
|
236
|
-
&
|
|
235
|
+
& pre code .line.highlighted.warning {
|
|
236
|
+
background: var(--app-color-warn-soft);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
& pre code .line.diff.add {
|
|
237
240
|
background: var(--app-color-good-soft);
|
|
238
|
-
color: var(--app-color-good);
|
|
239
241
|
}
|
|
240
242
|
|
|
241
|
-
& .
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
243
|
+
& pre code .line.diff.remove {
|
|
244
|
+
background: var(--app-color-bad-soft);
|
|
245
|
+
opacity: 0.8;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* 색 블록 위의 줄 표기 · 바탕보다 한 단계 진하게 */
|
|
249
|
+
& pre.wg_prose__code--bad code .line.highlighted,
|
|
250
|
+
& pre.wg_prose__code--bad code .line.highlighted.error,
|
|
251
|
+
& pre.wg_prose__code--bad code .line.diff.remove {
|
|
252
|
+
background: var(--app-color-bad-mark);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
& pre.wg_prose__code--good code .line.highlighted,
|
|
256
|
+
& pre.wg_prose__code--good code .line.diff.add {
|
|
257
|
+
background: var(--app-color-good-mark);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
& pre code .highlighted-word {
|
|
261
|
+
padding: 0 2px;
|
|
262
|
+
border-radius: var(--app-radius-inline);
|
|
263
|
+
background: var(--app-color-accent-soft);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/* 줄 번호 · 펜스 메타 line-numbers · 복사에 섞이지 않게 content 로 */
|
|
267
|
+
& pre.wg_prose__lineNumbers code {
|
|
268
|
+
counter-reset: line;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
& pre.wg_prose__lineNumbers code .line::before {
|
|
272
|
+
display: inline-block;
|
|
273
|
+
width: 2em;
|
|
274
|
+
margin-right: var(--app-space-stack);
|
|
275
|
+
color: var(--app-color-text-muted);
|
|
276
|
+
text-align: right;
|
|
277
|
+
content: counter(line);
|
|
278
|
+
counter-increment: line;
|
|
279
|
+
user-select: none;
|
|
245
280
|
}
|
|
281
|
+
/* stylelint-enable selector-class-pattern */
|
|
246
282
|
/* stylelint-enable selector-disallowed-list */
|
|
247
283
|
}
|
|
248
284
|
|
|
@@ -354,7 +390,7 @@
|
|
|
354
390
|
overflow-x: auto;
|
|
355
391
|
margin: var(--app-space-block) 0;
|
|
356
392
|
padding: var(--app-space-block);
|
|
357
|
-
border: 1px solid var(--app-color-border
|
|
393
|
+
border: 1px solid var(--app-color-border);
|
|
358
394
|
border-radius: var(--app-radius-box);
|
|
359
395
|
|
|
360
396
|
/* 라벨 위치는 자간 0의 격자로 계산 */
|
package/src/constant/copy.ts
CHANGED
|
@@ -126,3 +126,8 @@ export const copy_warn_unknown_status = "설정에 없는 상태 문구다";
|
|
|
126
126
|
* 문서에서 참조한 로컬 자원의 경로·형식 오류
|
|
127
127
|
*/
|
|
128
128
|
export const copy_error_asset = "자원이 올바르지 않다";
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* `<<<` 로 불러온 코드 스니펫의 경로·파일·region 오류
|
|
132
|
+
*/
|
|
133
|
+
export const copy_error_snippet = "코드 스니펫이 올바르지 않다";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 스니펫 줄 · `<<< ./path.ext`, 선택 `#region`, 선택 메타(펜스 메타와 같음)
|
|
3
|
+
*/
|
|
4
|
+
export const snippet_pattern = /^<<<\s+(\S+?)(?:#([\w-]+))?(?:\s+(.+?))?\s*$/;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* region 표시 줄 · 주석 모양은 자유 · `#region 이름`, `#endregion 이름`
|
|
8
|
+
*/
|
|
9
|
+
export const snippet_region_pattern = /^\s*\S*\s*#(end)?region\s+([\w-]+)/;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 확장자 → 펜스 언어 · 없으면 확장자 그대로
|
|
13
|
+
*/
|
|
14
|
+
export const snippet_language_by_extension: Record<string, string> = {
|
|
15
|
+
".cjs": "js",
|
|
16
|
+
".md": "markdown",
|
|
17
|
+
".mjs": "js",
|
|
18
|
+
".mts": "ts",
|
|
19
|
+
".txt": "text",
|
|
20
|
+
".yml": "yaml",
|
|
21
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {existsSync, readFileSync, realpathSync, statSync} from "node:fs";
|
|
2
|
+
import {dirname, extname, isAbsolute, relative, resolve, sep} 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_error_snippet} from "@/constant/copy";
|
|
7
|
+
import {snippet_language_by_extension, snippet_pattern, snippet_region_pattern} from "@/entry/cli/_constant/snippet";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 스니펫 수집 계약
|
|
11
|
+
*/
|
|
12
|
+
export interface SnippetOptions {
|
|
13
|
+
/**
|
|
14
|
+
* 스니펫을 읽을 수 있는 문서 루트
|
|
15
|
+
*/
|
|
16
|
+
root: string;
|
|
17
|
+
/**
|
|
18
|
+
* dev 새로고침이 지켜볼 불러온 파일 경로
|
|
19
|
+
*/
|
|
20
|
+
snippets: Set<string>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* region 이름이 있으면 그 안만, 없으면 전체 · region 표시 줄은 항상 제거 · 공통 들여쓰기 제거
|
|
25
|
+
*/
|
|
26
|
+
const toSnippetBody = (source: string, region: string | undefined): string | undefined => {
|
|
27
|
+
const lines = source.replace(/\r\n/g, "\n").split("\n");
|
|
28
|
+
let inside = region === undefined;
|
|
29
|
+
let found = region === undefined;
|
|
30
|
+
const kept: string[] = [];
|
|
31
|
+
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
const marker = snippet_region_pattern.exec(line);
|
|
34
|
+
|
|
35
|
+
if (marker !== null) {
|
|
36
|
+
if (region !== undefined && marker[2] === region) {
|
|
37
|
+
inside = marker[1] === undefined;
|
|
38
|
+
found = true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (inside) {
|
|
45
|
+
kept.push(line);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!found) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
while (kept.length > 0 && kept[0]?.trim() === "") {
|
|
54
|
+
kept.shift();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
while (kept.length > 0 && kept.at(-1)?.trim() === "") {
|
|
58
|
+
kept.pop();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const indents = kept.filter((line) => line.trim() !== "").map((line) => line.length - line.trimStart().length);
|
|
62
|
+
const indent = indents.length === 0 ? 0 : Math.min(...indents);
|
|
63
|
+
|
|
64
|
+
return kept.map((line) => line.slice(Math.min(indent, line.length - line.trimStart().length))).join("\n");
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* `<<< ./path.ext#region 메타` 문단 → 코드 펜스 · 파일 위치 기준 경로 · 문서 폴더 밖과 없는 파일은 빌드 실패
|
|
69
|
+
* 불러온 파일은 dev 감시 목록에 올림
|
|
70
|
+
*/
|
|
71
|
+
export const remarkSnippets = (options: SnippetOptions) => (tree: Root, file: VFile) => {
|
|
72
|
+
visit(tree, "paragraph", (node, index, parent) => {
|
|
73
|
+
const [child] = node.children;
|
|
74
|
+
|
|
75
|
+
if (node.children.length !== 1 || child === undefined || child.type !== "text" || parent === undefined || index === undefined) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const match = snippet_pattern.exec(child.value.trim());
|
|
80
|
+
|
|
81
|
+
if (match === null) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const [, path = "", region, meta] = match;
|
|
86
|
+
const target = resolve(dirname(file.path), path);
|
|
87
|
+
const location = relative(options.root, target);
|
|
88
|
+
|
|
89
|
+
if (location === ".." || location.startsWith(`..${sep}`) || isAbsolute(location)) {
|
|
90
|
+
file.fail(`${copy_error_snippet}: 문서 폴더 밖 · ${path}`, node);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!existsSync(target)) {
|
|
94
|
+
file.fail(`${copy_error_snippet}: 파일이 없다 · ${path}`, node);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const actual = realpathSync(target);
|
|
98
|
+
const actualLocation = relative(realpathSync(options.root), actual);
|
|
99
|
+
|
|
100
|
+
if (actualLocation === ".." || actualLocation.startsWith(`..${sep}`) || isAbsolute(actualLocation) || !statSync(actual).isFile()) {
|
|
101
|
+
file.fail(`${copy_error_snippet}: 문서 폴더 밖이거나 일반 파일이 아니다 · ${path}`, node);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const body = toSnippetBody(readFileSync(actual, "utf8"), region);
|
|
105
|
+
|
|
106
|
+
if (body === undefined) {
|
|
107
|
+
file.fail(`${copy_error_snippet}: region 이 없다 · ${path}#${region}`, node);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const extension = extname(actual).toLowerCase();
|
|
112
|
+
const lang = snippet_language_by_extension[extension] === undefined ? extension.slice(1) : snippet_language_by_extension[extension];
|
|
113
|
+
|
|
114
|
+
options.snippets.add(actual);
|
|
115
|
+
parent.children.splice(index, 1, {type: "code", lang, meta: meta === undefined ? null : meta, value: body});
|
|
116
|
+
});
|
|
117
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
transformerMetaHighlight,
|
|
3
|
+
transformerNotationDiff,
|
|
4
|
+
transformerNotationErrorLevel,
|
|
5
|
+
transformerNotationHighlight,
|
|
6
|
+
transformerNotationWordHighlight,
|
|
7
|
+
} from "@shikijs/transformers";
|
|
8
|
+
import type {ShikiTransformer} from "shiki";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 펜스 메타에 `line-numbers` 가 있으면 pre 에 줄 번호 클래스 · 번호는 CSS counter
|
|
12
|
+
*/
|
|
13
|
+
const lineNumbers: ShikiTransformer = {
|
|
14
|
+
name: "for-humanity:line-numbers",
|
|
15
|
+
pre(node) {
|
|
16
|
+
const meta = this.options.meta?.__raw;
|
|
17
|
+
|
|
18
|
+
if (typeof meta === "string" && /\bline-numbers\b/.test(meta)) {
|
|
19
|
+
this.addClassToHast(node, "wg_prose__lineNumbers");
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 펜스 메타 `bad`·`good` → pre 에 블록 색 클래스 · 바탕색은 token.css 가 바꿈
|
|
26
|
+
*/
|
|
27
|
+
const tone: ShikiTransformer = {
|
|
28
|
+
name: "for-humanity:code-tone",
|
|
29
|
+
pre(node) {
|
|
30
|
+
const meta = this.options.meta?.__raw;
|
|
31
|
+
|
|
32
|
+
if (typeof meta !== "string") {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const word of ["bad", "good"]) {
|
|
37
|
+
if (new RegExp(`\\b${word}\\b`).test(meta)) {
|
|
38
|
+
this.addClassToHast(node, `wg_prose__code--${word}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 코드 줄 표기 · `// [!code highlight|error|warning|++|--]`, `// [!code word:x]`, 메타 `{2,4-6}`, `line-numbers`, 블록 색 `bad`·`good`
|
|
46
|
+
*/
|
|
47
|
+
export const shikiTransformers: ShikiTransformer[] = [
|
|
48
|
+
transformerNotationHighlight({matchAlgorithm: "v3"}),
|
|
49
|
+
transformerNotationErrorLevel({matchAlgorithm: "v3"}),
|
|
50
|
+
transformerNotationDiff({matchAlgorithm: "v3"}),
|
|
51
|
+
transformerNotationWordHighlight({matchAlgorithm: "v3"}),
|
|
52
|
+
transformerMetaHighlight(),
|
|
53
|
+
lineNumbers,
|
|
54
|
+
tone,
|
|
55
|
+
];
|
|
@@ -13,7 +13,6 @@ import {rehypeHead} from "@/component/widget/prose/_function/rehype-head";
|
|
|
13
13
|
import {rehypeSections} from "@/component/widget/prose/_function/rehype-sections/rehype-sections";
|
|
14
14
|
import {rehypeTables} from "@/component/widget/prose/_function/rehype-tables";
|
|
15
15
|
import {remarkBlocks} from "@/component/widget/prose/_function/remark-blocks";
|
|
16
|
-
import {remarkCodeLabels} from "@/component/widget/prose/_function/remark-code-labels";
|
|
17
16
|
import {remarkFlow} from "@/component/widget/prose/_function/remark-flow";
|
|
18
17
|
import {remarkLinks} from "@/component/widget/prose/_function/remark-links";
|
|
19
18
|
import {remarkParts} from "@/component/widget/prose/_function/remark-parts";
|
|
@@ -23,31 +22,33 @@ import {remarkUnknownDirectives} from "@/component/widget/prose/_function/remark
|
|
|
23
22
|
import {rehypeAssets} from "@/entry/cli/_function/create-processor/_rehype-assets";
|
|
24
23
|
import {rehypeHeadingIds} from "@/entry/cli/_function/create-processor/_rehype-heading-ids";
|
|
25
24
|
import {remarkReport} from "@/entry/cli/_function/create-processor/_remark-report";
|
|
25
|
+
import {remarkSnippets} from "@/entry/cli/_function/create-processor/_remark-snippets";
|
|
26
|
+
import {shikiTransformers} from "@/entry/cli/_function/create-processor/_shiki-transformers";
|
|
26
27
|
import type {SiteConfig} from "@/type/site-config";
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
|
-
* Markdown 처리 순서: remark(
|
|
30
|
+
* Markdown 처리 순서: remark(스니펫·부품·흐름도) → 코드 강조(줄 표기·블록 색 transformer) → 머리 → 제목 id → 가름 수집 → 표
|
|
30
31
|
* 플러그인 HTML 조각은 마지막 rehype-raw에서 해석
|
|
31
32
|
* Shiki inline style: token.css의 --app-code-* 참조
|
|
32
33
|
*/
|
|
33
|
-
export const createProcessor = (options: {site: SiteConfig; root: string; files?: Map<string, string>}) => {
|
|
34
|
+
export const createProcessor = (options: {site: SiteConfig; root: string; files?: Map<string, string>; snippets?: Set<string>}) => {
|
|
34
35
|
return unified()
|
|
35
36
|
.use(remarkParse)
|
|
36
37
|
.use(remarkFrontmatter, ["yaml"])
|
|
37
38
|
.use(remarkGfm)
|
|
39
|
+
.use(remarkSnippets, {root: options.root, snippets: options.snippets ?? new Set<string>()})
|
|
38
40
|
.use(remarkSmartypants)
|
|
39
41
|
.use(remarkDirective)
|
|
40
42
|
.use(remarkBlocks)
|
|
41
43
|
.use(remarkParts)
|
|
42
44
|
.use(remarkFlow)
|
|
43
|
-
.use(remarkCodeLabels)
|
|
44
45
|
.use(remarkStatus, {status: options.site.status})
|
|
45
46
|
.use(remarkSwatch)
|
|
46
47
|
.use(remarkLinks, {root: options.root})
|
|
47
48
|
.use(remarkUnknownDirectives)
|
|
48
49
|
.use(remarkReport, {root: options.root})
|
|
49
50
|
.use(remarkRehype, {allowDangerousHtml: true})
|
|
50
|
-
.use(rehypeShiki, {theme: createCssVariablesTheme({name: "for-humanity", variablePrefix: "--app-code-"})})
|
|
51
|
+
.use(rehypeShiki, {theme: createCssVariablesTheme({name: "for-humanity", variablePrefix: "--app-code-"}), transformers: shikiTransformers})
|
|
51
52
|
.use(rehypeHead)
|
|
52
53
|
.use(rehypeHeadingIds)
|
|
53
54
|
.use(rehypeSections)
|
|
@@ -73,25 +73,78 @@ test("site port defaults to 4321 and accepts only integers in the TCP range", ()
|
|
|
73
73
|
}
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
test("code
|
|
77
|
-
const root = await mkdtemp(join(tmpdir(), "for-humanity-code-
|
|
76
|
+
test("code notation marks lines, meta highlights lines, and line-numbers tags the block", async () => {
|
|
77
|
+
const root = await mkdtemp(join(tmpdir(), "for-humanity-code-notation-"));
|
|
78
78
|
|
|
79
79
|
try {
|
|
80
80
|
await writeFile(
|
|
81
81
|
join(root, "guide.md"),
|
|
82
|
-
"---\nname: Guide\nlabel: Guide\ngroup: Guide\n---\n\n## Read\n\n```ts
|
|
82
|
+
"---\nname: Guide\nlabel: Guide\ngroup: Guide\n---\n\n## Read\n\n```ts line-numbers\nconst a = 1; // [!code error]\nconst b = 2; // [!code ++]\nconst c = 3;\n```\n\n```txt\n# [!code highlight]\nfirst\nsecond\n```\n\n```css {2}\n.a {\n color: red;\n}\n```\n",
|
|
83
83
|
);
|
|
84
84
|
const site = siteConfigSchema.parse({});
|
|
85
85
|
const [doc] = await readDocs({root, processor: createProcessor({site, root})});
|
|
86
86
|
|
|
87
87
|
assert.ok(doc);
|
|
88
|
-
assert.match(
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
assert.match(doc.html, /<pre class="[^"]*wg_prose__lineNumbers[^"]*"/);
|
|
89
|
+
assert.match(doc.html, /<span class="line highlighted error">/);
|
|
90
|
+
assert.match(doc.html, /<span class="line diff add">/);
|
|
91
|
+
assert.match(doc.html, /<span class="line highlighted">[^\n]*first/);
|
|
92
|
+
assert.equal(doc.html.includes("[!code"), false, "notation comments are removed from the output");
|
|
93
|
+
assert.equal(doc.html.split('<span class="line highlighted">').length - 1, 2, "a standalone txt notation line and the css {2} meta each highlight one line");
|
|
94
|
+
} finally {
|
|
95
|
+
await rm(root, {recursive: true, force: true});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("code snippets import a file or a region from inside the document root and pass fence meta", async () => {
|
|
100
|
+
const root = await mkdtemp(join(tmpdir(), "for-humanity-snippets-"));
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
await mkdir(join(root, "snippet"));
|
|
104
|
+
await writeFile(
|
|
105
|
+
join(root, "snippet", "example.ts"),
|
|
106
|
+
"const outside = 0;\n// #region picked\n const a = 1; // [!code error]\n const b = 2;\n// #endregion picked\nconst after = 3;\n",
|
|
107
|
+
);
|
|
108
|
+
await writeFile(
|
|
109
|
+
join(root, "guide.md"),
|
|
110
|
+
"---\nname: Guide\nlabel: Guide\ngroup: Guide\n---\n\n## Read\n\n<<< ./snippet/example.ts#picked bad line-numbers\n\n<<< ./snippet/example.ts\n",
|
|
111
|
+
);
|
|
112
|
+
const site = siteConfigSchema.parse({});
|
|
113
|
+
const snippets = new Set<string>();
|
|
114
|
+
const [doc] = await readDocs({root, processor: createProcessor({site, root, snippets})});
|
|
115
|
+
|
|
116
|
+
assert.ok(doc);
|
|
117
|
+
assert.match(doc.html, /<pre class="[^"]*wg_prose__lineNumbers[^"]*wg_prose__code--bad[^"]*"|<pre class="[^"]*wg_prose__code--bad[^"]*wg_prose__lineNumbers[^"]*"/);
|
|
118
|
+
assert.match(doc.html, /<span class="line highlighted error">[^\n]*const<\/span>[^\n]* a/);
|
|
119
|
+
assert.equal(doc.html.includes("#region"), false, "region marker lines are removed");
|
|
120
|
+
assert.equal(doc.html.split("const</span>").length - 1, 2 + 4, "the region block has two lines and the whole-file block four");
|
|
121
|
+
assert.deepEqual(
|
|
122
|
+
[...snippets].map((entry) => entry.endsWith(join("snippet", "example.ts"))),
|
|
123
|
+
[true],
|
|
91
124
|
);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
125
|
+
} finally {
|
|
126
|
+
await rm(root, {recursive: true, force: true});
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("code snippets reject files outside the document root, missing files and missing regions", async () => {
|
|
131
|
+
const root = await mkdtemp(join(tmpdir(), "for-humanity-snippets-reject-"));
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
await mkdir(join(root, "docs"));
|
|
135
|
+
await writeFile(join(root, "outside.ts"), "const x = 1;\n");
|
|
136
|
+
await writeFile(join(root, "docs", "present.ts"), "const y = 2;\n");
|
|
137
|
+
const site = siteConfigSchema.parse({});
|
|
138
|
+
const docsRoot = join(root, "docs");
|
|
139
|
+
|
|
140
|
+
for (const [reference, expected] of [
|
|
141
|
+
["../outside.ts", /문서 폴더 밖/],
|
|
142
|
+
["./missing.ts", /파일이 없다/],
|
|
143
|
+
["./present.ts#nope", /region 이 없다/],
|
|
144
|
+
] as const) {
|
|
145
|
+
await writeFile(join(docsRoot, "guide.md"), `---\nname: Guide\nlabel: Guide\ngroup: Guide\n---\n\n<<< ${reference}\n`);
|
|
146
|
+
await assert.rejects(readDocs({root: docsRoot, processor: createProcessor({site, root: docsRoot})}), expected, reference);
|
|
147
|
+
}
|
|
95
148
|
} finally {
|
|
96
149
|
await rm(root, {recursive: true, force: true});
|
|
97
150
|
}
|
package/src/entry/cli/cli.ts
CHANGED
|
@@ -92,7 +92,8 @@ const main = async () => {
|
|
|
92
92
|
...brand.files,
|
|
93
93
|
]);
|
|
94
94
|
const files = new Map(kitFiles);
|
|
95
|
-
const
|
|
95
|
+
const snippets = new Set<string>();
|
|
96
|
+
const processor = createProcessor({site: siteConfig, root: docsRoot, files, snippets});
|
|
96
97
|
const [docs, home] = await Promise.all([
|
|
97
98
|
readDocs({root: docsRoot, processor, reserved: siteConfig.url === undefined ? undefined : crawling_reserved_roots}),
|
|
98
99
|
readHome({root: docsRoot, processor}),
|
|
@@ -147,7 +148,8 @@ const main = async () => {
|
|
|
147
148
|
run: async () => {
|
|
148
149
|
const currentGeneration = generation;
|
|
149
150
|
const nextFiles = new Map(kitFiles);
|
|
150
|
-
const
|
|
151
|
+
const nextSnippets = new Set<string>();
|
|
152
|
+
const nextProcessor = createProcessor({site: siteConfig, root: docsRoot, files: nextFiles, snippets: nextSnippets});
|
|
151
153
|
const [docs, home] = await Promise.all([
|
|
152
154
|
readDocs({root: docsRoot, processor: nextProcessor, reserved: siteConfig.url === undefined ? undefined : crawling_reserved_roots}),
|
|
153
155
|
readHome({root: docsRoot, processor: nextProcessor}),
|
|
@@ -161,6 +163,10 @@ const main = async () => {
|
|
|
161
163
|
for (const entry of nextFiles) {
|
|
162
164
|
files.set(...entry);
|
|
163
165
|
}
|
|
166
|
+
snippets.clear();
|
|
167
|
+
for (const entry of nextSnippets) {
|
|
168
|
+
snippets.add(entry);
|
|
169
|
+
}
|
|
164
170
|
store.docs = docs;
|
|
165
171
|
store.home = home;
|
|
166
172
|
reload.emit("change");
|
|
@@ -171,12 +177,17 @@ const main = async () => {
|
|
|
171
177
|
);
|
|
172
178
|
|
|
173
179
|
/**
|
|
174
|
-
* 입력 변경 감지 · 출력·설치 폴더 제외 · 누락 자원 추가도 다시 확인
|
|
180
|
+
* 입력 변경 감지 · 출력·설치 폴더 제외 · 누락 자원 추가도 다시 확인 · 불러온 스니펫 파일 변경 포함
|
|
175
181
|
*/
|
|
176
182
|
const handleDocsChange = (_event: string, filename: string | null) => {
|
|
177
183
|
if (filename !== null) {
|
|
178
184
|
const segments = filename.split(sep);
|
|
179
|
-
|
|
185
|
+
const isSnippet = snippets.has(join(docsRoot, filename));
|
|
186
|
+
if (
|
|
187
|
+
segments.includes("dist") ||
|
|
188
|
+
segments.includes("node_modules") ||
|
|
189
|
+
(!isSnippet && !/\.mdx?$/i.test(filename) && !asset_media_extensions.has(extname(filename).toLowerCase()))
|
|
190
|
+
) {
|
|
180
191
|
return;
|
|
181
192
|
}
|
|
182
193
|
}
|
package/src/style/token.css
CHANGED
|
@@ -35,11 +35,15 @@
|
|
|
35
35
|
--app-color-accent: light-dark(#555, #b5b5b5);
|
|
36
36
|
--app-color-accent-soft: light-dark(#f5f5f2, #222);
|
|
37
37
|
|
|
38
|
-
/*
|
|
38
|
+
/* 코드 판정 색 · soft 는 블록 바탕(메타 bad·good), mark 는 그 위의 줄 표기 · warn 은 줄 표기만 */
|
|
39
39
|
--app-color-bad: light-dark(#b2412e, #ff9b8a);
|
|
40
|
-
--app-color-bad-soft: light-dark(#
|
|
40
|
+
--app-color-bad-soft: light-dark(#f9ebe8, #3a2522);
|
|
41
|
+
--app-color-bad-mark: light-dark(#f1cdc6, #5b322c);
|
|
41
42
|
--app-color-good: light-dark(#2f7a47, #8bd3a3);
|
|
42
|
-
--app-color-good-soft: light-dark(#
|
|
43
|
+
--app-color-good-soft: light-dark(#e4f3ed, #1c342c);
|
|
44
|
+
--app-color-good-mark: light-dark(#c4e4d4, #27503f);
|
|
45
|
+
--app-color-warn: light-dark(#8a6400, #e6c46a);
|
|
46
|
+
--app-color-warn-soft: light-dark(#fbf3d9, #3d3418);
|
|
43
47
|
|
|
44
48
|
/* 링크 · 기본·방문·hover */
|
|
45
49
|
--app-color-link: light-dark(#11a, #9db8ff);
|
|
@@ -172,11 +176,11 @@
|
|
|
172
176
|
}
|
|
173
177
|
}
|
|
174
178
|
|
|
175
|
-
/*
|
|
176
|
-
.
|
|
179
|
+
/* 블록 색 · 펜스 메타 bad·good · Shiki 가 pre 에 인라인으로 쓰는 코드 바탕을 바꿈 */
|
|
180
|
+
.wg_prose__code--bad {
|
|
177
181
|
--app-code-background: var(--app-color-bad-soft);
|
|
178
182
|
}
|
|
179
183
|
|
|
180
|
-
.
|
|
184
|
+
.wg_prose__code--good {
|
|
181
185
|
--app-code-background: var(--app-color-good-soft);
|
|
182
186
|
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import type {Root} from "mdast";
|
|
2
|
-
import {visit} from "unist-util-visit";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 펜스 메타의 라벨 · `bad 제목`, `good 제목` · 제목은 선택
|
|
6
|
-
*/
|
|
7
|
-
const code_label_pattern = /^(bad|good)(?:\s+(.+?))?\s*$/;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* figure 여는 조각, 코드, 닫는 조각
|
|
11
|
-
*/
|
|
12
|
-
const inserted_node_count = 3;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* 제목을 HTML 문자열에 넣기 위한 escape
|
|
16
|
-
*/
|
|
17
|
-
const escapeHtml = (text: string) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* 코드 펜스 메타 `bad`·`good` → 색 띠와 제목이 붙은 figure · 다른 메타는 그대로 둠
|
|
21
|
-
* 코드 노드는 유지해 Shiki 강조를 받고, figure 조각은 마지막 rehype-raw에서 합쳐짐
|
|
22
|
-
*/
|
|
23
|
-
export const remarkCodeLabels = () => (tree: Root) => {
|
|
24
|
-
visit(tree, "code", (node, index, parent) => {
|
|
25
|
-
if (typeof node.meta !== "string" || parent === undefined || index === undefined) {
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const match = code_label_pattern.exec(node.meta);
|
|
30
|
-
|
|
31
|
-
if (match === null) {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const [, tone, title] = match;
|
|
36
|
-
const caption = title === undefined ? "" : `<figcaption class="wg_prose__codeLabelTitle">${escapeHtml(title)}</figcaption>`;
|
|
37
|
-
|
|
38
|
-
node.meta = null;
|
|
39
|
-
parent.children.splice(index, 1, {type: "html", value: `<figure class="wg_prose__codeLabel wg_prose__codeLabel--${tone}">${caption}`}, node, {
|
|
40
|
-
type: "html",
|
|
41
|
-
value: "</figure>",
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// 끼워 넣은 세 노드 다음부터 계속
|
|
45
|
-
return index + inserted_node_count;
|
|
46
|
-
});
|
|
47
|
-
};
|