for-humanity 0.10.0 → 0.12.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,15 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.12.0 — 2026-10-13
6
+
7
+ - Add a `[file name]` fence meta that draws a title bar with the file name and language above the code block, sharing the `bad`/`good` tone.
8
+
9
+ ## 0.11.0 — 2026-10-13
10
+
11
+ - Add `<<< ./file.ext#region meta` code snippet imports: a file inside the document root becomes a code block, with optional `#region` extraction, dedent, fence meta passthrough and dev reloads when the file changes.
12
+ - Add `bad` and `good` fence meta that tint the whole code block, with line notation drawn one step darker on top.
13
+
5
14
  ## 0.10.0 — 2026-10-13
6
15
 
7
16
  - 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.
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.10.0
36
+ pnpm add -D --save-exact for-humanity@0.12.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.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).
55
+ Version `0.12.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.10.0` 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.12.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
@@ -189,11 +189,45 @@
189
189
  background: var(--app-color-bad-soft);
190
190
  opacity: 0.8;
191
191
  }
192
+ & pre.wg_prose__code--bad code .line.highlighted,
193
+ & pre.wg_prose__code--bad code .line.highlighted.error,
194
+ & pre.wg_prose__code--bad code .line.diff.remove {
195
+ background: var(--app-color-bad-mark);
196
+ }
197
+ & pre.wg_prose__code--good code .line.highlighted,
198
+ & pre.wg_prose__code--good code .line.diff.add {
199
+ background: var(--app-color-good-mark);
200
+ }
192
201
  & pre code .highlighted-word {
193
202
  padding: 0 2px;
194
203
  border-radius: var(--app-radius-inline);
195
204
  background: var(--app-color-accent-soft);
196
205
  }
206
+ & .wg_prose__codeFile {
207
+ margin: var(--app-space-block) 0;
208
+ }
209
+ & .wg_prose__codeFile pre {
210
+ margin: 0;
211
+ border-top-left-radius: 0;
212
+ border-top-right-radius: 0;
213
+ }
214
+ & .wg_prose__codeFileTitle {
215
+ display: flex;
216
+ gap: var(--app-space-block);
217
+ justify-content: space-between;
218
+ padding: var(--app-space-inline) var(--app-space-block);
219
+ border: 1px solid var(--app-color-border);
220
+ border-bottom: 0;
221
+ border-radius: var(--app-radius-box) var(--app-radius-box) 0 0;
222
+ background: var(--app-code-background);
223
+ color: var(--app-color-text-muted);
224
+ font-family: var(--app-font-mono);
225
+ font-size: var(--app-font-size-dense);
226
+ letter-spacing: 0;
227
+ }
228
+ & .wg_prose__codeFileLang {
229
+ text-transform: uppercase;
230
+ }
197
231
  & pre.wg_prose__lineNumbers code {
198
232
  counter-reset: line;
199
233
  }
@@ -779,9 +813,11 @@
779
813
  --app-color-accent: light-dark(#555, #b5b5b5);
780
814
  --app-color-accent-soft: light-dark(#f5f5f2, #222);
781
815
  --app-color-bad: light-dark(#b2412e, #ff9b8a);
782
- --app-color-bad-soft: light-dark(#fbe9e6, #42241f);
816
+ --app-color-bad-soft: light-dark(#f9ebe8, #3a2522);
817
+ --app-color-bad-mark: light-dark(#f1cdc6, #5b322c);
783
818
  --app-color-good: light-dark(#2f7a47, #8bd3a3);
784
- --app-color-good-soft: light-dark(#e3f3e8, #1f3a27);
819
+ --app-color-good-soft: light-dark(#e4f3ed, #1c342c);
820
+ --app-color-good-mark: light-dark(#c4e4d4, #27503f);
785
821
  --app-color-warn: light-dark(#8a6400, #e6c46a);
786
822
  --app-color-warn-soft: light-dark(#fbf3d9, #3d3418);
787
823
  --app-color-link: light-dark(#11a, #9db8ff);
@@ -875,6 +911,14 @@
875
911
  --app-space-gutter: 16px;
876
912
  }
877
913
  }
914
+ .wg_prose__code--bad,
915
+ .wg_prose__codeFile--bad {
916
+ --app-code-background: var(--app-color-bad-soft);
917
+ }
918
+ .wg_prose__code--good,
919
+ .wg_prose__codeFile--good {
920
+ --app-code-background: var(--app-color-good-soft);
921
+ }
878
922
 
879
923
  /* src/component/widget/shell/wg-shell.css */
880
924
  .wg_shell__root {
package/dist/cli.js CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  // src/entry/cli/cli.ts
4
4
  import { EventEmitter } from "node:events";
5
- import { existsSync as existsSync3, readFileSync as readFileSync3, watch } from "node:fs";
5
+ import { existsSync as existsSync4, readFileSync as readFileSync4, watch } from "node:fs";
6
6
  import { copyFile, mkdir, rm, writeFile } from "node:fs/promises";
7
- import { dirname as dirname4, extname as extname4, join as join4, relative as relative4, resolve as resolve3, sep as sep3 } from "node:path";
7
+ import { dirname as dirname5, extname as extname5, join as join4, relative as relative5, resolve as resolve4, sep as sep4 } from "node:path";
8
8
  import { fileURLToPath, pathToFileURL } from "node:url";
9
9
  import { serve } from "@hono/node-server";
10
10
  import { serveStatic } from "@hono/node-server/serve-static";
@@ -64,6 +64,7 @@ var copy_error_directive_syntax = "\uBD80\uD488 \uBB38\uBC95\uC774 \uC62C\uBC14\
64
64
  var copy_warn_broken_link = "\uC5C6\uB294 \uBB38\uC11C\uB85C \uAC74 \uB9C1\uD06C\uB2E4";
65
65
  var copy_warn_unknown_status = "\uC124\uC815\uC5D0 \uC5C6\uB294 \uC0C1\uD0DC \uBB38\uAD6C\uB2E4";
66
66
  var copy_error_asset = "\uC790\uC6D0\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uB2E4";
67
+ var copy_error_snippet = "\uCF54\uB4DC \uC2A4\uB2C8\uD3AB\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uB2E4";
67
68
 
68
69
  // src/constant/font.ts
69
70
  var font_mono_css = "src/asset/font/monoplex-kr/monoplex-kr.css";
@@ -1763,6 +1764,91 @@ var remarkReport = (options) => (_tree, file) => {
1763
1764
  }
1764
1765
  };
1765
1766
 
1767
+ // src/entry/cli/_function/create-processor/_remark-snippets.ts
1768
+ import { existsSync as existsSync3, readFileSync as readFileSync2, realpathSync as realpathSync2, statSync as statSync2 } from "node:fs";
1769
+ import { dirname as dirname3, extname as extname3, isAbsolute as isAbsolute2, relative as relative4, resolve as resolve3, sep as sep2 } from "node:path";
1770
+ import { visit as visit9 } from "unist-util-visit";
1771
+
1772
+ // src/entry/cli/_constant/snippet.ts
1773
+ var snippet_pattern = /^<<<\s+(\S+?)(?:#([\w-]+))?(?:\s+(.+?))?\s*$/;
1774
+ var snippet_region_pattern = /^\s*\S*\s*#(end)?region\s+([\w-]+)/;
1775
+ var snippet_language_by_extension = {
1776
+ ".cjs": "js",
1777
+ ".md": "markdown",
1778
+ ".mjs": "js",
1779
+ ".mts": "ts",
1780
+ ".txt": "text",
1781
+ ".yml": "yaml"
1782
+ };
1783
+
1784
+ // src/entry/cli/_function/create-processor/_remark-snippets.ts
1785
+ var toSnippetBody = (source, region) => {
1786
+ const lines = source.replace(/\r\n/g, "\n").split("\n");
1787
+ let inside = region === void 0;
1788
+ let found = region === void 0;
1789
+ const kept = [];
1790
+ for (const line of lines) {
1791
+ const marker = snippet_region_pattern.exec(line);
1792
+ if (marker !== null) {
1793
+ if (region !== void 0 && marker[2] === region) {
1794
+ inside = marker[1] === void 0;
1795
+ found = true;
1796
+ }
1797
+ continue;
1798
+ }
1799
+ if (inside) {
1800
+ kept.push(line);
1801
+ }
1802
+ }
1803
+ if (!found) {
1804
+ return void 0;
1805
+ }
1806
+ while (kept.length > 0 && kept[0]?.trim() === "") {
1807
+ kept.shift();
1808
+ }
1809
+ while (kept.length > 0 && kept.at(-1)?.trim() === "") {
1810
+ kept.pop();
1811
+ }
1812
+ const indents = kept.filter((line) => line.trim() !== "").map((line) => line.length - line.trimStart().length);
1813
+ const indent = indents.length === 0 ? 0 : Math.min(...indents);
1814
+ return kept.map((line) => line.slice(Math.min(indent, line.length - line.trimStart().length))).join("\n");
1815
+ };
1816
+ var remarkSnippets = (options) => (tree, file) => {
1817
+ visit9(tree, "paragraph", (node, index, parent) => {
1818
+ const [child] = node.children;
1819
+ if (node.children.length !== 1 || child === void 0 || child.type !== "text" || parent === void 0 || index === void 0) {
1820
+ return;
1821
+ }
1822
+ const match = snippet_pattern.exec(child.value.trim());
1823
+ if (match === null) {
1824
+ return;
1825
+ }
1826
+ const [, path = "", region, meta] = match;
1827
+ const target = resolve3(dirname3(file.path), path);
1828
+ const location2 = relative4(options.root, target);
1829
+ if (location2 === ".." || location2.startsWith(`..${sep2}`) || isAbsolute2(location2)) {
1830
+ file.fail(`${copy_error_snippet}: \uBB38\uC11C \uD3F4\uB354 \uBC16 \xB7 ${path}`, node);
1831
+ }
1832
+ if (!existsSync3(target)) {
1833
+ file.fail(`${copy_error_snippet}: \uD30C\uC77C\uC774 \uC5C6\uB2E4 \xB7 ${path}`, node);
1834
+ }
1835
+ const actual = realpathSync2(target);
1836
+ const actualLocation = relative4(realpathSync2(options.root), actual);
1837
+ if (actualLocation === ".." || actualLocation.startsWith(`..${sep2}`) || isAbsolute2(actualLocation) || !statSync2(actual).isFile()) {
1838
+ file.fail(`${copy_error_snippet}: \uBB38\uC11C \uD3F4\uB354 \uBC16\uC774\uAC70\uB098 \uC77C\uBC18 \uD30C\uC77C\uC774 \uC544\uB2C8\uB2E4 \xB7 ${path}`, node);
1839
+ }
1840
+ const body = toSnippetBody(readFileSync2(actual, "utf8"), region);
1841
+ if (body === void 0) {
1842
+ file.fail(`${copy_error_snippet}: region \uC774 \uC5C6\uB2E4 \xB7 ${path}#${region}`, node);
1843
+ return;
1844
+ }
1845
+ const extension = extname3(actual).toLowerCase();
1846
+ const lang = snippet_language_by_extension[extension] === void 0 ? extension.slice(1) : snippet_language_by_extension[extension];
1847
+ options.snippets.add(actual);
1848
+ parent.children.splice(index, 1, { type: "code", lang, meta: meta === void 0 ? null : meta, value: body });
1849
+ });
1850
+ };
1851
+
1766
1852
  // src/entry/cli/_function/create-processor/_shiki-transformers.ts
1767
1853
  import {
1768
1854
  transformerMetaHighlight,
@@ -1780,23 +1866,66 @@ var lineNumbers = {
1780
1866
  }
1781
1867
  }
1782
1868
  };
1869
+ var tone = {
1870
+ name: "for-humanity:code-tone",
1871
+ pre(node) {
1872
+ const meta = this.options.meta?.__raw;
1873
+ if (typeof meta !== "string") {
1874
+ return;
1875
+ }
1876
+ for (const word of ["bad", "good"]) {
1877
+ if (new RegExp(`\\b${word}\\b`).test(meta)) {
1878
+ this.addClassToHast(node, `wg_prose__code--${word}`);
1879
+ }
1880
+ }
1881
+ }
1882
+ };
1883
+ var fileTitle = {
1884
+ name: "for-humanity:code-title",
1885
+ root(root) {
1886
+ const meta = this.options.meta?.__raw;
1887
+ const title = typeof meta === "string" ? /\[([^\]]+)\]/.exec(meta)?.[1] : void 0;
1888
+ if (title === void 0) {
1889
+ return;
1890
+ }
1891
+ const tones = typeof meta === "string" ? ["bad", "good"].filter((word) => new RegExp(`\\b${word}\\b`).test(meta)) : [];
1892
+ const caption = {
1893
+ type: "element",
1894
+ tagName: "figcaption",
1895
+ properties: { className: ["wg_prose__codeFileTitle"] },
1896
+ children: [
1897
+ { type: "text", value: title },
1898
+ { type: "element", tagName: "span", properties: { className: ["wg_prose__codeFileLang"] }, children: [{ type: "text", value: this.options.lang }] }
1899
+ ]
1900
+ };
1901
+ const figure = {
1902
+ type: "element",
1903
+ tagName: "figure",
1904
+ properties: { className: ["wg_prose__codeFile", ...tones.map((tone2) => `wg_prose__codeFile--${tone2}`)] },
1905
+ children: [caption, ...root.children.filter((child) => child.type !== "doctype")]
1906
+ };
1907
+ root.children = [figure];
1908
+ }
1909
+ };
1783
1910
  var shikiTransformers = [
1784
1911
  transformerNotationHighlight({ matchAlgorithm: "v3" }),
1785
1912
  transformerNotationErrorLevel({ matchAlgorithm: "v3" }),
1786
1913
  transformerNotationDiff({ matchAlgorithm: "v3" }),
1787
1914
  transformerNotationWordHighlight({ matchAlgorithm: "v3" }),
1788
1915
  transformerMetaHighlight(),
1789
- lineNumbers
1916
+ lineNumbers,
1917
+ tone,
1918
+ fileTitle
1790
1919
  ];
1791
1920
 
1792
1921
  // src/entry/cli/_function/create-processor/create-processor.ts
1793
1922
  var createProcessor = (options) => {
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 });
1923
+ return unified().use(remarkParse).use(remarkFrontmatter, ["yaml"]).use(remarkGfm).use(remarkSnippets, { root: options.root, snippets: options.snippets ?? /* @__PURE__ */ new Set() }).use(remarkSmartypants).use(remarkDirective).use(remarkBlocks).use(remarkParts).use(remarkFlow).use(remarkStatus, { status: options.site.status }).use(remarkSwatch).use(remarkLinks, { root: options.root }).use(remarkUnknownDirectives).use(remarkReport, { root: options.root }).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeShiki, { theme: createCssVariablesTheme({ name: "for-humanity", variablePrefix: "--app-code-" }), transformers: shikiTransformers }).use(rehypeHead).use(rehypeHeadingIds).use(rehypeSections).use(rehypeTables).use(rehypeRaw).use(rehypeAssets, { root: options.root, files: options.files ?? /* @__PURE__ */ new Map() }).use(rehypeStringify, { allowDangerousHtml: true });
1795
1924
  };
1796
1925
 
1797
1926
  // src/entry/cli/_function/read-docs.ts
1798
1927
  import { readdir, readFile as readFile2 } from "node:fs/promises";
1799
- import { join, posix as posix3, sep as sep2 } from "node:path";
1928
+ import { join, posix as posix3, sep as sep3 } from "node:path";
1800
1929
  import { VFile } from "vfile";
1801
1930
  import { parse as parseYaml } from "yaml";
1802
1931
 
@@ -1845,11 +1974,11 @@ var docDataSchema = z2.object({
1845
1974
  // src/entry/cli/_function/read-docs.ts
1846
1975
  var readDocs = async (options) => {
1847
1976
  const paths = (await readdir(options.root, { recursive: true })).filter(
1848
- (path) => /\.mdx?$/i.test(path) && !/^(readme|agents|claude)\.md$/i.test(path) && !path.split(sep2).includes("node_modules") && !path.startsWith(`dist${sep2}`)
1977
+ (path) => /\.mdx?$/i.test(path) && !/^(readme|agents|claude)\.md$/i.test(path) && !path.split(sep3).includes("node_modules") && !path.startsWith(`dist${sep3}`)
1849
1978
  );
1850
1979
  const docs = await Promise.all(
1851
1980
  paths.map(async (path) => {
1852
- const id = path.replace(/\.mdx?$/i, "").split(sep2).join("/").toLowerCase();
1981
+ const id = path.replace(/\.mdx?$/i, "").split(sep3).join("/").toLowerCase();
1853
1982
  const segments = id.split("/");
1854
1983
  if (segments.some((segment) => segment === "" || segment === "." || segment === ".." || segment === "index.html") || /[#?%*:\\]/.test(id) || /[\p{Cc}]/u.test(id) || options.reserved?.includes(segments[0]) === true || segments[0] === asset_dir || segments[0] === asset_favicon_path.slice(1)) {
1855
1984
  throw new Error(`${copy_error_doc_id}: ${path}`);
@@ -2043,15 +2172,15 @@ var toErrorMessage = (error) => {
2043
2172
 
2044
2173
  // src/util/font/to-font-css.ts
2045
2174
  import { createHash as createHash2 } from "node:crypto";
2046
- import { readFileSync as readFileSync2 } from "node:fs";
2047
- import { basename, dirname as dirname3, extname as extname3, join as join3 } from "node:path";
2175
+ import { readFileSync as readFileSync3 } from "node:fs";
2176
+ import { basename, dirname as dirname4, extname as extname4, join as join3 } from "node:path";
2048
2177
  var toFontCss = (options) => {
2049
2178
  const files = /* @__PURE__ */ new Map();
2050
- const css = [...readFileSync2(options.css, "utf8").matchAll(/@font-face\s*\{[^}]*\}/g)].map(
2179
+ const css = [...readFileSync3(options.css, "utf8").matchAll(/@font-face\s*\{[^}]*\}/g)].map(
2051
2180
  (match) => match[0].replace(/font-display:\s*[^;]+;/g, `font-display: ${font_display_strategy};`).replace(/url\(([^)]+)\)/g, (_match, relativeUrl) => {
2052
- const file = join3(dirname3(options.css), relativeUrl);
2053
- const extension = extname3(file);
2054
- const hash = createHash2("sha256").update(readFileSync2(file)).digest("hex").slice(0, font_hash_length);
2181
+ const file = join3(dirname4(options.css), relativeUrl);
2182
+ const extension = extname4(file);
2183
+ const hash = createHash2("sha256").update(readFileSync3(file)).digest("hex").slice(0, font_hash_length);
2055
2184
  const url = `${options.fontDir}/${basename(file, extension)}.${hash}${extension}`;
2056
2185
  files.set(url, file);
2057
2186
  return `url(${url})`;
@@ -2068,10 +2197,10 @@ var main = async () => {
2068
2197
  throw new Error(`${copy_error_unknown_command}: ${command} (${commands.join(", ")})`);
2069
2198
  }
2070
2199
  const kitRoot = fileURLToPath(new URL("../", import.meta.url));
2071
- const docsRoot = resolve3(docsDir);
2200
+ const docsRoot = resolve4(docsDir);
2072
2201
  const outDir = join4(docsRoot, "dist");
2073
2202
  const configFile = join4(docsRoot, cli_config_file_name);
2074
- const parsedConfig = siteConfigSchema.safeParse(existsSync3(configFile) ? (await import(pathToFileURL(configFile).href)).default : site_config_absent);
2203
+ const parsedConfig = siteConfigSchema.safeParse(existsSync4(configFile) ? (await import(pathToFileURL(configFile).href)).default : site_config_absent);
2075
2204
  if (!parsedConfig.success) {
2076
2205
  throw new Error(`${copy_error_config}: ${parsedConfig.error.issues.map((issue) => `${issue.path.join(".")} ${issue.message}`).join(", ")}`);
2077
2206
  }
@@ -2081,7 +2210,7 @@ var main = async () => {
2081
2210
  app2.use(
2082
2211
  "/*",
2083
2212
  serveStatic({
2084
- root: relative4(process.cwd(), outDir),
2213
+ root: relative5(process.cwd(), outDir),
2085
2214
  onFound: (_path, c) => {
2086
2215
  if (c.req.path.startsWith(`${asset_font_dir}/`)) {
2087
2216
  c.header("Cache-Control", font_cache_control);
@@ -2097,7 +2226,7 @@ var main = async () => {
2097
2226
  const brand = toFontCss({ css: join4(kitRoot, font_brand_css), fontDir: asset_font_dir });
2098
2227
  const fontCss = [mono.css, body.css, brand.css].join("\n");
2099
2228
  const stylePath = toAssetPath({ path: asset_style_path, file: join4(kitRoot, "dist/cli.css") });
2100
- const browserScript = readFileSync3(join4(kitRoot, "dist/browser.js"), "utf8");
2229
+ const browserScript = readFileSync4(join4(kitRoot, "dist/browser.js"), "utf8");
2101
2230
  const kitFiles = new Map([
2102
2231
  [stylePath, join4(kitRoot, "dist/cli.css")],
2103
2232
  [asset_favicon_path, join4(kitRoot, "src/asset/favicon.svg")],
@@ -2106,7 +2235,8 @@ var main = async () => {
2106
2235
  ...brand.files
2107
2236
  ]);
2108
2237
  const files = new Map(kitFiles);
2109
- const processor = createProcessor({ site: siteConfig, root: docsRoot, files });
2238
+ const snippets = /* @__PURE__ */ new Set();
2239
+ const processor = createProcessor({ site: siteConfig, root: docsRoot, files, snippets });
2110
2240
  const [docs, home] = await Promise.all([
2111
2241
  readDocs({ root: docsRoot, processor, reserved: siteConfig.url === void 0 ? void 0 : crawling_reserved_roots }),
2112
2242
  readHome({ root: docsRoot, processor })
@@ -2129,11 +2259,11 @@ var main = async () => {
2129
2259
  throw crawlerResult.error;
2130
2260
  }
2131
2261
  for (const [url, file] of files) {
2132
- await mkdir(dirname4(join4(outDir, url)), { recursive: true });
2262
+ await mkdir(dirname5(join4(outDir, url)), { recursive: true });
2133
2263
  await copyFile(file, join4(outDir, url));
2134
2264
  }
2135
2265
  console.log(
2136
- `${crawlerResult === void 0 ? result.files.length : result.files.length + crawlerResult.files.length} files, ${files.size} assets \u2192 ${relative4(process.cwd(), outDir)}`
2266
+ `${crawlerResult === void 0 ? result.files.length : result.files.length + crawlerResult.files.length} files, ${files.size} assets \u2192 ${relative5(process.cwd(), outDir)}`
2137
2267
  );
2138
2268
  return;
2139
2269
  }
@@ -2145,7 +2275,8 @@ var main = async () => {
2145
2275
  run: async () => {
2146
2276
  const currentGeneration = generation;
2147
2277
  const nextFiles = new Map(kitFiles);
2148
- const nextProcessor = createProcessor({ site: siteConfig, root: docsRoot, files: nextFiles });
2278
+ const nextSnippets = /* @__PURE__ */ new Set();
2279
+ const nextProcessor = createProcessor({ site: siteConfig, root: docsRoot, files: nextFiles, snippets: nextSnippets });
2149
2280
  const [docs2, home2] = await Promise.all([
2150
2281
  readDocs({ root: docsRoot, processor: nextProcessor, reserved: siteConfig.url === void 0 ? void 0 : crawling_reserved_roots }),
2151
2282
  readHome({ root: docsRoot, processor: nextProcessor })
@@ -2157,6 +2288,10 @@ var main = async () => {
2157
2288
  for (const entry of nextFiles) {
2158
2289
  files.set(...entry);
2159
2290
  }
2291
+ snippets.clear();
2292
+ for (const entry of nextSnippets) {
2293
+ snippets.add(entry);
2294
+ }
2160
2295
  store.docs = docs2;
2161
2296
  store.home = home2;
2162
2297
  reload.emit("change");
@@ -2167,8 +2302,9 @@ var main = async () => {
2167
2302
  );
2168
2303
  const handleDocsChange = (_event, filename) => {
2169
2304
  if (filename !== null) {
2170
- const segments = filename.split(sep3);
2171
- if (segments.includes("dist") || segments.includes("node_modules") || !/\.mdx?$/i.test(filename) && !asset_media_extensions.has(extname4(filename).toLowerCase())) {
2305
+ const segments = filename.split(sep4);
2306
+ const isSnippet = snippets.has(join4(docsRoot, filename));
2307
+ if (segments.includes("dist") || segments.includes("node_modules") || !isSnippet && !/\.mdx?$/i.test(filename) && !asset_media_extensions.has(extname5(filename).toLowerCase())) {
2172
2308
  return;
2173
2309
  }
2174
2310
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "for-humanity",
3
- "version": "0.10.0",
3
+ "version": "0.12.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",
@@ -245,12 +245,54 @@
245
245
  opacity: 0.8;
246
246
  }
247
247
 
248
+ /* 색 블록 위의 줄 표기 · 바탕보다 한 단계 진하게 */
249
+ & pre.wg_prose__code--bad code .line.highlighted,
250
+ & pre.wg_prose__code--bad code .line.highlighted.error,
251
+ & pre.wg_prose__code--bad code .line.diff.remove {
252
+ background: var(--app-color-bad-mark);
253
+ }
254
+
255
+ & pre.wg_prose__code--good code .line.highlighted,
256
+ & pre.wg_prose__code--good code .line.diff.add {
257
+ background: var(--app-color-good-mark);
258
+ }
259
+
248
260
  & pre code .highlighted-word {
249
261
  padding: 0 2px;
250
262
  border-radius: var(--app-radius-inline);
251
263
  background: var(--app-color-accent-soft);
252
264
  }
253
265
 
266
+ /* 파일 이름 띠 · 펜스 메타 [파일 이름] · 코드 상자와 한 몸, 여백은 figure 가 맡음 */
267
+ & .wg_prose__codeFile {
268
+ margin: var(--app-space-block) 0;
269
+ }
270
+
271
+ & .wg_prose__codeFile pre {
272
+ margin: 0;
273
+ border-top-left-radius: 0;
274
+ border-top-right-radius: 0;
275
+ }
276
+
277
+ & .wg_prose__codeFileTitle {
278
+ display: flex;
279
+ gap: var(--app-space-block);
280
+ justify-content: space-between;
281
+ padding: var(--app-space-inline) var(--app-space-block);
282
+ border: 1px solid var(--app-color-border);
283
+ border-bottom: 0;
284
+ border-radius: var(--app-radius-box) var(--app-radius-box) 0 0;
285
+ background: var(--app-code-background);
286
+ color: var(--app-color-text-muted);
287
+ font-family: var(--app-font-mono);
288
+ font-size: var(--app-font-size-dense);
289
+ letter-spacing: 0;
290
+ }
291
+
292
+ & .wg_prose__codeFileLang {
293
+ text-transform: uppercase;
294
+ }
295
+
254
296
  /* 줄 번호 · 펜스 메타 line-numbers · 복사에 섞이지 않게 content 로 */
255
297
  & pre.wg_prose__lineNumbers code {
256
298
  counter-reset: line;
@@ -126,3 +126,8 @@ export const copy_warn_unknown_status = "설정에 없는 상태 문구다";
126
126
  * 문서에서 참조한 로컬 자원의 경로·형식 오류
127
127
  */
128
128
  export const copy_error_asset = "자원이 올바르지 않다";
129
+
130
+ /**
131
+ * `<<<` 로 불러온 코드 스니펫의 경로·파일·region 오류
132
+ */
133
+ export const copy_error_snippet = "코드 스니펫이 올바르지 않다";
@@ -0,0 +1,21 @@
1
+ /**
2
+ * 스니펫 줄 · `<<< ./path.ext`, 선택 `#region`, 선택 메타(펜스 메타와 같음)
3
+ */
4
+ export const snippet_pattern = /^<<<\s+(\S+?)(?:#([\w-]+))?(?:\s+(.+?))?\s*$/;
5
+
6
+ /**
7
+ * region 표시 줄 · 주석 모양은 자유 · `#region 이름`, `#endregion 이름`
8
+ */
9
+ export const snippet_region_pattern = /^\s*\S*\s*#(end)?region\s+([\w-]+)/;
10
+
11
+ /**
12
+ * 확장자 → 펜스 언어 · 없으면 확장자 그대로
13
+ */
14
+ export const snippet_language_by_extension: Record<string, string> = {
15
+ ".cjs": "js",
16
+ ".md": "markdown",
17
+ ".mjs": "js",
18
+ ".mts": "ts",
19
+ ".txt": "text",
20
+ ".yml": "yaml",
21
+ };
@@ -0,0 +1,117 @@
1
+ import {existsSync, readFileSync, realpathSync, statSync} from "node:fs";
2
+ import {dirname, extname, isAbsolute, relative, resolve, sep} from "node:path";
3
+ import type {Root} from "mdast";
4
+ import {visit} from "unist-util-visit";
5
+ import type {VFile} from "vfile";
6
+ import {copy_error_snippet} from "@/constant/copy";
7
+ import {snippet_language_by_extension, snippet_pattern, snippet_region_pattern} from "@/entry/cli/_constant/snippet";
8
+
9
+ /**
10
+ * 스니펫 수집 계약
11
+ */
12
+ export interface SnippetOptions {
13
+ /**
14
+ * 스니펫을 읽을 수 있는 문서 루트
15
+ */
16
+ root: string;
17
+ /**
18
+ * dev 새로고침이 지켜볼 불러온 파일 경로
19
+ */
20
+ snippets: Set<string>;
21
+ }
22
+
23
+ /**
24
+ * region 이름이 있으면 그 안만, 없으면 전체 · region 표시 줄은 항상 제거 · 공통 들여쓰기 제거
25
+ */
26
+ const toSnippetBody = (source: string, region: string | undefined): string | undefined => {
27
+ const lines = source.replace(/\r\n/g, "\n").split("\n");
28
+ let inside = region === undefined;
29
+ let found = region === undefined;
30
+ const kept: string[] = [];
31
+
32
+ for (const line of lines) {
33
+ const marker = snippet_region_pattern.exec(line);
34
+
35
+ if (marker !== null) {
36
+ if (region !== undefined && marker[2] === region) {
37
+ inside = marker[1] === undefined;
38
+ found = true;
39
+ }
40
+
41
+ continue;
42
+ }
43
+
44
+ if (inside) {
45
+ kept.push(line);
46
+ }
47
+ }
48
+
49
+ if (!found) {
50
+ return undefined;
51
+ }
52
+
53
+ while (kept.length > 0 && kept[0]?.trim() === "") {
54
+ kept.shift();
55
+ }
56
+
57
+ while (kept.length > 0 && kept.at(-1)?.trim() === "") {
58
+ kept.pop();
59
+ }
60
+
61
+ const indents = kept.filter((line) => line.trim() !== "").map((line) => line.length - line.trimStart().length);
62
+ const indent = indents.length === 0 ? 0 : Math.min(...indents);
63
+
64
+ return kept.map((line) => line.slice(Math.min(indent, line.length - line.trimStart().length))).join("\n");
65
+ };
66
+
67
+ /**
68
+ * `<<< ./path.ext#region 메타` 문단 → 코드 펜스 · 파일 위치 기준 경로 · 문서 폴더 밖과 없는 파일은 빌드 실패
69
+ * 불러온 파일은 dev 감시 목록에 올림
70
+ */
71
+ export const remarkSnippets = (options: SnippetOptions) => (tree: Root, file: VFile) => {
72
+ visit(tree, "paragraph", (node, index, parent) => {
73
+ const [child] = node.children;
74
+
75
+ if (node.children.length !== 1 || child === undefined || child.type !== "text" || parent === undefined || index === undefined) {
76
+ return;
77
+ }
78
+
79
+ const match = snippet_pattern.exec(child.value.trim());
80
+
81
+ if (match === null) {
82
+ return;
83
+ }
84
+
85
+ const [, path = "", region, meta] = match;
86
+ const target = resolve(dirname(file.path), path);
87
+ const location = relative(options.root, target);
88
+
89
+ if (location === ".." || location.startsWith(`..${sep}`) || isAbsolute(location)) {
90
+ file.fail(`${copy_error_snippet}: 문서 폴더 밖 · ${path}`, node);
91
+ }
92
+
93
+ if (!existsSync(target)) {
94
+ file.fail(`${copy_error_snippet}: 파일이 없다 · ${path}`, node);
95
+ }
96
+
97
+ const actual = realpathSync(target);
98
+ const actualLocation = relative(realpathSync(options.root), actual);
99
+
100
+ if (actualLocation === ".." || actualLocation.startsWith(`..${sep}`) || isAbsolute(actualLocation) || !statSync(actual).isFile()) {
101
+ file.fail(`${copy_error_snippet}: 문서 폴더 밖이거나 일반 파일이 아니다 · ${path}`, node);
102
+ }
103
+
104
+ const body = toSnippetBody(readFileSync(actual, "utf8"), region);
105
+
106
+ if (body === undefined) {
107
+ file.fail(`${copy_error_snippet}: region 이 없다 · ${path}#${region}`, node);
108
+ return;
109
+ }
110
+
111
+ const extension = extname(actual).toLowerCase();
112
+ const lang = snippet_language_by_extension[extension] === undefined ? extension.slice(1) : snippet_language_by_extension[extension];
113
+
114
+ options.snippets.add(actual);
115
+ parent.children.splice(index, 1, {type: "code", lang, meta: meta === undefined ? null : meta, value: body});
116
+ });
117
+ };
@@ -5,6 +5,7 @@ import {
5
5
  transformerNotationHighlight,
6
6
  transformerNotationWordHighlight,
7
7
  } from "@shikijs/transformers";
8
+ import type {Element, ElementContent} from "hast";
8
9
  import type {ShikiTransformer} from "shiki";
9
10
 
10
11
  /**
@@ -22,7 +23,61 @@ const lineNumbers: ShikiTransformer = {
22
23
  };
23
24
 
24
25
  /**
25
- * 코드 줄 표기 · `// [!code highlight|error|warning|++|--]`, `// [!code word:x]`, 메타 `{2,4-6}`, `line-numbers`
26
+ * 펜스 메타 `bad`·`good` → pre 에 블록 색 클래스 · 바탕색은 token.css 가 바꿈
27
+ */
28
+ const tone: ShikiTransformer = {
29
+ name: "for-humanity:code-tone",
30
+ pre(node) {
31
+ const meta = this.options.meta?.__raw;
32
+
33
+ if (typeof meta !== "string") {
34
+ return;
35
+ }
36
+
37
+ for (const word of ["bad", "good"]) {
38
+ if (new RegExp(`\\b${word}\\b`).test(meta)) {
39
+ this.addClassToHast(node, `wg_prose__code--${word}`);
40
+ }
41
+ }
42
+ },
43
+ };
44
+
45
+ /**
46
+ * 펜스 메타 `[파일 이름]` → figure 로 감싸고 제목 띠에 파일 이름과 언어 · 블록 색 클래스는 figure 에도 둠
47
+ */
48
+ const fileTitle: ShikiTransformer = {
49
+ name: "for-humanity:code-title",
50
+ root(root) {
51
+ const meta = this.options.meta?.__raw;
52
+ const title = typeof meta === "string" ? /\[([^\]]+)\]/.exec(meta)?.[1] : undefined;
53
+
54
+ if (title === undefined) {
55
+ return;
56
+ }
57
+
58
+ const tones = typeof meta === "string" ? ["bad", "good"].filter((word) => new RegExp(`\\b${word}\\b`).test(meta)) : [];
59
+ const caption: Element = {
60
+ type: "element",
61
+ tagName: "figcaption",
62
+ properties: {className: ["wg_prose__codeFileTitle"]},
63
+ children: [
64
+ {type: "text", value: title},
65
+ {type: "element", tagName: "span", properties: {className: ["wg_prose__codeFileLang"]}, children: [{type: "text", value: this.options.lang}]},
66
+ ],
67
+ };
68
+ const figure: Element = {
69
+ type: "element",
70
+ tagName: "figure",
71
+ properties: {className: ["wg_prose__codeFile", ...tones.map((tone) => `wg_prose__codeFile--${tone}`)]},
72
+ children: [caption, ...root.children.filter((child): child is ElementContent => child.type !== "doctype")],
73
+ };
74
+
75
+ root.children = [figure];
76
+ },
77
+ };
78
+
79
+ /**
80
+ * 코드 줄 표기 · `// [!code highlight|error|warning|++|--]`, `// [!code word:x]`, 메타 `{2,4-6}`, `line-numbers`, 블록 색 `bad`·`good`, 제목 `[파일 이름]`
26
81
  */
27
82
  export const shikiTransformers: ShikiTransformer[] = [
28
83
  transformerNotationHighlight({matchAlgorithm: "v3"}),
@@ -31,4 +86,6 @@ export const shikiTransformers: ShikiTransformer[] = [
31
86
  transformerNotationWordHighlight({matchAlgorithm: "v3"}),
32
87
  transformerMetaHighlight(),
33
88
  lineNumbers,
89
+ tone,
90
+ fileTitle,
34
91
  ];
@@ -22,19 +22,21 @@ import {remarkUnknownDirectives} from "@/component/widget/prose/_function/remark
22
22
  import {rehypeAssets} from "@/entry/cli/_function/create-processor/_rehype-assets";
23
23
  import {rehypeHeadingIds} from "@/entry/cli/_function/create-processor/_rehype-heading-ids";
24
24
  import {remarkReport} from "@/entry/cli/_function/create-processor/_remark-report";
25
+ import {remarkSnippets} from "@/entry/cli/_function/create-processor/_remark-snippets";
25
26
  import {shikiTransformers} from "@/entry/cli/_function/create-processor/_shiki-transformers";
26
27
  import type {SiteConfig} from "@/type/site-config";
27
28
 
28
29
  /**
29
- * Markdown 처리 순서: remark(부품·흐름도) → 코드 강조(줄 표기 transformer) → 머리 → 제목 id → 가름 수집 → 표
30
+ * Markdown 처리 순서: remark(스니펫·부품·흐름도) → 코드 강조(줄 표기·블록 색 transformer) → 머리 → 제목 id → 가름 수집 → 표
30
31
  * 플러그인 HTML 조각은 마지막 rehype-raw에서 해석
31
32
  * Shiki inline style: token.css의 --app-code-* 참조
32
33
  */
33
- export const createProcessor = (options: {site: SiteConfig; root: string; files?: Map<string, string>}) => {
34
+ export const createProcessor = (options: {site: SiteConfig; root: string; files?: Map<string, string>; snippets?: Set<string>}) => {
34
35
  return unified()
35
36
  .use(remarkParse)
36
37
  .use(remarkFrontmatter, ["yaml"])
37
38
  .use(remarkGfm)
39
+ .use(remarkSnippets, {root: options.root, snippets: options.snippets ?? new Set<string>()})
38
40
  .use(remarkSmartypants)
39
41
  .use(remarkDirective)
40
42
  .use(remarkBlocks)
@@ -96,6 +96,82 @@ test("code notation marks lines, meta highlights lines, and line-numbers tags th
96
96
  }
97
97
  });
98
98
 
99
+ test("a bracketed fence meta renders a file title bar that carries the block tone", async () => {
100
+ const root = await mkdtemp(join(tmpdir(), "for-humanity-code-title-"));
101
+
102
+ try {
103
+ await writeFile(
104
+ join(root, "guide.md"),
105
+ "---\nname: Guide\nlabel: Guide\ngroup: Guide\n---\n\n## Read\n\n```ts bad [page/products.ts]\nconst a = 1;\n```\n\n```ts {1}\nconst b = 2;\n```\n",
106
+ );
107
+ const site = siteConfigSchema.parse({});
108
+ const [doc] = await readDocs({root, processor: createProcessor({site, root})});
109
+
110
+ assert.ok(doc);
111
+ assert.match(
112
+ doc.html,
113
+ /<figure class="wg_prose__codeFile wg_prose__codeFile--bad"><figcaption class="wg_prose__codeFileTitle">page\/products\.ts<span class="wg_prose__codeFileLang">ts<\/span><\/figcaption><pre class="[^"]*wg_prose__code--bad/,
114
+ );
115
+ assert.equal(doc.html.split("<figure").length - 1, 1, "braces meta is not a title");
116
+ } finally {
117
+ await rm(root, {recursive: true, force: true});
118
+ }
119
+ });
120
+
121
+ test("code snippets import a file or a region from inside the document root and pass fence meta", async () => {
122
+ const root = await mkdtemp(join(tmpdir(), "for-humanity-snippets-"));
123
+
124
+ try {
125
+ await mkdir(join(root, "snippet"));
126
+ await writeFile(
127
+ join(root, "snippet", "example.ts"),
128
+ "const outside = 0;\n// #region picked\n const a = 1; // [!code error]\n const b = 2;\n// #endregion picked\nconst after = 3;\n",
129
+ );
130
+ await writeFile(
131
+ join(root, "guide.md"),
132
+ "---\nname: Guide\nlabel: Guide\ngroup: Guide\n---\n\n## Read\n\n<<< ./snippet/example.ts#picked bad line-numbers\n\n<<< ./snippet/example.ts\n",
133
+ );
134
+ const site = siteConfigSchema.parse({});
135
+ const snippets = new Set<string>();
136
+ const [doc] = await readDocs({root, processor: createProcessor({site, root, snippets})});
137
+
138
+ assert.ok(doc);
139
+ assert.match(doc.html, /<pre class="[^"]*wg_prose__lineNumbers[^"]*wg_prose__code--bad[^"]*"|<pre class="[^"]*wg_prose__code--bad[^"]*wg_prose__lineNumbers[^"]*"/);
140
+ assert.match(doc.html, /<span class="line highlighted error">[^\n]*const<\/span>[^\n]* a/);
141
+ assert.equal(doc.html.includes("#region"), false, "region marker lines are removed");
142
+ assert.equal(doc.html.split("const</span>").length - 1, 2 + 4, "the region block has two lines and the whole-file block four");
143
+ assert.deepEqual(
144
+ [...snippets].map((entry) => entry.endsWith(join("snippet", "example.ts"))),
145
+ [true],
146
+ );
147
+ } finally {
148
+ await rm(root, {recursive: true, force: true});
149
+ }
150
+ });
151
+
152
+ test("code snippets reject files outside the document root, missing files and missing regions", async () => {
153
+ const root = await mkdtemp(join(tmpdir(), "for-humanity-snippets-reject-"));
154
+
155
+ try {
156
+ await mkdir(join(root, "docs"));
157
+ await writeFile(join(root, "outside.ts"), "const x = 1;\n");
158
+ await writeFile(join(root, "docs", "present.ts"), "const y = 2;\n");
159
+ const site = siteConfigSchema.parse({});
160
+ const docsRoot = join(root, "docs");
161
+
162
+ for (const [reference, expected] of [
163
+ ["../outside.ts", /문서 폴더 밖/],
164
+ ["./missing.ts", /파일이 없다/],
165
+ ["./present.ts#nope", /region 이 없다/],
166
+ ] as const) {
167
+ await writeFile(join(docsRoot, "guide.md"), `---\nname: Guide\nlabel: Guide\ngroup: Guide\n---\n\n<<< ${reference}\n`);
168
+ await assert.rejects(readDocs({root: docsRoot, processor: createProcessor({site, root: docsRoot})}), expected, reference);
169
+ }
170
+ } finally {
171
+ await rm(root, {recursive: true, force: true});
172
+ }
173
+ });
174
+
99
175
  test("cursor face is off unless the site turns it on", () => {
100
176
  assert.equal(siteConfigSchema.parse({}).cursorFace, false);
101
177
  assert.equal(siteConfigSchema.parse({cursorFace: true}).cursorFace, true);
@@ -92,7 +92,8 @@ const main = async () => {
92
92
  ...brand.files,
93
93
  ]);
94
94
  const files = new Map(kitFiles);
95
- const processor = createProcessor({site: siteConfig, root: docsRoot, files});
95
+ const snippets = new Set<string>();
96
+ const processor = createProcessor({site: siteConfig, root: docsRoot, files, snippets});
96
97
  const [docs, home] = await Promise.all([
97
98
  readDocs({root: docsRoot, processor, reserved: siteConfig.url === undefined ? undefined : crawling_reserved_roots}),
98
99
  readHome({root: docsRoot, processor}),
@@ -147,7 +148,8 @@ const main = async () => {
147
148
  run: async () => {
148
149
  const currentGeneration = generation;
149
150
  const nextFiles = new Map(kitFiles);
150
- const nextProcessor = createProcessor({site: siteConfig, root: docsRoot, files: nextFiles});
151
+ const nextSnippets = new Set<string>();
152
+ const nextProcessor = createProcessor({site: siteConfig, root: docsRoot, files: nextFiles, snippets: nextSnippets});
151
153
  const [docs, home] = await Promise.all([
152
154
  readDocs({root: docsRoot, processor: nextProcessor, reserved: siteConfig.url === undefined ? undefined : crawling_reserved_roots}),
153
155
  readHome({root: docsRoot, processor: nextProcessor}),
@@ -161,6 +163,10 @@ const main = async () => {
161
163
  for (const entry of nextFiles) {
162
164
  files.set(...entry);
163
165
  }
166
+ snippets.clear();
167
+ for (const entry of nextSnippets) {
168
+ snippets.add(entry);
169
+ }
164
170
  store.docs = docs;
165
171
  store.home = home;
166
172
  reload.emit("change");
@@ -171,12 +177,17 @@ const main = async () => {
171
177
  );
172
178
 
173
179
  /**
174
- * 입력 변경 감지 · 출력·설치 폴더 제외 · 누락 자원 추가도 다시 확인
180
+ * 입력 변경 감지 · 출력·설치 폴더 제외 · 누락 자원 추가도 다시 확인 · 불러온 스니펫 파일 변경 포함
175
181
  */
176
182
  const handleDocsChange = (_event: string, filename: string | null) => {
177
183
  if (filename !== null) {
178
184
  const segments = filename.split(sep);
179
- if (segments.includes("dist") || segments.includes("node_modules") || (!/\.mdx?$/i.test(filename) && !asset_media_extensions.has(extname(filename).toLowerCase()))) {
185
+ const isSnippet = snippets.has(join(docsRoot, filename));
186
+ if (
187
+ segments.includes("dist") ||
188
+ segments.includes("node_modules") ||
189
+ (!isSnippet && !/\.mdx?$/i.test(filename) && !asset_media_extensions.has(extname(filename).toLowerCase()))
190
+ ) {
180
191
  return;
181
192
  }
182
193
  }
@@ -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
- /* 코드 줄 표기 · error·remove 는 bad, add 는 good, warning 은 warn · soft 가 줄 바탕 */
38
+ /* 코드 판정 색 · soft 는 블록 바탕(메타 bad·good), mark 는 그 위의 줄 표기 · warn 은 줄 표기만 */
39
39
  --app-color-bad: light-dark(#b2412e, #ff9b8a);
40
- --app-color-bad-soft: light-dark(#fbe9e6, #42241f);
40
+ --app-color-bad-soft: light-dark(#f9ebe8, #3a2522);
41
+ --app-color-bad-mark: light-dark(#f1cdc6, #5b322c);
41
42
  --app-color-good: light-dark(#2f7a47, #8bd3a3);
42
- --app-color-good-soft: light-dark(#e3f3e8, #1f3a27);
43
+ --app-color-good-soft: light-dark(#e4f3ed, #1c342c);
44
+ --app-color-good-mark: light-dark(#c4e4d4, #27503f);
43
45
  --app-color-warn: light-dark(#8a6400, #e6c46a);
44
46
  --app-color-warn-soft: light-dark(#fbf3d9, #3d3418);
45
47
 
@@ -173,3 +175,14 @@
173
175
  --app-space-gutter: 16px;
174
176
  }
175
177
  }
178
+
179
+ /* 블록 색 · 펜스 메타 bad·good · Shiki 가 pre 에 인라인으로 쓰는 코드 바탕과 제목 띠의 바탕을 바꿈 */
180
+ .wg_prose__code--bad,
181
+ .wg_prose__codeFile--bad {
182
+ --app-code-background: var(--app-color-bad-soft);
183
+ }
184
+
185
+ .wg_prose__code--good,
186
+ .wg_prose__codeFile--good {
187
+ --app-code-background: var(--app-color-good-soft);
188
+ }