for-humanity 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.md +3 -3
- package/dist/cli.css +19 -2
- package/dist/cli.js +131 -23
- package/package.json +1 -1
- package/src/component/widget/prose/wg-prose.css +12 -0
- package/src/constant/copy.ts +5 -0
- package/src/entry/cli/_constant/snippet.ts +21 -0
- package/src/entry/cli/_function/create-processor/_remark-snippets.ts +117 -0
- package/src/entry/cli/_function/create-processor/_shiki-transformers.ts +22 -1
- package/src/entry/cli/_function/create-processor/create-processor.ts +4 -2
- package/src/entry/cli/cli.test.ts +54 -0
- package/src/entry/cli/cli.ts +15 -4
- package/src/style/token.css +14 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.11.0 — 2026-10-13
|
|
6
|
+
|
|
7
|
+
- Add `<<< ./file.ext#region meta` code snippet imports: a file inside the document root becomes a code block, with optional `#region` extraction, dedent, fence meta passthrough and dev reloads when the file changes.
|
|
8
|
+
- Add `bad` and `good` fence meta that tint the whole code block, with line notation drawn one step darker on top.
|
|
9
|
+
|
|
5
10
|
## 0.10.0 — 2026-10-13
|
|
6
11
|
|
|
7
12
|
- Add VitePress-style code notation through Shiki transformers: `// [!code highlight|error|warning|++|--]`, `// [!code word:name]`, meta `{2,4-6}` and a `line-numbers` meta that numbers lines with CSS counters.
|
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ Write Markdown. Keep the context. Share a static site.
|
|
|
33
33
|
Node.js 22 or later and pnpm. Install the CLI in your document project:
|
|
34
34
|
|
|
35
35
|
```sh
|
|
36
|
-
pnpm add -D --save-exact for-humanity@0.
|
|
36
|
+
pnpm add -D --save-exact for-humanity@0.11.0
|
|
37
37
|
mkdir docs
|
|
38
38
|
```
|
|
39
39
|
|
|
@@ -52,7 +52,7 @@ pnpm exec for-humanity preview docs
|
|
|
52
52
|
|
|
53
53
|
If your Markdown files are in the project root, use `.` instead of `docs`. The root `README.md` becomes the home page; root `AGENTS.md` and `CLAUDE.md` stay out of the site.
|
|
54
54
|
|
|
55
|
-
Version `0.
|
|
55
|
+
Version `0.11.0` supports `dev`, `build` and `preview`. `init` is planned; see the [Roadmap](https://github.com/minu-ha/for-humanity/blob/main/templates/document/roadmap.md) and [Changelog](https://github.com/minu-ha/for-humanity/blob/main/CHANGELOG.md).
|
|
56
56
|
Cloudflare Pages can host the generated site. See [Deployment](https://github.com/minu-ha/for-humanity/blob/main/templates/document/deployment.md) for the build settings.
|
|
57
57
|
Set an optional public root `url` in the site configuration to generate `robots.txt` and `sitemap.xml`; projects without a URL emit neither file. See [Crawling](https://github.com/minu-ha/for-humanity/blob/main/templates/document/crawling.md).
|
|
58
58
|
|
|
@@ -75,7 +75,7 @@ pnpm preview # Preview the static site
|
|
|
75
75
|
pnpm build:pages # Add Cloudflare Pages headers and a 404 page
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
Changes pushed to `main` update the documentation site. A stable version tag such as `v0.
|
|
78
|
+
Changes pushed to `main` update the documentation site. A stable version tag such as `v0.11.0` runs checks, publishes the package to npm, then creates a GitHub Release with the same package file.
|
|
79
79
|
The npm README updates with each package release. See [Maintenance](https://github.com/minu-ha/for-humanity/blob/main/templates/document/maintenance.md#packaging-and-release) for the release steps.
|
|
80
80
|
|
|
81
81
|
## Make it yours
|
package/dist/cli.css
CHANGED
|
@@ -189,6 +189,15 @@
|
|
|
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);
|
|
@@ -779,9 +788,11 @@
|
|
|
779
788
|
--app-color-accent: light-dark(#555, #b5b5b5);
|
|
780
789
|
--app-color-accent-soft: light-dark(#f5f5f2, #222);
|
|
781
790
|
--app-color-bad: light-dark(#b2412e, #ff9b8a);
|
|
782
|
-
--app-color-bad-soft: light-dark(#
|
|
791
|
+
--app-color-bad-soft: light-dark(#f9ebe8, #3a2522);
|
|
792
|
+
--app-color-bad-mark: light-dark(#f1cdc6, #5b322c);
|
|
783
793
|
--app-color-good: light-dark(#2f7a47, #8bd3a3);
|
|
784
|
-
--app-color-good-soft: light-dark(#
|
|
794
|
+
--app-color-good-soft: light-dark(#e4f3ed, #1c342c);
|
|
795
|
+
--app-color-good-mark: light-dark(#c4e4d4, #27503f);
|
|
785
796
|
--app-color-warn: light-dark(#8a6400, #e6c46a);
|
|
786
797
|
--app-color-warn-soft: light-dark(#fbf3d9, #3d3418);
|
|
787
798
|
--app-color-link: light-dark(#11a, #9db8ff);
|
|
@@ -875,6 +886,12 @@
|
|
|
875
886
|
--app-space-gutter: 16px;
|
|
876
887
|
}
|
|
877
888
|
}
|
|
889
|
+
.wg_prose__code--bad {
|
|
890
|
+
--app-code-background: var(--app-color-bad-soft);
|
|
891
|
+
}
|
|
892
|
+
.wg_prose__code--good {
|
|
893
|
+
--app-code-background: var(--app-color-good-soft);
|
|
894
|
+
}
|
|
878
895
|
|
|
879
896
|
/* src/component/widget/shell/wg-shell.css */
|
|
880
897
|
.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
|
|
5
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4, watch } from "node:fs";
|
|
6
6
|
import { copyFile, mkdir, rm, writeFile } from "node:fs/promises";
|
|
7
|
-
import { dirname as
|
|
7
|
+
import { dirname as dirname5, extname as extname5, join as join4, relative as relative5, resolve as resolve4, sep as sep4 } from "node:path";
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
9
|
import { serve } from "@hono/node-server";
|
|
10
10
|
import { serveStatic } from "@hono/node-server/serve-static";
|
|
@@ -64,6 +64,7 @@ var copy_error_directive_syntax = "\uBD80\uD488 \uBB38\uBC95\uC774 \uC62C\uBC14\
|
|
|
64
64
|
var copy_warn_broken_link = "\uC5C6\uB294 \uBB38\uC11C\uB85C \uAC74 \uB9C1\uD06C\uB2E4";
|
|
65
65
|
var copy_warn_unknown_status = "\uC124\uC815\uC5D0 \uC5C6\uB294 \uC0C1\uD0DC \uBB38\uAD6C\uB2E4";
|
|
66
66
|
var copy_error_asset = "\uC790\uC6D0\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uB2E4";
|
|
67
|
+
var copy_error_snippet = "\uCF54\uB4DC \uC2A4\uB2C8\uD3AB\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uB2E4";
|
|
67
68
|
|
|
68
69
|
// src/constant/font.ts
|
|
69
70
|
var font_mono_css = "src/asset/font/monoplex-kr/monoplex-kr.css";
|
|
@@ -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,38 @@ 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
|
+
};
|
|
1783
1883
|
var shikiTransformers = [
|
|
1784
1884
|
transformerNotationHighlight({ matchAlgorithm: "v3" }),
|
|
1785
1885
|
transformerNotationErrorLevel({ matchAlgorithm: "v3" }),
|
|
1786
1886
|
transformerNotationDiff({ matchAlgorithm: "v3" }),
|
|
1787
1887
|
transformerNotationWordHighlight({ matchAlgorithm: "v3" }),
|
|
1788
1888
|
transformerMetaHighlight(),
|
|
1789
|
-
lineNumbers
|
|
1889
|
+
lineNumbers,
|
|
1890
|
+
tone
|
|
1790
1891
|
];
|
|
1791
1892
|
|
|
1792
1893
|
// src/entry/cli/_function/create-processor/create-processor.ts
|
|
1793
1894
|
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 });
|
|
1895
|
+
return unified().use(remarkParse).use(remarkFrontmatter, ["yaml"]).use(remarkGfm).use(remarkSnippets, { root: options.root, snippets: options.snippets ?? /* @__PURE__ */ new Set() }).use(remarkSmartypants).use(remarkDirective).use(remarkBlocks).use(remarkParts).use(remarkFlow).use(remarkStatus, { status: options.site.status }).use(remarkSwatch).use(remarkLinks, { root: options.root }).use(remarkUnknownDirectives).use(remarkReport, { root: options.root }).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeShiki, { theme: createCssVariablesTheme({ name: "for-humanity", variablePrefix: "--app-code-" }), transformers: shikiTransformers }).use(rehypeHead).use(rehypeHeadingIds).use(rehypeSections).use(rehypeTables).use(rehypeRaw).use(rehypeAssets, { root: options.root, files: options.files ?? /* @__PURE__ */ new Map() }).use(rehypeStringify, { allowDangerousHtml: true });
|
|
1795
1896
|
};
|
|
1796
1897
|
|
|
1797
1898
|
// src/entry/cli/_function/read-docs.ts
|
|
1798
1899
|
import { readdir, readFile as readFile2 } from "node:fs/promises";
|
|
1799
|
-
import { join, posix as posix3, sep as
|
|
1900
|
+
import { join, posix as posix3, sep as sep3 } from "node:path";
|
|
1800
1901
|
import { VFile } from "vfile";
|
|
1801
1902
|
import { parse as parseYaml } from "yaml";
|
|
1802
1903
|
|
|
@@ -1845,11 +1946,11 @@ var docDataSchema = z2.object({
|
|
|
1845
1946
|
// src/entry/cli/_function/read-docs.ts
|
|
1846
1947
|
var readDocs = async (options) => {
|
|
1847
1948
|
const paths = (await readdir(options.root, { recursive: true })).filter(
|
|
1848
|
-
(path) => /\.mdx?$/i.test(path) && !/^(readme|agents|claude)\.md$/i.test(path) && !path.split(
|
|
1949
|
+
(path) => /\.mdx?$/i.test(path) && !/^(readme|agents|claude)\.md$/i.test(path) && !path.split(sep3).includes("node_modules") && !path.startsWith(`dist${sep3}`)
|
|
1849
1950
|
);
|
|
1850
1951
|
const docs = await Promise.all(
|
|
1851
1952
|
paths.map(async (path) => {
|
|
1852
|
-
const id = path.replace(/\.mdx?$/i, "").split(
|
|
1953
|
+
const id = path.replace(/\.mdx?$/i, "").split(sep3).join("/").toLowerCase();
|
|
1853
1954
|
const segments = id.split("/");
|
|
1854
1955
|
if (segments.some((segment) => segment === "" || segment === "." || segment === ".." || segment === "index.html") || /[#?%*:\\]/.test(id) || /[\p{Cc}]/u.test(id) || options.reserved?.includes(segments[0]) === true || segments[0] === asset_dir || segments[0] === asset_favicon_path.slice(1)) {
|
|
1855
1956
|
throw new Error(`${copy_error_doc_id}: ${path}`);
|
|
@@ -2043,15 +2144,15 @@ var toErrorMessage = (error) => {
|
|
|
2043
2144
|
|
|
2044
2145
|
// src/util/font/to-font-css.ts
|
|
2045
2146
|
import { createHash as createHash2 } from "node:crypto";
|
|
2046
|
-
import { readFileSync as
|
|
2047
|
-
import { basename, dirname as
|
|
2147
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
2148
|
+
import { basename, dirname as dirname4, extname as extname4, join as join3 } from "node:path";
|
|
2048
2149
|
var toFontCss = (options) => {
|
|
2049
2150
|
const files = /* @__PURE__ */ new Map();
|
|
2050
|
-
const css = [...
|
|
2151
|
+
const css = [...readFileSync3(options.css, "utf8").matchAll(/@font-face\s*\{[^}]*\}/g)].map(
|
|
2051
2152
|
(match) => match[0].replace(/font-display:\s*[^;]+;/g, `font-display: ${font_display_strategy};`).replace(/url\(([^)]+)\)/g, (_match, relativeUrl) => {
|
|
2052
|
-
const file = join3(
|
|
2053
|
-
const extension =
|
|
2054
|
-
const hash = createHash2("sha256").update(
|
|
2153
|
+
const file = join3(dirname4(options.css), relativeUrl);
|
|
2154
|
+
const extension = extname4(file);
|
|
2155
|
+
const hash = createHash2("sha256").update(readFileSync3(file)).digest("hex").slice(0, font_hash_length);
|
|
2055
2156
|
const url = `${options.fontDir}/${basename(file, extension)}.${hash}${extension}`;
|
|
2056
2157
|
files.set(url, file);
|
|
2057
2158
|
return `url(${url})`;
|
|
@@ -2068,10 +2169,10 @@ var main = async () => {
|
|
|
2068
2169
|
throw new Error(`${copy_error_unknown_command}: ${command} (${commands.join(", ")})`);
|
|
2069
2170
|
}
|
|
2070
2171
|
const kitRoot = fileURLToPath(new URL("../", import.meta.url));
|
|
2071
|
-
const docsRoot =
|
|
2172
|
+
const docsRoot = resolve4(docsDir);
|
|
2072
2173
|
const outDir = join4(docsRoot, "dist");
|
|
2073
2174
|
const configFile = join4(docsRoot, cli_config_file_name);
|
|
2074
|
-
const parsedConfig = siteConfigSchema.safeParse(
|
|
2175
|
+
const parsedConfig = siteConfigSchema.safeParse(existsSync4(configFile) ? (await import(pathToFileURL(configFile).href)).default : site_config_absent);
|
|
2075
2176
|
if (!parsedConfig.success) {
|
|
2076
2177
|
throw new Error(`${copy_error_config}: ${parsedConfig.error.issues.map((issue) => `${issue.path.join(".")} ${issue.message}`).join(", ")}`);
|
|
2077
2178
|
}
|
|
@@ -2081,7 +2182,7 @@ var main = async () => {
|
|
|
2081
2182
|
app2.use(
|
|
2082
2183
|
"/*",
|
|
2083
2184
|
serveStatic({
|
|
2084
|
-
root:
|
|
2185
|
+
root: relative5(process.cwd(), outDir),
|
|
2085
2186
|
onFound: (_path, c) => {
|
|
2086
2187
|
if (c.req.path.startsWith(`${asset_font_dir}/`)) {
|
|
2087
2188
|
c.header("Cache-Control", font_cache_control);
|
|
@@ -2097,7 +2198,7 @@ var main = async () => {
|
|
|
2097
2198
|
const brand = toFontCss({ css: join4(kitRoot, font_brand_css), fontDir: asset_font_dir });
|
|
2098
2199
|
const fontCss = [mono.css, body.css, brand.css].join("\n");
|
|
2099
2200
|
const stylePath = toAssetPath({ path: asset_style_path, file: join4(kitRoot, "dist/cli.css") });
|
|
2100
|
-
const browserScript =
|
|
2201
|
+
const browserScript = readFileSync4(join4(kitRoot, "dist/browser.js"), "utf8");
|
|
2101
2202
|
const kitFiles = new Map([
|
|
2102
2203
|
[stylePath, join4(kitRoot, "dist/cli.css")],
|
|
2103
2204
|
[asset_favicon_path, join4(kitRoot, "src/asset/favicon.svg")],
|
|
@@ -2106,7 +2207,8 @@ var main = async () => {
|
|
|
2106
2207
|
...brand.files
|
|
2107
2208
|
]);
|
|
2108
2209
|
const files = new Map(kitFiles);
|
|
2109
|
-
const
|
|
2210
|
+
const snippets = /* @__PURE__ */ new Set();
|
|
2211
|
+
const processor = createProcessor({ site: siteConfig, root: docsRoot, files, snippets });
|
|
2110
2212
|
const [docs, home] = await Promise.all([
|
|
2111
2213
|
readDocs({ root: docsRoot, processor, reserved: siteConfig.url === void 0 ? void 0 : crawling_reserved_roots }),
|
|
2112
2214
|
readHome({ root: docsRoot, processor })
|
|
@@ -2129,11 +2231,11 @@ var main = async () => {
|
|
|
2129
2231
|
throw crawlerResult.error;
|
|
2130
2232
|
}
|
|
2131
2233
|
for (const [url, file] of files) {
|
|
2132
|
-
await mkdir(
|
|
2234
|
+
await mkdir(dirname5(join4(outDir, url)), { recursive: true });
|
|
2133
2235
|
await copyFile(file, join4(outDir, url));
|
|
2134
2236
|
}
|
|
2135
2237
|
console.log(
|
|
2136
|
-
`${crawlerResult === void 0 ? result.files.length : result.files.length + crawlerResult.files.length} files, ${files.size} assets \u2192 ${
|
|
2238
|
+
`${crawlerResult === void 0 ? result.files.length : result.files.length + crawlerResult.files.length} files, ${files.size} assets \u2192 ${relative5(process.cwd(), outDir)}`
|
|
2137
2239
|
);
|
|
2138
2240
|
return;
|
|
2139
2241
|
}
|
|
@@ -2145,7 +2247,8 @@ var main = async () => {
|
|
|
2145
2247
|
run: async () => {
|
|
2146
2248
|
const currentGeneration = generation;
|
|
2147
2249
|
const nextFiles = new Map(kitFiles);
|
|
2148
|
-
const
|
|
2250
|
+
const nextSnippets = /* @__PURE__ */ new Set();
|
|
2251
|
+
const nextProcessor = createProcessor({ site: siteConfig, root: docsRoot, files: nextFiles, snippets: nextSnippets });
|
|
2149
2252
|
const [docs2, home2] = await Promise.all([
|
|
2150
2253
|
readDocs({ root: docsRoot, processor: nextProcessor, reserved: siteConfig.url === void 0 ? void 0 : crawling_reserved_roots }),
|
|
2151
2254
|
readHome({ root: docsRoot, processor: nextProcessor })
|
|
@@ -2157,6 +2260,10 @@ var main = async () => {
|
|
|
2157
2260
|
for (const entry of nextFiles) {
|
|
2158
2261
|
files.set(...entry);
|
|
2159
2262
|
}
|
|
2263
|
+
snippets.clear();
|
|
2264
|
+
for (const entry of nextSnippets) {
|
|
2265
|
+
snippets.add(entry);
|
|
2266
|
+
}
|
|
2160
2267
|
store.docs = docs2;
|
|
2161
2268
|
store.home = home2;
|
|
2162
2269
|
reload.emit("change");
|
|
@@ -2167,8 +2274,9 @@ var main = async () => {
|
|
|
2167
2274
|
);
|
|
2168
2275
|
const handleDocsChange = (_event, filename) => {
|
|
2169
2276
|
if (filename !== null) {
|
|
2170
|
-
const segments = filename.split(
|
|
2171
|
-
|
|
2277
|
+
const segments = filename.split(sep4);
|
|
2278
|
+
const isSnippet = snippets.has(join4(docsRoot, filename));
|
|
2279
|
+
if (segments.includes("dist") || segments.includes("node_modules") || !isSnippet && !/\.mdx?$/i.test(filename) && !asset_media_extensions.has(extname5(filename).toLowerCase())) {
|
|
2172
2280
|
return;
|
|
2173
2281
|
}
|
|
2174
2282
|
}
|
package/package.json
CHANGED
|
@@ -245,6 +245,18 @@
|
|
|
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);
|
package/src/constant/copy.ts
CHANGED
|
@@ -126,3 +126,8 @@ export const copy_warn_unknown_status = "설정에 없는 상태 문구다";
|
|
|
126
126
|
* 문서에서 참조한 로컬 자원의 경로·형식 오류
|
|
127
127
|
*/
|
|
128
128
|
export const copy_error_asset = "자원이 올바르지 않다";
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* `<<<` 로 불러온 코드 스니펫의 경로·파일·region 오류
|
|
132
|
+
*/
|
|
133
|
+
export const copy_error_snippet = "코드 스니펫이 올바르지 않다";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 스니펫 줄 · `<<< ./path.ext`, 선택 `#region`, 선택 메타(펜스 메타와 같음)
|
|
3
|
+
*/
|
|
4
|
+
export const snippet_pattern = /^<<<\s+(\S+?)(?:#([\w-]+))?(?:\s+(.+?))?\s*$/;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* region 표시 줄 · 주석 모양은 자유 · `#region 이름`, `#endregion 이름`
|
|
8
|
+
*/
|
|
9
|
+
export const snippet_region_pattern = /^\s*\S*\s*#(end)?region\s+([\w-]+)/;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 확장자 → 펜스 언어 · 없으면 확장자 그대로
|
|
13
|
+
*/
|
|
14
|
+
export const snippet_language_by_extension: Record<string, string> = {
|
|
15
|
+
".cjs": "js",
|
|
16
|
+
".md": "markdown",
|
|
17
|
+
".mjs": "js",
|
|
18
|
+
".mts": "ts",
|
|
19
|
+
".txt": "text",
|
|
20
|
+
".yml": "yaml",
|
|
21
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import {existsSync, readFileSync, realpathSync, statSync} from "node:fs";
|
|
2
|
+
import {dirname, extname, isAbsolute, relative, resolve, sep} from "node:path";
|
|
3
|
+
import type {Root} from "mdast";
|
|
4
|
+
import {visit} from "unist-util-visit";
|
|
5
|
+
import type {VFile} from "vfile";
|
|
6
|
+
import {copy_error_snippet} from "@/constant/copy";
|
|
7
|
+
import {snippet_language_by_extension, snippet_pattern, snippet_region_pattern} from "@/entry/cli/_constant/snippet";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 스니펫 수집 계약
|
|
11
|
+
*/
|
|
12
|
+
export interface SnippetOptions {
|
|
13
|
+
/**
|
|
14
|
+
* 스니펫을 읽을 수 있는 문서 루트
|
|
15
|
+
*/
|
|
16
|
+
root: string;
|
|
17
|
+
/**
|
|
18
|
+
* dev 새로고침이 지켜볼 불러온 파일 경로
|
|
19
|
+
*/
|
|
20
|
+
snippets: Set<string>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* region 이름이 있으면 그 안만, 없으면 전체 · region 표시 줄은 항상 제거 · 공통 들여쓰기 제거
|
|
25
|
+
*/
|
|
26
|
+
const toSnippetBody = (source: string, region: string | undefined): string | undefined => {
|
|
27
|
+
const lines = source.replace(/\r\n/g, "\n").split("\n");
|
|
28
|
+
let inside = region === undefined;
|
|
29
|
+
let found = region === undefined;
|
|
30
|
+
const kept: string[] = [];
|
|
31
|
+
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
const marker = snippet_region_pattern.exec(line);
|
|
34
|
+
|
|
35
|
+
if (marker !== null) {
|
|
36
|
+
if (region !== undefined && marker[2] === region) {
|
|
37
|
+
inside = marker[1] === undefined;
|
|
38
|
+
found = true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (inside) {
|
|
45
|
+
kept.push(line);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!found) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
while (kept.length > 0 && kept[0]?.trim() === "") {
|
|
54
|
+
kept.shift();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
while (kept.length > 0 && kept.at(-1)?.trim() === "") {
|
|
58
|
+
kept.pop();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const indents = kept.filter((line) => line.trim() !== "").map((line) => line.length - line.trimStart().length);
|
|
62
|
+
const indent = indents.length === 0 ? 0 : Math.min(...indents);
|
|
63
|
+
|
|
64
|
+
return kept.map((line) => line.slice(Math.min(indent, line.length - line.trimStart().length))).join("\n");
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* `<<< ./path.ext#region 메타` 문단 → 코드 펜스 · 파일 위치 기준 경로 · 문서 폴더 밖과 없는 파일은 빌드 실패
|
|
69
|
+
* 불러온 파일은 dev 감시 목록에 올림
|
|
70
|
+
*/
|
|
71
|
+
export const remarkSnippets = (options: SnippetOptions) => (tree: Root, file: VFile) => {
|
|
72
|
+
visit(tree, "paragraph", (node, index, parent) => {
|
|
73
|
+
const [child] = node.children;
|
|
74
|
+
|
|
75
|
+
if (node.children.length !== 1 || child === undefined || child.type !== "text" || parent === undefined || index === undefined) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const match = snippet_pattern.exec(child.value.trim());
|
|
80
|
+
|
|
81
|
+
if (match === null) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const [, path = "", region, meta] = match;
|
|
86
|
+
const target = resolve(dirname(file.path), path);
|
|
87
|
+
const location = relative(options.root, target);
|
|
88
|
+
|
|
89
|
+
if (location === ".." || location.startsWith(`..${sep}`) || isAbsolute(location)) {
|
|
90
|
+
file.fail(`${copy_error_snippet}: 문서 폴더 밖 · ${path}`, node);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!existsSync(target)) {
|
|
94
|
+
file.fail(`${copy_error_snippet}: 파일이 없다 · ${path}`, node);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const actual = realpathSync(target);
|
|
98
|
+
const actualLocation = relative(realpathSync(options.root), actual);
|
|
99
|
+
|
|
100
|
+
if (actualLocation === ".." || actualLocation.startsWith(`..${sep}`) || isAbsolute(actualLocation) || !statSync(actual).isFile()) {
|
|
101
|
+
file.fail(`${copy_error_snippet}: 문서 폴더 밖이거나 일반 파일이 아니다 · ${path}`, node);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const body = toSnippetBody(readFileSync(actual, "utf8"), region);
|
|
105
|
+
|
|
106
|
+
if (body === undefined) {
|
|
107
|
+
file.fail(`${copy_error_snippet}: region 이 없다 · ${path}#${region}`, node);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const extension = extname(actual).toLowerCase();
|
|
112
|
+
const lang = snippet_language_by_extension[extension] === undefined ? extension.slice(1) : snippet_language_by_extension[extension];
|
|
113
|
+
|
|
114
|
+
options.snippets.add(actual);
|
|
115
|
+
parent.children.splice(index, 1, {type: "code", lang, meta: meta === undefined ? null : meta, value: body});
|
|
116
|
+
});
|
|
117
|
+
};
|
|
@@ -22,7 +22,27 @@ const lineNumbers: ShikiTransformer = {
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* 펜스 메타 `bad`·`good` → pre 에 블록 색 클래스 · 바탕색은 token.css 가 바꿈
|
|
26
|
+
*/
|
|
27
|
+
const tone: ShikiTransformer = {
|
|
28
|
+
name: "for-humanity:code-tone",
|
|
29
|
+
pre(node) {
|
|
30
|
+
const meta = this.options.meta?.__raw;
|
|
31
|
+
|
|
32
|
+
if (typeof meta !== "string") {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const word of ["bad", "good"]) {
|
|
37
|
+
if (new RegExp(`\\b${word}\\b`).test(meta)) {
|
|
38
|
+
this.addClassToHast(node, `wg_prose__code--${word}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 코드 줄 표기 · `// [!code highlight|error|warning|++|--]`, `// [!code word:x]`, 메타 `{2,4-6}`, `line-numbers`, 블록 색 `bad`·`good`
|
|
26
46
|
*/
|
|
27
47
|
export const shikiTransformers: ShikiTransformer[] = [
|
|
28
48
|
transformerNotationHighlight({matchAlgorithm: "v3"}),
|
|
@@ -31,4 +51,5 @@ export const shikiTransformers: ShikiTransformer[] = [
|
|
|
31
51
|
transformerNotationWordHighlight({matchAlgorithm: "v3"}),
|
|
32
52
|
transformerMetaHighlight(),
|
|
33
53
|
lineNumbers,
|
|
54
|
+
tone,
|
|
34
55
|
];
|
|
@@ -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(
|
|
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,60 @@ test("code notation marks lines, meta highlights lines, and line-numbers tags th
|
|
|
96
96
|
}
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
+
test("code snippets import a file or a region from inside the document root and pass fence meta", async () => {
|
|
100
|
+
const root = await mkdtemp(join(tmpdir(), "for-humanity-snippets-"));
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
await mkdir(join(root, "snippet"));
|
|
104
|
+
await writeFile(
|
|
105
|
+
join(root, "snippet", "example.ts"),
|
|
106
|
+
"const outside = 0;\n// #region picked\n const a = 1; // [!code error]\n const b = 2;\n// #endregion picked\nconst after = 3;\n",
|
|
107
|
+
);
|
|
108
|
+
await writeFile(
|
|
109
|
+
join(root, "guide.md"),
|
|
110
|
+
"---\nname: Guide\nlabel: Guide\ngroup: Guide\n---\n\n## Read\n\n<<< ./snippet/example.ts#picked bad line-numbers\n\n<<< ./snippet/example.ts\n",
|
|
111
|
+
);
|
|
112
|
+
const site = siteConfigSchema.parse({});
|
|
113
|
+
const snippets = new Set<string>();
|
|
114
|
+
const [doc] = await readDocs({root, processor: createProcessor({site, root, snippets})});
|
|
115
|
+
|
|
116
|
+
assert.ok(doc);
|
|
117
|
+
assert.match(doc.html, /<pre class="[^"]*wg_prose__lineNumbers[^"]*wg_prose__code--bad[^"]*"|<pre class="[^"]*wg_prose__code--bad[^"]*wg_prose__lineNumbers[^"]*"/);
|
|
118
|
+
assert.match(doc.html, /<span class="line highlighted error">[^\n]*const<\/span>[^\n]* a/);
|
|
119
|
+
assert.equal(doc.html.includes("#region"), false, "region marker lines are removed");
|
|
120
|
+
assert.equal(doc.html.split("const</span>").length - 1, 2 + 4, "the region block has two lines and the whole-file block four");
|
|
121
|
+
assert.deepEqual(
|
|
122
|
+
[...snippets].map((entry) => entry.endsWith(join("snippet", "example.ts"))),
|
|
123
|
+
[true],
|
|
124
|
+
);
|
|
125
|
+
} finally {
|
|
126
|
+
await rm(root, {recursive: true, force: true});
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("code snippets reject files outside the document root, missing files and missing regions", async () => {
|
|
131
|
+
const root = await mkdtemp(join(tmpdir(), "for-humanity-snippets-reject-"));
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
await mkdir(join(root, "docs"));
|
|
135
|
+
await writeFile(join(root, "outside.ts"), "const x = 1;\n");
|
|
136
|
+
await writeFile(join(root, "docs", "present.ts"), "const y = 2;\n");
|
|
137
|
+
const site = siteConfigSchema.parse({});
|
|
138
|
+
const docsRoot = join(root, "docs");
|
|
139
|
+
|
|
140
|
+
for (const [reference, expected] of [
|
|
141
|
+
["../outside.ts", /문서 폴더 밖/],
|
|
142
|
+
["./missing.ts", /파일이 없다/],
|
|
143
|
+
["./present.ts#nope", /region 이 없다/],
|
|
144
|
+
] as const) {
|
|
145
|
+
await writeFile(join(docsRoot, "guide.md"), `---\nname: Guide\nlabel: Guide\ngroup: Guide\n---\n\n<<< ${reference}\n`);
|
|
146
|
+
await assert.rejects(readDocs({root: docsRoot, processor: createProcessor({site, root: docsRoot})}), expected, reference);
|
|
147
|
+
}
|
|
148
|
+
} finally {
|
|
149
|
+
await rm(root, {recursive: true, force: true});
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
99
153
|
test("cursor face is off unless the site turns it on", () => {
|
|
100
154
|
assert.equal(siteConfigSchema.parse({}).cursorFace, false);
|
|
101
155
|
assert.equal(siteConfigSchema.parse({cursorFace: true}).cursorFace, true);
|
package/src/entry/cli/cli.ts
CHANGED
|
@@ -92,7 +92,8 @@ const main = async () => {
|
|
|
92
92
|
...brand.files,
|
|
93
93
|
]);
|
|
94
94
|
const files = new Map(kitFiles);
|
|
95
|
-
const
|
|
95
|
+
const snippets = new Set<string>();
|
|
96
|
+
const processor = createProcessor({site: siteConfig, root: docsRoot, files, snippets});
|
|
96
97
|
const [docs, home] = await Promise.all([
|
|
97
98
|
readDocs({root: docsRoot, processor, reserved: siteConfig.url === undefined ? undefined : crawling_reserved_roots}),
|
|
98
99
|
readHome({root: docsRoot, processor}),
|
|
@@ -147,7 +148,8 @@ const main = async () => {
|
|
|
147
148
|
run: async () => {
|
|
148
149
|
const currentGeneration = generation;
|
|
149
150
|
const nextFiles = new Map(kitFiles);
|
|
150
|
-
const
|
|
151
|
+
const nextSnippets = new Set<string>();
|
|
152
|
+
const nextProcessor = createProcessor({site: siteConfig, root: docsRoot, files: nextFiles, snippets: nextSnippets});
|
|
151
153
|
const [docs, home] = await Promise.all([
|
|
152
154
|
readDocs({root: docsRoot, processor: nextProcessor, reserved: siteConfig.url === undefined ? undefined : crawling_reserved_roots}),
|
|
153
155
|
readHome({root: docsRoot, processor: nextProcessor}),
|
|
@@ -161,6 +163,10 @@ const main = async () => {
|
|
|
161
163
|
for (const entry of nextFiles) {
|
|
162
164
|
files.set(...entry);
|
|
163
165
|
}
|
|
166
|
+
snippets.clear();
|
|
167
|
+
for (const entry of nextSnippets) {
|
|
168
|
+
snippets.add(entry);
|
|
169
|
+
}
|
|
164
170
|
store.docs = docs;
|
|
165
171
|
store.home = home;
|
|
166
172
|
reload.emit("change");
|
|
@@ -171,12 +177,17 @@ const main = async () => {
|
|
|
171
177
|
);
|
|
172
178
|
|
|
173
179
|
/**
|
|
174
|
-
* 입력 변경 감지 · 출력·설치 폴더 제외 · 누락 자원 추가도 다시 확인
|
|
180
|
+
* 입력 변경 감지 · 출력·설치 폴더 제외 · 누락 자원 추가도 다시 확인 · 불러온 스니펫 파일 변경 포함
|
|
175
181
|
*/
|
|
176
182
|
const handleDocsChange = (_event: string, filename: string | null) => {
|
|
177
183
|
if (filename !== null) {
|
|
178
184
|
const segments = filename.split(sep);
|
|
179
|
-
|
|
185
|
+
const isSnippet = snippets.has(join(docsRoot, filename));
|
|
186
|
+
if (
|
|
187
|
+
segments.includes("dist") ||
|
|
188
|
+
segments.includes("node_modules") ||
|
|
189
|
+
(!isSnippet && !/\.mdx?$/i.test(filename) && !asset_media_extensions.has(extname(filename).toLowerCase()))
|
|
190
|
+
) {
|
|
180
191
|
return;
|
|
181
192
|
}
|
|
182
193
|
}
|
package/src/style/token.css
CHANGED
|
@@ -35,11 +35,13 @@
|
|
|
35
35
|
--app-color-accent: light-dark(#555, #b5b5b5);
|
|
36
36
|
--app-color-accent-soft: light-dark(#f5f5f2, #222);
|
|
37
37
|
|
|
38
|
-
/* 코드
|
|
38
|
+
/* 코드 판정 색 · soft 는 블록 바탕(메타 bad·good), mark 는 그 위의 줄 표기 · warn 은 줄 표기만 */
|
|
39
39
|
--app-color-bad: light-dark(#b2412e, #ff9b8a);
|
|
40
|
-
--app-color-bad-soft: light-dark(#
|
|
40
|
+
--app-color-bad-soft: light-dark(#f9ebe8, #3a2522);
|
|
41
|
+
--app-color-bad-mark: light-dark(#f1cdc6, #5b322c);
|
|
41
42
|
--app-color-good: light-dark(#2f7a47, #8bd3a3);
|
|
42
|
-
--app-color-good-soft: light-dark(#
|
|
43
|
+
--app-color-good-soft: light-dark(#e4f3ed, #1c342c);
|
|
44
|
+
--app-color-good-mark: light-dark(#c4e4d4, #27503f);
|
|
43
45
|
--app-color-warn: light-dark(#8a6400, #e6c46a);
|
|
44
46
|
--app-color-warn-soft: light-dark(#fbf3d9, #3d3418);
|
|
45
47
|
|
|
@@ -173,3 +175,12 @@
|
|
|
173
175
|
--app-space-gutter: 16px;
|
|
174
176
|
}
|
|
175
177
|
}
|
|
178
|
+
|
|
179
|
+
/* 블록 색 · 펜스 메타 bad·good · Shiki 가 pre 에 인라인으로 쓰는 코드 바탕을 바꿈 */
|
|
180
|
+
.wg_prose__code--bad {
|
|
181
|
+
--app-code-background: var(--app-color-bad-soft);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.wg_prose__code--good {
|
|
185
|
+
--app-code-background: var(--app-color-good-soft);
|
|
186
|
+
}
|