for-humanity 0.9.0 → 0.10.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 +46 -27
- package/dist/cli.js +41 -40
- package/package.json +2 -1
- package/src/component/widget/prose/wg-prose.css +51 -26
- package/src/entry/cli/_function/create-processor/_shiki-transformers.ts +34 -0
- package/src/entry/cli/_function/create-processor/create-processor.ts +3 -4
- package/src/entry/cli/cli.test.ts +9 -10
- package/src/style/token.css +6 -4
- 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.10.0 — 2026-10-13
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
- Remove the `bad`/`good` code labels added in 0.9.0; mark the lines instead.
|
|
9
|
+
- Draw tables and flowchart boxes with the same border color as code blocks.
|
|
10
|
+
|
|
11
|
+
## 0.9.1 — 2026-10-13
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
- Tint the whole code block of a `bad` or `good` label instead of relying on the caption, so a label can carry its reason as a code comment.
|
|
15
|
+
|
|
5
16
|
## 0.9.0 — 2026-10-13
|
|
6
17
|
|
|
7
18
|
- Add code labels: a fence meta of `bad` or `good` with an optional title renders the code block with a colored left stripe and a caption, keeping Shiki highlighting.
|
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.10.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.10.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.10.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
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
}
|
|
24
24
|
& h2 {
|
|
25
25
|
margin: var(--app-space-section) 0 var(--app-space-stack);
|
|
26
|
-
padding-top: var(--app-space-
|
|
27
|
-
border-top: 1px
|
|
26
|
+
padding-top: var(--app-space-group);
|
|
27
|
+
border-top: 1px solid var(--app-color-border);
|
|
28
28
|
color: var(--app-color-text-strong);
|
|
29
29
|
font-size: var(--app-font-size-section);
|
|
30
30
|
font-weight: var(--app-font-weight-strong);
|
|
@@ -138,13 +138,14 @@
|
|
|
138
138
|
}
|
|
139
139
|
& table {
|
|
140
140
|
width: 100%;
|
|
141
|
+
border: 1px solid var(--app-color-border);
|
|
141
142
|
border-collapse: collapse;
|
|
142
143
|
font-size: var(--app-font-size-dense);
|
|
143
144
|
font-variant-numeric: tabular-nums;
|
|
144
145
|
}
|
|
145
146
|
& th {
|
|
146
147
|
padding: var(--app-space-inline) var(--app-space-stack);
|
|
147
|
-
border
|
|
148
|
+
border: 1px solid var(--app-color-border);
|
|
148
149
|
background: var(--app-color-code);
|
|
149
150
|
color: var(--app-color-text-strong);
|
|
150
151
|
font-weight: var(--app-font-weight-strong);
|
|
@@ -154,13 +155,10 @@
|
|
|
154
155
|
}
|
|
155
156
|
& td {
|
|
156
157
|
padding: var(--app-space-inline) var(--app-space-stack);
|
|
157
|
-
border
|
|
158
|
+
border: 1px solid var(--app-color-border);
|
|
158
159
|
text-align: left;
|
|
159
160
|
vertical-align: top;
|
|
160
161
|
}
|
|
161
|
-
& tbody tr:last-child td {
|
|
162
|
-
border-bottom: 0;
|
|
163
|
-
}
|
|
164
162
|
& tbody tr:hover td {
|
|
165
163
|
background: var(--app-color-accent-soft);
|
|
166
164
|
}
|
|
@@ -169,28 +167,45 @@
|
|
|
169
167
|
border: 0;
|
|
170
168
|
border-top: 1px dotted var(--app-color-border);
|
|
171
169
|
}
|
|
172
|
-
& .
|
|
173
|
-
|
|
174
|
-
|
|
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);
|
|
175
175
|
}
|
|
176
|
-
& .
|
|
177
|
-
|
|
176
|
+
& pre code .line.highlighted {
|
|
177
|
+
background: var(--app-color-accent-soft);
|
|
178
178
|
}
|
|
179
|
-
& .
|
|
180
|
-
padding: var(--app-space-inline) var(--app-space-block);
|
|
179
|
+
& pre code .line.highlighted.error {
|
|
181
180
|
background: var(--app-color-bad-soft);
|
|
182
|
-
color: var(--app-color-bad);
|
|
183
|
-
font-size: var(--app-font-size-dense);
|
|
184
|
-
font-weight: var(--app-font-weight-strong);
|
|
185
181
|
}
|
|
186
|
-
&
|
|
182
|
+
& pre code .line.highlighted.warning {
|
|
183
|
+
background: var(--app-color-warn-soft);
|
|
184
|
+
}
|
|
185
|
+
& pre code .line.diff.add {
|
|
187
186
|
background: var(--app-color-good-soft);
|
|
188
|
-
color: var(--app-color-good);
|
|
189
187
|
}
|
|
190
|
-
& .
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
188
|
+
& pre code .line.diff.remove {
|
|
189
|
+
background: var(--app-color-bad-soft);
|
|
190
|
+
opacity: 0.8;
|
|
191
|
+
}
|
|
192
|
+
& pre code .highlighted-word {
|
|
193
|
+
padding: 0 2px;
|
|
194
|
+
border-radius: var(--app-radius-inline);
|
|
195
|
+
background: var(--app-color-accent-soft);
|
|
196
|
+
}
|
|
197
|
+
& pre.wg_prose__lineNumbers code {
|
|
198
|
+
counter-reset: line;
|
|
199
|
+
}
|
|
200
|
+
& pre.wg_prose__lineNumbers code .line::before {
|
|
201
|
+
display: inline-block;
|
|
202
|
+
width: 2em;
|
|
203
|
+
margin-right: var(--app-space-stack);
|
|
204
|
+
color: var(--app-color-text-muted);
|
|
205
|
+
text-align: right;
|
|
206
|
+
content: counter(line);
|
|
207
|
+
counter-increment: line;
|
|
208
|
+
user-select: none;
|
|
194
209
|
}
|
|
195
210
|
}
|
|
196
211
|
.wg_prose__head {
|
|
@@ -272,7 +287,9 @@
|
|
|
272
287
|
.wg_prose__flow {
|
|
273
288
|
overflow-x: auto;
|
|
274
289
|
margin: var(--app-space-block) 0;
|
|
275
|
-
padding: var(--app-space-block)
|
|
290
|
+
padding: var(--app-space-block);
|
|
291
|
+
border: 1px solid var(--app-color-border);
|
|
292
|
+
border-radius: var(--app-radius-box);
|
|
276
293
|
letter-spacing: 0;
|
|
277
294
|
& svg {
|
|
278
295
|
display: block;
|
|
@@ -762,9 +779,11 @@
|
|
|
762
779
|
--app-color-accent: light-dark(#555, #b5b5b5);
|
|
763
780
|
--app-color-accent-soft: light-dark(#f5f5f2, #222);
|
|
764
781
|
--app-color-bad: light-dark(#b2412e, #ff9b8a);
|
|
765
|
-
--app-color-bad-soft: light-dark(#
|
|
782
|
+
--app-color-bad-soft: light-dark(#fbe9e6, #42241f);
|
|
766
783
|
--app-color-good: light-dark(#2f7a47, #8bd3a3);
|
|
767
|
-
--app-color-good-soft: light-dark(#
|
|
784
|
+
--app-color-good-soft: light-dark(#e3f3e8, #1f3a27);
|
|
785
|
+
--app-color-warn: light-dark(#8a6400, #e6c46a);
|
|
786
|
+
--app-color-warn-soft: light-dark(#fbf3d9, #3d3418);
|
|
768
787
|
--app-color-link: light-dark(#11a, #9db8ff);
|
|
769
788
|
--app-color-link-visited: light-dark(#51a, #c5a5ed);
|
|
770
789
|
--app-color-link-hover: light-dark(#44d, #c1d1ff);
|
|
@@ -804,7 +823,7 @@
|
|
|
804
823
|
--app-space-block: 14px;
|
|
805
824
|
--app-space-group: 20px;
|
|
806
825
|
--app-space-subsection: 28px;
|
|
807
|
-
--app-space-section:
|
|
826
|
+
--app-space-section: 64px;
|
|
808
827
|
--app-space-tree-row: 0px;
|
|
809
828
|
--app-space-tree-group: 16px;
|
|
810
829
|
--app-space-tree-label: 2px;
|
package/dist/cli.js
CHANGED
|
@@ -1231,33 +1231,8 @@ var remarkBlocks = () => (tree, file) => {
|
|
|
1231
1231
|
});
|
|
1232
1232
|
};
|
|
1233
1233
|
|
|
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
1234
|
// src/component/widget/prose/_function/remark-flow.ts
|
|
1260
|
-
import { visit as
|
|
1235
|
+
import { visit as visit3 } from "unist-util-visit";
|
|
1261
1236
|
|
|
1262
1237
|
// src/util/mermaid/render-flow.js
|
|
1263
1238
|
import { renderMermaidASCII } from "beautiful-mermaid";
|
|
@@ -1571,7 +1546,7 @@ var renderFlow = (source) => gridToSvg(renderMermaidASCII(widenCjk(source), ASCI
|
|
|
1571
1546
|
|
|
1572
1547
|
// src/component/widget/prose/_function/remark-flow.ts
|
|
1573
1548
|
var remarkFlow = () => (tree, file) => {
|
|
1574
|
-
|
|
1549
|
+
visit3(tree, "code", (node, index, parent) => {
|
|
1575
1550
|
if (node.lang !== "mermaid" || parent === void 0 || index === void 0) {
|
|
1576
1551
|
return;
|
|
1577
1552
|
}
|
|
@@ -1589,10 +1564,10 @@ var remarkFlow = () => (tree, file) => {
|
|
|
1589
1564
|
// src/component/widget/prose/_function/remark-links.ts
|
|
1590
1565
|
import { existsSync } from "node:fs";
|
|
1591
1566
|
import { dirname, posix, relative, resolve } from "node:path";
|
|
1592
|
-
import { visit as
|
|
1567
|
+
import { visit as visit4 } from "unist-util-visit";
|
|
1593
1568
|
var remarkLinks = (options) => (tree, file) => {
|
|
1594
1569
|
const here = dirname(relative(options.root, file.path));
|
|
1595
|
-
|
|
1570
|
+
visit4(tree, (node) => {
|
|
1596
1571
|
if (node.type !== "link" && node.type !== "definition") {
|
|
1597
1572
|
return;
|
|
1598
1573
|
}
|
|
@@ -1626,7 +1601,7 @@ var remarkParts = () => (tree) => {
|
|
|
1626
1601
|
};
|
|
1627
1602
|
|
|
1628
1603
|
// src/component/widget/prose/_function/remark-status.ts
|
|
1629
|
-
import { escape as
|
|
1604
|
+
import { escape as escapeHtml, escapeRegExp } from "es-toolkit/string";
|
|
1630
1605
|
import { findAndReplace } from "mdast-util-find-and-replace";
|
|
1631
1606
|
|
|
1632
1607
|
// src/component/widget/prose/_constant/status.ts
|
|
@@ -1652,7 +1627,7 @@ var remarkStatus = (options) => {
|
|
|
1652
1627
|
}
|
|
1653
1628
|
return {
|
|
1654
1629
|
type: "html",
|
|
1655
|
-
value: `<span class="wg_prose__pill wg_prose__pill--${item.kind}">${
|
|
1630
|
+
value: `<span class="wg_prose__pill wg_prose__pill--${item.kind}">${escapeHtml(text)}</span>`
|
|
1656
1631
|
};
|
|
1657
1632
|
}
|
|
1658
1633
|
],
|
|
@@ -1676,9 +1651,9 @@ var remarkStatus = (options) => {
|
|
|
1676
1651
|
};
|
|
1677
1652
|
|
|
1678
1653
|
// src/component/widget/prose/_function/remark-swatch.ts
|
|
1679
|
-
import { visit as
|
|
1654
|
+
import { visit as visit5 } from "unist-util-visit";
|
|
1680
1655
|
var remarkSwatch = () => (tree) => {
|
|
1681
|
-
|
|
1656
|
+
visit5(tree, "inlineCode", (node) => {
|
|
1682
1657
|
if (/^#[0-9a-f]{3,8}$/i.test(node.value.trim())) {
|
|
1683
1658
|
node.data = {
|
|
1684
1659
|
hProperties: { className: ["wg_prose__color"], style: `--wg-prose-chip-color: ${node.value.trim()}` }
|
|
@@ -1688,9 +1663,9 @@ var remarkSwatch = () => (tree) => {
|
|
|
1688
1663
|
};
|
|
1689
1664
|
|
|
1690
1665
|
// src/component/widget/prose/_function/remark-unknown-directives.ts
|
|
1691
|
-
import { SKIP as SKIP2, visit as
|
|
1666
|
+
import { SKIP as SKIP2, visit as visit6 } from "unist-util-visit";
|
|
1692
1667
|
var remarkUnknownDirectives = () => (tree, file) => {
|
|
1693
|
-
|
|
1668
|
+
visit6(tree, (node, index, parent) => {
|
|
1694
1669
|
if (node.type === "textDirective" && parent !== void 0 && index !== void 0) {
|
|
1695
1670
|
parent.children[index] = {
|
|
1696
1671
|
type: "text",
|
|
@@ -1707,7 +1682,7 @@ var remarkUnknownDirectives = () => (tree, file) => {
|
|
|
1707
1682
|
// src/entry/cli/_function/create-processor/_rehype-assets.ts
|
|
1708
1683
|
import { existsSync as existsSync2, realpathSync, statSync } from "node:fs";
|
|
1709
1684
|
import { dirname as dirname2, extname as extname2, isAbsolute, relative as relative2, resolve as resolve2, sep } from "node:path";
|
|
1710
|
-
import { visit as
|
|
1685
|
+
import { visit as visit7 } from "unist-util-visit";
|
|
1711
1686
|
|
|
1712
1687
|
// src/util/file/to-asset-path.ts
|
|
1713
1688
|
import { createHash } from "node:crypto";
|
|
@@ -1720,7 +1695,7 @@ var toAssetPath = (options) => {
|
|
|
1720
1695
|
|
|
1721
1696
|
// src/entry/cli/_function/create-processor/_rehype-assets.ts
|
|
1722
1697
|
var rehypeAssets = (options) => (tree, file) => {
|
|
1723
|
-
|
|
1698
|
+
visit7(tree, "element", (node) => {
|
|
1724
1699
|
const property = node.tagName === "img" ? "src" : "href";
|
|
1725
1700
|
const url = node.properties[property];
|
|
1726
1701
|
if (node.tagName !== "img" && node.tagName !== "a" || typeof url !== "string" || /^(?:[a-z][a-z\d+.-]*:|\/|#)/i.test(url)) {
|
|
@@ -1760,11 +1735,11 @@ var rehypeAssets = (options) => (tree, file) => {
|
|
|
1760
1735
|
// src/entry/cli/_function/create-processor/_rehype-heading-ids.ts
|
|
1761
1736
|
import Slugger from "github-slugger";
|
|
1762
1737
|
import { toString as toPlainText3 } from "hast-util-to-string";
|
|
1763
|
-
import { visit as
|
|
1738
|
+
import { visit as visit8 } from "unist-util-visit";
|
|
1764
1739
|
var rehypeHeadingIds = () => (tree, file) => {
|
|
1765
1740
|
const slugger = new Slugger();
|
|
1766
1741
|
const headings = [];
|
|
1767
|
-
|
|
1742
|
+
visit8(tree, "element", (node) => {
|
|
1768
1743
|
const depth = /^h([1-6])$/.exec(node.tagName)?.[1];
|
|
1769
1744
|
if (depth === void 0) {
|
|
1770
1745
|
return;
|
|
@@ -1788,9 +1763,35 @@ var remarkReport = (options) => (_tree, file) => {
|
|
|
1788
1763
|
}
|
|
1789
1764
|
};
|
|
1790
1765
|
|
|
1766
|
+
// src/entry/cli/_function/create-processor/_shiki-transformers.ts
|
|
1767
|
+
import {
|
|
1768
|
+
transformerMetaHighlight,
|
|
1769
|
+
transformerNotationDiff,
|
|
1770
|
+
transformerNotationErrorLevel,
|
|
1771
|
+
transformerNotationHighlight,
|
|
1772
|
+
transformerNotationWordHighlight
|
|
1773
|
+
} from "@shikijs/transformers";
|
|
1774
|
+
var lineNumbers = {
|
|
1775
|
+
name: "for-humanity:line-numbers",
|
|
1776
|
+
pre(node) {
|
|
1777
|
+
const meta = this.options.meta?.__raw;
|
|
1778
|
+
if (typeof meta === "string" && /\bline-numbers\b/.test(meta)) {
|
|
1779
|
+
this.addClassToHast(node, "wg_prose__lineNumbers");
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
var shikiTransformers = [
|
|
1784
|
+
transformerNotationHighlight({ matchAlgorithm: "v3" }),
|
|
1785
|
+
transformerNotationErrorLevel({ matchAlgorithm: "v3" }),
|
|
1786
|
+
transformerNotationDiff({ matchAlgorithm: "v3" }),
|
|
1787
|
+
transformerNotationWordHighlight({ matchAlgorithm: "v3" }),
|
|
1788
|
+
transformerMetaHighlight(),
|
|
1789
|
+
lineNumbers
|
|
1790
|
+
];
|
|
1791
|
+
|
|
1791
1792
|
// src/entry/cli/_function/create-processor/create-processor.ts
|
|
1792
1793
|
var createProcessor = (options) => {
|
|
1793
|
-
return unified().use(remarkParse).use(remarkFrontmatter, ["yaml"]).use(remarkGfm).use(remarkSmartypants).use(remarkDirective).use(remarkBlocks).use(remarkParts).use(remarkFlow).use(
|
|
1794
|
+
return unified().use(remarkParse).use(remarkFrontmatter, ["yaml"]).use(remarkGfm).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
1795
|
};
|
|
1795
1796
|
|
|
1796
1797
|
// src/entry/cli/_function/read-docs.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "for-humanity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
|
|
35
35
|
& h2 {
|
|
36
36
|
margin: var(--app-space-section) 0 var(--app-space-stack);
|
|
37
|
-
padding-top: var(--app-space-
|
|
38
|
-
border-top: 1px
|
|
37
|
+
padding-top: var(--app-space-group);
|
|
38
|
+
border-top: 1px solid var(--app-color-border);
|
|
39
39
|
color: var(--app-color-text-strong);
|
|
40
40
|
font-size: var(--app-font-size-section);
|
|
41
41
|
font-weight: var(--app-font-weight-strong);
|
|
@@ -176,8 +176,10 @@
|
|
|
176
176
|
margin-bottom: 0;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
/* 표 · 코드 상자와 같은 선 색으로 바깥 선과 칸 선 · 머리 행은 바탕 */
|
|
179
180
|
& table {
|
|
180
181
|
width: 100%;
|
|
182
|
+
border: 1px solid var(--app-color-border);
|
|
181
183
|
border-collapse: collapse;
|
|
182
184
|
font-size: var(--app-font-size-dense);
|
|
183
185
|
font-variant-numeric: tabular-nums;
|
|
@@ -185,7 +187,7 @@
|
|
|
185
187
|
|
|
186
188
|
& th {
|
|
187
189
|
padding: var(--app-space-inline) var(--app-space-stack);
|
|
188
|
-
border
|
|
190
|
+
border: 1px solid var(--app-color-border);
|
|
189
191
|
background: var(--app-color-code);
|
|
190
192
|
color: var(--app-color-text-strong);
|
|
191
193
|
font-weight: var(--app-font-weight-strong);
|
|
@@ -196,15 +198,11 @@
|
|
|
196
198
|
|
|
197
199
|
& td {
|
|
198
200
|
padding: var(--app-space-inline) var(--app-space-stack);
|
|
199
|
-
border
|
|
201
|
+
border: 1px solid var(--app-color-border);
|
|
200
202
|
text-align: left;
|
|
201
203
|
vertical-align: top;
|
|
202
204
|
}
|
|
203
205
|
|
|
204
|
-
& tbody tr:last-child td {
|
|
205
|
-
border-bottom: 0;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
206
|
/* 표의 읽는 행 표시 */
|
|
209
207
|
& tbody tr:hover td {
|
|
210
208
|
background: var(--app-color-accent-soft);
|
|
@@ -216,34 +214,59 @@
|
|
|
216
214
|
border-top: 1px dotted var(--app-color-border);
|
|
217
215
|
}
|
|
218
216
|
|
|
219
|
-
/*
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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);
|
|
223
225
|
}
|
|
224
226
|
|
|
225
|
-
& .
|
|
226
|
-
|
|
227
|
+
& pre code .line.highlighted {
|
|
228
|
+
background: var(--app-color-accent-soft);
|
|
227
229
|
}
|
|
228
230
|
|
|
229
|
-
& .
|
|
230
|
-
padding: var(--app-space-inline) var(--app-space-block);
|
|
231
|
+
& pre code .line.highlighted.error {
|
|
231
232
|
background: var(--app-color-bad-soft);
|
|
232
|
-
color: var(--app-color-bad);
|
|
233
|
-
font-size: var(--app-font-size-dense);
|
|
234
|
-
font-weight: var(--app-font-weight-strong);
|
|
235
233
|
}
|
|
236
234
|
|
|
237
|
-
&
|
|
235
|
+
& pre code .line.highlighted.warning {
|
|
236
|
+
background: var(--app-color-warn-soft);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
& pre code .line.diff.add {
|
|
238
240
|
background: var(--app-color-good-soft);
|
|
239
|
-
color: var(--app-color-good);
|
|
240
241
|
}
|
|
241
242
|
|
|
242
|
-
& .
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
243
|
+
& pre code .line.diff.remove {
|
|
244
|
+
background: var(--app-color-bad-soft);
|
|
245
|
+
opacity: 0.8;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
& pre code .highlighted-word {
|
|
249
|
+
padding: 0 2px;
|
|
250
|
+
border-radius: var(--app-radius-inline);
|
|
251
|
+
background: var(--app-color-accent-soft);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* 줄 번호 · 펜스 메타 line-numbers · 복사에 섞이지 않게 content 로 */
|
|
255
|
+
& pre.wg_prose__lineNumbers code {
|
|
256
|
+
counter-reset: line;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
& pre.wg_prose__lineNumbers code .line::before {
|
|
260
|
+
display: inline-block;
|
|
261
|
+
width: 2em;
|
|
262
|
+
margin-right: var(--app-space-stack);
|
|
263
|
+
color: var(--app-color-text-muted);
|
|
264
|
+
text-align: right;
|
|
265
|
+
content: counter(line);
|
|
266
|
+
counter-increment: line;
|
|
267
|
+
user-select: none;
|
|
246
268
|
}
|
|
269
|
+
/* stylelint-enable selector-class-pattern */
|
|
247
270
|
/* stylelint-enable selector-disallowed-list */
|
|
248
271
|
}
|
|
249
272
|
|
|
@@ -354,7 +377,9 @@
|
|
|
354
377
|
.wg_prose__flow {
|
|
355
378
|
overflow-x: auto;
|
|
356
379
|
margin: var(--app-space-block) 0;
|
|
357
|
-
padding: var(--app-space-block)
|
|
380
|
+
padding: var(--app-space-block);
|
|
381
|
+
border: 1px solid var(--app-color-border);
|
|
382
|
+
border-radius: var(--app-radius-box);
|
|
358
383
|
|
|
359
384
|
/* 라벨 위치는 자간 0의 격자로 계산 */
|
|
360
385
|
letter-spacing: 0;
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
* 코드 줄 표기 · `// [!code highlight|error|warning|++|--]`, `// [!code word:x]`, 메타 `{2,4-6}`, `line-numbers`
|
|
26
|
+
*/
|
|
27
|
+
export const shikiTransformers: ShikiTransformer[] = [
|
|
28
|
+
transformerNotationHighlight({matchAlgorithm: "v3"}),
|
|
29
|
+
transformerNotationErrorLevel({matchAlgorithm: "v3"}),
|
|
30
|
+
transformerNotationDiff({matchAlgorithm: "v3"}),
|
|
31
|
+
transformerNotationWordHighlight({matchAlgorithm: "v3"}),
|
|
32
|
+
transformerMetaHighlight(),
|
|
33
|
+
lineNumbers,
|
|
34
|
+
];
|
|
@@ -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,10 +22,11 @@ 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 {shikiTransformers} from "@/entry/cli/_function/create-processor/_shiki-transformers";
|
|
26
26
|
import type {SiteConfig} from "@/type/site-config";
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* Markdown 처리 순서: remark(
|
|
29
|
+
* Markdown 처리 순서: remark(부품·흐름도) → 코드 강조(줄 표기 transformer) → 머리 → 제목 id → 가름 수집 → 표
|
|
30
30
|
* 플러그인 HTML 조각은 마지막 rehype-raw에서 해석
|
|
31
31
|
* Shiki inline style: token.css의 --app-code-* 참조
|
|
32
32
|
*/
|
|
@@ -40,14 +40,13 @@ export const createProcessor = (options: {site: SiteConfig; root: string; files?
|
|
|
40
40
|
.use(remarkBlocks)
|
|
41
41
|
.use(remarkParts)
|
|
42
42
|
.use(remarkFlow)
|
|
43
|
-
.use(remarkCodeLabels)
|
|
44
43
|
.use(remarkStatus, {status: options.site.status})
|
|
45
44
|
.use(remarkSwatch)
|
|
46
45
|
.use(remarkLinks, {root: options.root})
|
|
47
46
|
.use(remarkUnknownDirectives)
|
|
48
47
|
.use(remarkReport, {root: options.root})
|
|
49
48
|
.use(remarkRehype, {allowDangerousHtml: true})
|
|
50
|
-
.use(rehypeShiki, {theme: createCssVariablesTheme({name: "for-humanity", variablePrefix: "--app-code-"})})
|
|
49
|
+
.use(rehypeShiki, {theme: createCssVariablesTheme({name: "for-humanity", variablePrefix: "--app-code-"}), transformers: shikiTransformers})
|
|
51
50
|
.use(rehypeHead)
|
|
52
51
|
.use(rehypeHeadingIds)
|
|
53
52
|
.use(rehypeSections)
|
|
@@ -73,25 +73,24 @@ 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
|
-
|
|
91
|
-
);
|
|
92
|
-
assert.
|
|
93
|
-
assert.equal(doc.html.split(
|
|
94
|
-
assert.equal(doc.html.split("</figure>").length - 1, 2);
|
|
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");
|
|
95
94
|
} finally {
|
|
96
95
|
await rm(root, {recursive: true, force: true});
|
|
97
96
|
}
|
package/src/style/token.css
CHANGED
|
@@ -35,11 +35,13 @@
|
|
|
35
35
|
--app-color-accent: light-dark(#555, #b5b5b5);
|
|
36
36
|
--app-color-accent-soft: light-dark(#f5f5f2, #222);
|
|
37
37
|
|
|
38
|
-
/*
|
|
38
|
+
/* 코드 줄 표기 · error·remove 는 bad, add 는 good, warning 은 warn · soft 가 줄 바탕 */
|
|
39
39
|
--app-color-bad: light-dark(#b2412e, #ff9b8a);
|
|
40
|
-
--app-color-bad-soft: light-dark(#
|
|
40
|
+
--app-color-bad-soft: light-dark(#fbe9e6, #42241f);
|
|
41
41
|
--app-color-good: light-dark(#2f7a47, #8bd3a3);
|
|
42
|
-
--app-color-good-soft: light-dark(#
|
|
42
|
+
--app-color-good-soft: light-dark(#e3f3e8, #1f3a27);
|
|
43
|
+
--app-color-warn: light-dark(#8a6400, #e6c46a);
|
|
44
|
+
--app-color-warn-soft: light-dark(#fbf3d9, #3d3418);
|
|
43
45
|
|
|
44
46
|
/* 링크 · 기본·방문·hover */
|
|
45
47
|
--app-color-link: light-dark(#11a, #9db8ff);
|
|
@@ -97,7 +99,7 @@
|
|
|
97
99
|
--app-space-block: 14px;
|
|
98
100
|
--app-space-group: 20px;
|
|
99
101
|
--app-space-subsection: 28px;
|
|
100
|
-
--app-space-section:
|
|
102
|
+
--app-space-section: 64px;
|
|
101
103
|
|
|
102
104
|
/* 탐색 트리 · 항목 위아래 여백·묶음 사이·묶음 라벨 아래·중첩 들여쓰기 */
|
|
103
105
|
--app-space-tree-row: 0px;
|
|
@@ -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
|
-
};
|