for-humanity 0.9.1 → 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 +6 -0
- package/README.md +3 -3
- package/dist/cli.css +41 -29
- package/dist/cli.js +41 -40
- package/package.json +2 -1
- package/src/component/widget/prose/wg-prose.css +47 -23
- 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 +5 -12
- package/src/component/widget/prose/_function/remark-code-labels.ts +0 -47
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
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
|
+
|
|
5
11
|
## 0.9.1 — 2026-10-13
|
|
6
12
|
|
|
7
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.
|
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
|
@@ -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,45 @@
|
|
|
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 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;
|
|
193
209
|
}
|
|
194
210
|
}
|
|
195
211
|
.wg_prose__head {
|
|
@@ -272,7 +288,7 @@
|
|
|
272
288
|
overflow-x: auto;
|
|
273
289
|
margin: var(--app-space-block) 0;
|
|
274
290
|
padding: var(--app-space-block);
|
|
275
|
-
border: 1px solid var(--app-color-border
|
|
291
|
+
border: 1px solid var(--app-color-border);
|
|
276
292
|
border-radius: var(--app-radius-box);
|
|
277
293
|
letter-spacing: 0;
|
|
278
294
|
& svg {
|
|
@@ -763,9 +779,11 @@
|
|
|
763
779
|
--app-color-accent: light-dark(#555, #b5b5b5);
|
|
764
780
|
--app-color-accent-soft: light-dark(#f5f5f2, #222);
|
|
765
781
|
--app-color-bad: light-dark(#b2412e, #ff9b8a);
|
|
766
|
-
--app-color-bad-soft: light-dark(#
|
|
782
|
+
--app-color-bad-soft: light-dark(#fbe9e6, #42241f);
|
|
767
783
|
--app-color-good: light-dark(#2f7a47, #8bd3a3);
|
|
768
|
-
--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);
|
|
769
787
|
--app-color-link: light-dark(#11a, #9db8ff);
|
|
770
788
|
--app-color-link-visited: light-dark(#51a, #c5a5ed);
|
|
771
789
|
--app-color-link-hover: light-dark(#44d, #c1d1ff);
|
|
@@ -857,12 +875,6 @@
|
|
|
857
875
|
--app-space-gutter: 16px;
|
|
858
876
|
}
|
|
859
877
|
}
|
|
860
|
-
.wg_prose__codeLabel--bad {
|
|
861
|
-
--app-code-background: var(--app-color-bad-soft);
|
|
862
|
-
}
|
|
863
|
-
.wg_prose__codeLabel--good {
|
|
864
|
-
--app-code-background: var(--app-color-good-soft);
|
|
865
|
-
}
|
|
866
878
|
|
|
867
879
|
/* src/component/widget/shell/wg-shell.css */
|
|
868
880
|
.wg_shell__root {
|
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",
|
|
@@ -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,59 @@
|
|
|
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
|
+
& 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;
|
|
245
268
|
}
|
|
269
|
+
/* stylelint-enable selector-class-pattern */
|
|
246
270
|
/* stylelint-enable selector-disallowed-list */
|
|
247
271
|
}
|
|
248
272
|
|
|
@@ -354,7 +378,7 @@
|
|
|
354
378
|
overflow-x: auto;
|
|
355
379
|
margin: var(--app-space-block) 0;
|
|
356
380
|
padding: var(--app-space-block);
|
|
357
|
-
border: 1px solid var(--app-color-border
|
|
381
|
+
border: 1px solid var(--app-color-border);
|
|
358
382
|
border-radius: var(--app-radius-box);
|
|
359
383
|
|
|
360
384
|
/* 라벨 위치는 자간 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);
|
|
@@ -171,12 +173,3 @@
|
|
|
171
173
|
--app-space-gutter: 16px;
|
|
172
174
|
}
|
|
173
175
|
}
|
|
174
|
-
|
|
175
|
-
/* 라벨 코드 · Shiki 가 pre 에 인라인으로 쓰는 코드 바탕을 figure 색으로 바꿈 */
|
|
176
|
-
.wg_prose__codeLabel--bad {
|
|
177
|
-
--app-code-background: var(--app-color-bad-soft);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
.wg_prose__codeLabel--good {
|
|
181
|
-
--app-code-background: var(--app-color-good-soft);
|
|
182
|
-
}
|
|
@@ -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
|
-
};
|