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 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.9.1
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.9.1` 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).
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.9.1` runs checks, publishes the package to npm, then creates a GitHub Release with the same package file.
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-soft);
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-soft);
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-soft);
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
- & .wg_prose__codeLabel {
172
- margin: var(--app-space-block) 0;
173
- border-left: 3px solid var(--app-color-bad);
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
- & .wg_prose__codeLabel--good {
176
- border-left-color: var(--app-color-good);
176
+ & pre code .line.highlighted {
177
+ background: var(--app-color-accent-soft);
177
178
  }
178
- & .wg_prose__codeLabelTitle {
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
- & .wg_prose__codeLabel--good .wg_prose__codeLabelTitle {
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
- & .wg_prose__codeLabel pre {
190
- margin: 0;
191
- border-left: 0;
192
- border-radius: 0 var(--app-radius-box) var(--app-radius-box) 0;
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-soft);
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(#fbeeec, #3a2522);
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(#eaf5ee, #1f3327);
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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 visit4 } from "unist-util-visit";
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
- visit4(tree, "code", (node, index, parent) => {
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 visit5 } from "unist-util-visit";
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
- visit5(tree, (node) => {
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 escapeHtml2, escapeRegExp } from "es-toolkit/string";
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}">${escapeHtml2(text)}</span>`
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 visit6 } from "unist-util-visit";
1654
+ import { visit as visit5 } from "unist-util-visit";
1680
1655
  var remarkSwatch = () => (tree) => {
1681
- visit6(tree, "inlineCode", (node) => {
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 visit7 } from "unist-util-visit";
1666
+ import { SKIP as SKIP2, visit as visit6 } from "unist-util-visit";
1692
1667
  var remarkUnknownDirectives = () => (tree, file) => {
1693
- visit7(tree, (node, index, parent) => {
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 visit8 } from "unist-util-visit";
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
- visit8(tree, "element", (node) => {
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 visit9 } from "unist-util-visit";
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
- visit9(tree, "element", (node) => {
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(remarkCodeLabels).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-" }) }).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
+ 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.9.1",
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-soft);
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-soft);
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-soft);
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
- /* 라벨 코드 · 왼쪽 띠와 제목 · 코드 상자의 여백은 figure 가 맡음 · 토큰은 token.css 의 bad·good */
219
- & .wg_prose__codeLabel {
220
- margin: var(--app-space-block) 0;
221
- border-left: 3px solid var(--app-color-bad);
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
- & .wg_prose__codeLabel--good {
225
- border-left-color: var(--app-color-good);
227
+ & pre code .line.highlighted {
228
+ background: var(--app-color-accent-soft);
226
229
  }
227
230
 
228
- & .wg_prose__codeLabelTitle {
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
- & .wg_prose__codeLabel--good .wg_prose__codeLabelTitle {
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
- & .wg_prose__codeLabel pre {
242
- margin: 0;
243
- border-left: 0;
244
- border-radius: 0 var(--app-radius-box) var(--app-radius-box) 0;
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-soft);
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(부품·흐름도·코드 라벨) → 코드 강조 → 머리 → 제목 id → 가름 수집 → 표
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 fences labelled bad or good render a captioned figure and keep highlighting", async () => {
77
- const root = await mkdtemp(join(tmpdir(), "for-humanity-code-labels-"));
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 bad 리터럴 <폴백>\nconst a = 1;\n```\n\n```ts good\nconst b = 2;\n```\n\n```ts other meta\nconst c = 3;\n```\n",
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
- doc.html,
90
- /<figure class="wg_prose__codeLabel wg_prose__codeLabel--bad"><figcaption class="wg_prose__codeLabelTitle">리터럴 &#x3C;폴백><\/figcaption>\s*<pre class="shiki/,
91
- );
92
- assert.match(doc.html, /<figure class="wg_prose__codeLabel wg_prose__codeLabel--good">\s*<pre class="shiki/);
93
- assert.equal(doc.html.split("<figure").length - 1, 2, "other meta stays a plain code block");
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
  }
@@ -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
- /* 라벨 코드 · bad·good 띠와 제목 · soft 제목 바탕 */
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(#fbeeec, #3a2522);
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(#eaf5ee, #1f3327);
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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
- };