@chengzhao-dev/dsh-quarto-book 0.6.3 → 0.6.5
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/README.en.md +3 -3
- package/README.md +5 -3
- package/lib/index.js +49 -7
- package/lib/tools/pass-report.js +12 -12
- package/lib/tools/render-check.js +253 -65
- package/lib/workspace.js +1 -0
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +9 -6
- package/skills/designing-theme/SKILL.md +1 -1
- package/skills/designing-theme/assets/theme/css/content.css +31 -8
- package/skills/designing-theme/references/theme-spine.md +1 -1
- package/skills/designing-theme/scripts/checking-layout.py +11 -3
- package/skills/designing-theme/scripts/measuring-pages.mjs +2 -2
- package/skills/writing-quarto/SKILL.md +11 -2
- package/skills/writing-quarto/examples/03-block-after.md +29 -0
- package/skills/writing-quarto/examples/03-excerpt-lead.md +10 -0
- package/skills/writing-quarto/examples/03-qmd-elements.md +29 -2
- package/skills/writing-quarto/examples/04-paragraph-cases.md +13 -0
- package/skills/writing-quarto/examples/07-layout-list.md +34 -0
- package/skills/writing-quarto/quarto/authoring.md +5 -4
- package/skills/writing-quarto/quarto/basics.md +1 -0
- package/skills/writing-quarto/quarto/coords.md +2 -1
- package/skills/writing-quarto/quarto/figures.md +49 -0
- package/skills/writing-quarto/quarto/mermaid.md +2 -2
- package/skills/writing-quarto/quarto/refactor.md +1 -1
- package/skills/writing-quarto/quarto/render.md +4 -4
- package/skills/writing-quarto/quarto/tables.md +79 -0
- package/skills/writing-quarto/references/book-journey.md +38 -0
- package/skills/writing-quarto/references/token-budget.md +26 -0
- package/skills/writing-quarto/scripts/checking-callouts.py +2 -2
- package/skills/writing-quarto/scripts/checking-dom.py +12 -6
- package/skills/writing-quarto/scripts/checking-links.py +70 -13
- package/skills/writing-quarto/scripts/checking-names.py +3 -1
- package/skills/writing-quarto/scripts/checking-output.py +3 -3
- package/skills/writing-quarto/scripts/checking-prose.py +11 -7
- package/skills/writing-quarto/scripts/testing-content.py +68 -1
- package/skills/writing-quarto/scripts/verifying-content.py +187 -35
- package/skills/writing-quarto/steps/02-structure.md +1 -1
- package/skills/writing-quarto/steps/03-blocks.md +19 -6
- package/skills/writing-quarto/steps/04-paragraphs.md +1 -0
- package/skills/writing-quarto/steps/06-wording.md +17 -0
- package/skills/writing-quarto/steps/07-layout.md +5 -1
- package/skills/writing-quarto/steps/08-reread.md +1 -1
package/README.en.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
**English** | [简体中文](https://github.com/chengzhao-dev/dsh-quarto-book/blob/main/README.md)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Take a Quarto Book from chapters to publishable output: `writing-quarto` owns the chapter skeleton, whole-book order and Chinese prose; `designing-theme` owns the theme; after a render, `quarto_render_check` decides whether the HTML is publishable and `quarto_pass_report` prints the closing receipt. Both skills appear only inside `type: book` repositories, and the workflow needs Quarto CLI and Python 3.
|
|
9
9
|
|
|
10
10
|
## What it provides
|
|
11
11
|
|
|
@@ -16,7 +16,7 @@ Write Chinese technical chapters in a Quarto Book repository: `writing-quarto` o
|
|
|
16
16
|
| `quarto_render_check` | Check that `output-dir` is safe, render once when asked, and inspect HTML in `_book/` for publishing | Editing `.qmd`, fixing config automatically, or re-running a failed render |
|
|
17
17
|
| `quarto_pass_report` | Compress the run into a fixed-line receipt card: skill, mode, files and steps changed, items held, check verdicts | Saving to disk, quoting chapter prose, or changing the text based on the receipt |
|
|
18
18
|
|
|
19
|
-
Both skills are discovered from the package's `skills/` directory and loaded on demand. Relative paths inside a skill resolve against that skill's directory: `writing-quarto` uses `steps/`, `examples/`, `quarto/`, `scripts/`; `designing-theme` uses `references/`, `knowledge/`, `assets/`, `scripts/`.
|
|
19
|
+
Both skills are discovered from the package's `skills/` directory and loaded on demand. Relative paths inside a skill resolve against that skill's directory: `writing-quarto` uses `steps/`, `examples/`, `quarto/`, `references/`, `scripts/`; `designing-theme` uses `references/`, `knowledge/`, `assets/`, `scripts/`.
|
|
20
20
|
|
|
21
21
|
## Quick start
|
|
22
22
|
|
|
@@ -29,7 +29,7 @@ Both skills are discovered from the package's `skills/` directory and loaded on
|
|
|
29
29
|
2. Open a session in the book repository. `_quarto.yml` needs a standalone `type: book` line, and the book needs at least one `.qmd` or `.md` chapter source; only then do both skill names appear.
|
|
30
30
|
3. Call a skill: `writing-quarto` for prose, `designing-theme` for the theme.
|
|
31
31
|
|
|
32
|
-
Replace `--profile` with the profile you actually run. The desktop app always runs the `desktop` profile, and that profile is owned by the app, so `dsh --profile desktop` is rejected; install or upgrade with `dsh plugin --profile desktop add <package>`, and use the app's Settings → Plugins → Add plugin only if the app refuses that write. To pin a version, write `npm:@chengzhao-dev/dsh-quarto-book@0.6.
|
|
32
|
+
Replace `--profile` with the profile you actually run. The desktop app always runs the `desktop` profile, and that profile is owned by the app, so `dsh --profile desktop` is rejected; install or upgrade with `dsh plugin --profile desktop add <package>`, and use the app's Settings → Plugins → Add plugin only if the app refuses that write. To pin a version, write `npm:@chengzhao-dev/dsh-quarto-book@0.6.5`; to follow a GitHub tag, write `github:chengzhao-dev/dsh-quarto-book#v0.6.5`.
|
|
33
33
|
|
|
34
34
|
Upgrading from the old unscoped package `dsh-quarto-book` (removed from npm with 0.3.4, so this only affects profiles that installed it earlier): remove it from the same profile first with `dsh plugin --profile <profile> remove dsh-quarto-book`, then add the scoped package. Keeping both installed registers `writing-quarto` and `designing-theme` twice at the same rank 550.
|
|
35
35
|
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
[English](https://github.com/chengzhao-dev/dsh-quarto-book/blob/main/README.en.md) | **简体中文**
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
把一份 Quarto Book 从章节写到可发布:`writing-quarto` 负责章节骨架、整书次序与中文行文,`designing-theme` 负责主题;渲染后由 `quarto_render_check` 判断 HTML 是否可发布,`quarto_pass_report` 给出收尾回执。两个技能只在 `type: book` 的仓库里出现,运行需要 Quarto CLI 与 Python 3。
|
|
9
9
|
|
|
10
10
|
## 提供什么
|
|
11
11
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
| `quarto_render_check` | 先判断 `output-dir` 是否安全,再按需渲染一次,检查 `_book/` 里的 HTML 是否可发布 | 不改 `.qmd`,不自动修配置,不重复渲染 |
|
|
17
17
|
| `quarto_pass_report` | 收尾时把执行结论压成固定行数的回执卡片:技能、模式、改过的文件与步骤、未改项、检查结论 | 不保存到磁盘,不贴章节原文,不据回执改文章 |
|
|
18
18
|
|
|
19
|
-
两个技能从包内 `skills/` 目录发现,按需加载;技能里的相对路径以技能目录为基准:`writing-quarto` 用 `steps/`、`examples/`、`quarto/`、`scripts/`,`designing-theme` 用 `references/`、`knowledge/`、`assets/`、`scripts/`。
|
|
19
|
+
两个技能从包内 `skills/` 目录发现,按需加载;技能里的相对路径以技能目录为基准:`writing-quarto` 用 `steps/`、`examples/`、`quarto/`、`references/`、`scripts/`,`designing-theme` 用 `references/`、`knowledge/`、`assets/`、`scripts/`。
|
|
20
20
|
|
|
21
21
|
## 快速开始
|
|
22
22
|
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
2. 在书仓库里打开会话。`_quarto.yml` 要有独立一行的 `type: book`,书根至少有一个 `.qmd` 或 `.md` 章节源文件;两个技能名这时才会出现。
|
|
30
30
|
3. 调用技能:写正文用 `writing-quarto`,改主题用 `designing-theme`。
|
|
31
31
|
|
|
32
|
-
`--profile` 换成你实际运行的那个 profile。桌面应用固定跑 `desktop` profile,且该 profile 由应用独占管理,`dsh --profile desktop` 直接启动会被拒绝;安装或升级用 `dsh plugin --profile desktop add <包>`,应用拒绝写入时再走「设置 → 插件 → 添加插件」。需要固定版本时写 `npm:@chengzhao-dev/dsh-quarto-book@0.6.
|
|
32
|
+
`--profile` 换成你实际运行的那个 profile。桌面应用固定跑 `desktop` profile,且该 profile 由应用独占管理,`dsh --profile desktop` 直接启动会被拒绝;安装或升级用 `dsh plugin --profile desktop add <包>`,应用拒绝写入时再走「设置 → 插件 → 添加插件」。需要固定版本时写 `npm:@chengzhao-dev/dsh-quarto-book@0.6.5`,跟 GitHub 标签时写 `github:chengzhao-dev/dsh-quarto-book#v0.6.5`。
|
|
33
33
|
|
|
34
34
|
从旧的无作用域包 `dsh-quarto-book` 升级(该包已随 0.3.4 从 npm 删除,只影响之前装过它的 profile):先在同一个 profile 里 `dsh plugin --profile <profile> remove dsh-quarto-book`,再装新包。两个包同时留着会以相同的 rank 550 各注册一遍 `writing-quarto` 和 `designing-theme`。
|
|
35
35
|
|
|
@@ -87,10 +87,12 @@ python scripts/checking-names.py --root <book>
|
|
|
87
87
|
|
|
88
88
|
| 文档 | 内容 |
|
|
89
89
|
| --- | --- |
|
|
90
|
+
| [docs/architecture.md](https://github.com/chengzhao-dev/dsh-quarto-book/blob/main/docs/architecture.md) | 两个技能与两个工具的分层、四个写作域与文件职责 |
|
|
90
91
|
| [docs/plugin-flow.md](https://github.com/chengzhao-dev/dsh-quarto-book/blob/main/docs/plugin-flow.md) | 从安装到回执的五层结构、文件分流与渲染顺序 |
|
|
91
92
|
| [docs/plugin-boundaries.md](https://github.com/chengzhao-dev/dsh-quarto-book/blob/main/docs/plugin-boundaries.md) | 工作区检查的六类情形与判定顺序 |
|
|
92
93
|
| [docs/plugin-layout.md](https://github.com/chengzhao-dev/dsh-quarto-book/blob/main/docs/plugin-layout.md) | 仓库目录、发布包内容与 Python/Quarto 分工 |
|
|
93
94
|
| [docs/writing-quarto-layout.md](https://github.com/chengzhao-dev/dsh-quarto-book/blob/main/docs/writing-quarto-layout.md) | 写作技能的目录分层 |
|
|
95
|
+
| [docs/triggering.md](https://github.com/chengzhao-dev/dsh-quarto-book/blob/main/docs/triggering.md) | 两个技能的触发判据、四个写作域与正反例 |
|
|
94
96
|
| [docs/versioning.md](https://github.com/chengzhao-dev/dsh-quarto-book/blob/main/docs/versioning.md) | 版本号与发布顺序 |
|
|
95
97
|
|
|
96
98
|
## 开发
|
package/lib/index.js
CHANGED
|
@@ -24,15 +24,17 @@ const RANK = 550;
|
|
|
24
24
|
const SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
25
25
|
const PACKAGE_ROOT = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
26
26
|
const SKILLS_ROOT = path.join(PACKAGE_ROOT, 'skills');
|
|
27
|
-
/** 读取 SKILL.md
|
|
27
|
+
/** 读取 SKILL.md 实际用到的几个键,只做最小的 YAML frontmatter 解析。 */
|
|
28
28
|
function parseFrontmatter(source) {
|
|
29
|
-
const
|
|
29
|
+
const normalized = source.replace(/^\uFEFF/, '');
|
|
30
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(normalized);
|
|
30
31
|
if (!match)
|
|
31
|
-
return { data: {}, body:
|
|
32
|
+
return { data: {}, body: normalized };
|
|
32
33
|
const data = {};
|
|
33
34
|
const stack = [{ indent: -1, target: data }];
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
const lines = match[1].split(/\r?\n/);
|
|
36
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
37
|
+
const line = lines[index].replace(/\t/g, ' ');
|
|
36
38
|
if (!line.trim() || line.trimStart().startsWith('#'))
|
|
37
39
|
continue;
|
|
38
40
|
const indent = line.length - line.trimStart().length;
|
|
@@ -43,16 +45,56 @@ function parseFrontmatter(source) {
|
|
|
43
45
|
stack.pop();
|
|
44
46
|
const parent = stack[stack.length - 1].target;
|
|
45
47
|
const key = pair[1];
|
|
46
|
-
const value = (pair[2] ?? '').trim();
|
|
48
|
+
const value = stripInlineComment(pair[2] ?? '').trim();
|
|
47
49
|
if (value === '') {
|
|
48
50
|
const child = {};
|
|
49
51
|
parent[key] = child;
|
|
50
52
|
stack.push({ indent, target: child });
|
|
51
53
|
continue;
|
|
52
54
|
}
|
|
55
|
+
if (/^[>|][+-]?\d?$/.test(value)) {
|
|
56
|
+
const block = [];
|
|
57
|
+
let cursor = index + 1;
|
|
58
|
+
for (; cursor < lines.length; cursor += 1) {
|
|
59
|
+
const next = lines[cursor].replace(/\t/g, ' ');
|
|
60
|
+
if (!next.trim()) {
|
|
61
|
+
block.push('');
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const nextIndent = next.length - next.trimStart().length;
|
|
65
|
+
if (nextIndent <= indent)
|
|
66
|
+
break;
|
|
67
|
+
block.push(next);
|
|
68
|
+
}
|
|
69
|
+
const nonBlank = block.filter((item) => item.trim());
|
|
70
|
+
const cut = nonBlank.length > 0 ? Math.min(...nonBlank.map((item) => item.length - item.trimStart().length)) : 0;
|
|
71
|
+
const text = block.map((item) => item.slice(cut)).join('\n');
|
|
72
|
+
parent[key] = value[0] === '|' ? text : text.replace(/([^\n])\n(?=[^\n])/g, '$1 ').replace(/\n{2,}/g, '\n');
|
|
73
|
+
index = cursor - 1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
53
76
|
parent[key] = parseScalar(value);
|
|
54
77
|
}
|
|
55
|
-
return { data, body:
|
|
78
|
+
return { data, body: normalized.slice(match[0].length) };
|
|
79
|
+
}
|
|
80
|
+
/** 去掉 plain scalar 末尾的 ` # 注释`;引号内的 # 保留。 */
|
|
81
|
+
function stripInlineComment(value) {
|
|
82
|
+
let quote;
|
|
83
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
84
|
+
const char = value[index];
|
|
85
|
+
if (quote) {
|
|
86
|
+
if (char === quote && value[index - 1] !== '\\')
|
|
87
|
+
quote = undefined;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (char === '"' || char === "'") {
|
|
91
|
+
quote = char;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (char === '#' && (index === 0 || /\s/.test(value[index - 1])))
|
|
95
|
+
return value.slice(0, index);
|
|
96
|
+
}
|
|
97
|
+
return value;
|
|
56
98
|
}
|
|
57
99
|
function parseScalar(value) {
|
|
58
100
|
if ((value.startsWith('"') && value.endsWith('"')) ||
|
package/lib/tools/pass-report.js
CHANGED
|
@@ -74,6 +74,7 @@ function normalizeFiles(value) {
|
|
|
74
74
|
fail('files 必须是数组');
|
|
75
75
|
if (value.length > MAX_FILES)
|
|
76
76
|
fail(`files 最多 ${MAX_FILES} 个路径`);
|
|
77
|
+
const seen = new Set();
|
|
77
78
|
return value.map((item) => {
|
|
78
79
|
if (!isRecord(item))
|
|
79
80
|
fail('files 的每一项必须是 { path, edits }');
|
|
@@ -85,7 +86,11 @@ function normalizeFiles(value) {
|
|
|
85
86
|
fail('files 的 path 必须是非空字符串');
|
|
86
87
|
if (!isPlainCount(item.edits))
|
|
87
88
|
fail('files 的 edits 必须是非负整数');
|
|
88
|
-
|
|
89
|
+
const normalizedPath = filePath.trim();
|
|
90
|
+
if (seen.has(normalizedPath))
|
|
91
|
+
fail(`files 的 path 重复:${normalizedPath}`);
|
|
92
|
+
seen.add(normalizedPath);
|
|
93
|
+
return { path: normalizedPath, edits: item.edits };
|
|
89
94
|
});
|
|
90
95
|
}
|
|
91
96
|
function normalizeSteps(value, skill) {
|
|
@@ -194,17 +199,12 @@ export function buildPassReport(input) {
|
|
|
194
199
|
fail('未执行时必须给出 skip_reason');
|
|
195
200
|
if ([...reason.trim()].length > MAX_SKIP_REASON)
|
|
196
201
|
fail(`skip_reason 最多 ${MAX_SKIP_REASON} 个字`);
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (input.held !== undefined && Array.isArray(input.held) && input.held.length > 0) {
|
|
204
|
-
fail('未执行时 held 必须为空');
|
|
205
|
-
}
|
|
206
|
-
if (input.checks !== undefined && Array.isArray(input.checks) && input.checks.length > 0) {
|
|
207
|
-
fail('未执行时 checks 必须为空');
|
|
202
|
+
for (const key of ['files', 'steps', 'held', 'checks']) {
|
|
203
|
+
const value = input[key];
|
|
204
|
+
if (value === undefined)
|
|
205
|
+
continue;
|
|
206
|
+
if (!Array.isArray(value) || value.length > 0)
|
|
207
|
+
fail(`未执行时 ${key} 必须为空数组`);
|
|
208
208
|
}
|
|
209
209
|
return { receipt: `未执行 ${typedSkill}:${reason.trim()}` };
|
|
210
210
|
}
|