关于 d2-slides
d2-slides 是一款技能,可用 Markdown 编写幻灯片,并以 D2 图表构建 HTML。它适合需要可审阅、可重复生成的演示文稿作者,并提供渲染、文本、对比度、图表事实及读回效果检查。
主要功能
- 01用 Markdown 编写幻灯片
- 02构建 HTML 和打印视图
- 03提示正文溢出
- 04检查页面完整性与对比度
- 05依据事实表验证图表
- 06检查幻灯片文本的保真度与顺序
适用场景
- 01通过代码差异审阅演示文稿
- 02检查幻灯片裁切和溢出
- 03依据预期事实验证图表
- 04对照幻灯片文本检查读者读回内容
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mizchi/ explainer --skill d2-slides安装后在 Claude Code 中输入/d2-slides确认已加载。
附带文件assets/build-deck.mjsassets/deck-review.mjs
详细介绍
d2-slides
以下路径中:<skill> 是此技能的目录,<skills> 是技能安装所在的目录(在 mizchi/explainer 中为 skills/,在安装了这些技能的项目中为 .claude/skills/)。
一套幻灯片就是文本:正文是 Markdown,每张图都是一个 ```d2 围栏代码块,构建结果完全由文件决定。不需要拖拽,因此可以在差异中审阅幻灯片、在重命名后重新生成,而且——由于输出是 HTML 页面——还可以检查。最后这一点才是关键:幻灯片通常没有失败状态,所以没人会注意到项目符号被画面截掉了一半。
node <skill>/assets/build-deck.mjs deck.md --out built
# ✓ built/index.html: 8 slides, 5 figure(s) laid out by tala, 26 copy lines → built/copy.txt
# the deck: built/index.html → vlmkit check integrity
# all slides: built/print.html → vlmkit check copy --manifest, check a11y contrast, print to PDF
需要安装带 TALA 的 d2(d2 layout 必须列出 tala (bundled)——安装方法见 d2-diagram),除此之外无需任何依赖:构建器没有依赖项,幻灯片也不会加载网络资源。
幻灯片格式
---
title: Nothing tells you the picture is wrong
subtitle: two rounds, measured
date: 2026-09-14
---
# Nothing tells you the picture is wrong ← a lone `#` heading is the title slide
---
## A D2 diagram is a drawing ← `##` is a slide heading
- `d2 validate` reads **syntax** ← bullets; `code`, **bold**, *italic* work
- there is no `--expect`
```d2 ← the figure, laid out by TALA
direction: right
write -> check -> facts
```
← speaker notes: printed, never on screen
- 幻灯片之间用一行内容恰好为
---的分隔线隔开;围栏代码块中的---不算分隔线。 - 项目符号加图形会将两者并排显示;只有图形时会占据整个画布;只有标题时则是一张标题幻灯片。
> line是引用语句,其他语言的围栏代码块仍会作为代码块处理。 - 每张幻灯片都是固定的 1280×720 画面,会缩放以适应显示它的屏幕。在笔记本电脑和投影仪上显示时,内容不会重新排布,这也是为什么第 4 张幻灯片的屏幕截图在任何地方看起来都一样。
工作流程
1. write deck.md prose in Markdown, every figure a ```d2 fence
2. node …/build-deck.mjs deck.md --out built fails on the first figure that does not compile,
naming the slide; warns when a slide's prose
overflows its frame's character budget
3. vlmkit check integrity built/index.html a slide that breaks, clips, collides or paints nothing
4. vlmkit check integrity built/print.html --viewports 1280
every slide at once — this is what catches the
bullet the frame cut off
5. vlmkit check copy built/print.html --manifest built/copy.txt --allow-invisible unknown
every line of the deck's own text still on a slide
(add --forbid stale.txt when you are editing a deck:
the claim you removed must really be gone)
6. vlmkit check a11y contrast built/print.html readable from the back of the room
7. node …/d2-facts.mjs --from-svg built/slide-NN.svg --expect slide-NN.facts.json
what each figure DRAWS, for every figure that has
facts it must not get wrong — steps 3-6 are blind to it
8. node …/deck-review.mjs built --out review shoot each slide, hand the PNGs to a reader
node …/deck-review.mjs built --answers a.json score what it read back: fidelity, order, SPLITs
9. read built/index.html in a browser, or the PNG of a slide; fix the Markdown; go to 2
第 7 步不是可有可无的装饰,而且四项页面检查也不涵盖这一步。
它们检查的是页面:一张图即便能编译、能绘制,却画错了内容,也能通过全部四项检查。检查器会读取构建过程已生成的幻灯片 SVG,因此不需要 d2(它读取的是渲染结果),每张图只需运行一条命令:
node <skills>/d2-diagram/assets/d2-facts.mjs --from-svg built/slide-04.svg \
--expect slide-04.facts.json # exits 1 and names the box or edge that is wrong
同样的方法还能相互约束两张图,这是其他方法做不到的:如果第 2 张和第 3 张都声称 ledger 是写入方,就让两张 SVG 都符合一份事实表({"boxes": ["checkout","ledger"], "deps": ["checkout->ledger"], "forbidden": ["checkout->orders"]})。更新了一张图,却忘了另一张,这份事实表就会因你漏掉的那张图而检查失败。事实表的架构属于 d2-diagram;设置 exhaustive: true 后,多余的方框就会报错,而不是被忽略。
第 8 步:总得有人读一遍
第 3–7 步处理的是页面和图形的几何布局。它们都不会像观众那样阅读幻灯片,而这种差距并非纸上谈兵:在 v1/v2 轮次中,有两个缺陷进入了交付的演示文稿——一个句子被渲染成两个文本块,还有一个片段出现在它所属的项目符号上方——而且所有四道检查都通过了,因为每个片段都是可见文本,也没有任何内容溢出。
node <skill>/assets/deck-review.mjs built --out review
# → review/slide-NN.png, one per slide, plus review-brief.md
# hand that directory to a vision model or an agent; it answers JSON
node <skill>/assets/deck-review.mjs built --answers review/answers.json
# read 19/21, invented 6, fidelity 0.70, order 1.00
# ⚠ SPLIT: "…it calls ledger, which is now the only writer to orders."
# every word is on the slide, the sentence is not — read back as 2 blocks
评分是确定性的,依据的是构建时写入的 slides.json:每张幻灯片按阅读顺序排列的文本(copy.txt 不是按此顺序——清单是一个集合,而且项目符号列在段落之前,而页面上段落先于项目符号显示),再加上图形绘制的文字。
需要关注两个数字和一行提示:
- 保真度——读到的原文行数 /(原文行数 + 虚构内容)。1.00 表示“与幻灯片一致,没有多余内容”。在 v3 中测得:两个读取器对无缺陷演示文稿的保真度都是 1.00;同一演示文稿由一个拆分了项目符号的构建器生成时,保真度为 0.70。
- 顺序——阅读顺序与原文顺序不一致的文本行对数。它用于检测真正的顺序错乱幻灯片;请注意,它未能发现 v3 的拆分,因为片段仍保持着相对于原文的顺序。保真度和 SPLIT 行发现了这个问题。
- ⚠ SPLIT——某一原文行没有任何单条读取行能覆盖,但其中的词语都分散在两条或更多读取行中。这表示一个句子被渲染成了两个文本块。无论读取器是否有对应的词来描述问题,工具都会指出它:某个 v3 读取器把同一个缺陷称为“右边缘被裁切”——幻灯片没错,但判断的机制错了——诊断结果仍然识别出了这个问题。
--sheet <other-build>/slides.json 会将一次渲染结果与另一个构建版本的原文进行评分,用于将前后版本与同一份基准原文进行比较。
请诚实看待其局限性。读取器没有转录任何图形标签,并不能证明标签难以辨认,只能说明它读取了正文——figures: k of n 这一行是提示,不是结论。读取结果是一种测量,进程退出码为 0;如何处理 0.7,由作者决定。读取器偶尔也会报告并非真正原因的机制,因此在相信它给出的原因之前,先阅读它指出的幻灯片。
完成条件
- 构建进程以 0 退出,且没有溢出警告。
check integrity对index.html(全部三个视口)和 1280 宽度下的print.html均报告无失败。warn不算失败,命令仍会以 0 退出——在花一轮时间追查之前,请先看下面关于图形内部结构的说明。check copy --manifest报告没有缺失内容(如果你在编辑现有演示文稿,也没有被禁止的内容)。check a11y contrast报告无失败,且读取数量足以表明每张幻灯片都已读取。数量取决于内容——内容简短的演示文稿每张约有 7 个元素,内容密集的演示文稿每张约有 17 个——因此数量本身不是检查标准。也要对index.html运行检查:它一次只显示一张幻灯片,因此结果会是个位数。打印视图 ≫ 演示文稿视图才是检查重点。- 每张包含不得出错事实的图形,都通过了
d2-facts --expect检查。 - 有人读过了。 你亲自翻看了整份演示文稿,或有读取器使用
deck-review进行了评分——保真度接近 1.00,且没有SPLIT。只有这一步能发现一个句子被拆成两个文本块的问题。
CI 中的同一循环
每次修改技能、示例或它所使用的三个门禁时,此仓库都会让自己的完整示例走一遍该循环,因此上述说法是经过测量验证的,而非仅仅断言。分为两部分,因为它们需要不同的机器:
tests/d2/d2-slides.test.mjs(在常规测试套件中运行,不需要d2,也不需要浏览器)——绘制图形前构建器所做的决策:幻灯片在哪里结束(代码围栏内的---不是分隔符)、幻灯片采用哪种布局、以渲染后文本作为清单、指出幻灯片名称的溢出警告、未能编译的图形会导致构建失败,以及examples/d2-slides/built/仍与其deck.md一致。D2指向一个会回显固定 SVG 的存根——构建器已经会读取该变量。.github/workflows/d2-slides.yml(固定为v0.9.0的真实d2、真实 Chromium)——在全新构建上运行上述四个门禁,另加与已提交构建的字节级比较,以及两个负向对照:清单中一行未在演示文稿中出现的文本必须报告为copy-missing,缩短到 260px 的画框必须报告为clipped-content。如果测试套件只看到通过,就无法区分真正起作用的门禁和永远放行的门禁。
编辑示例的 deck.md 后,使用 npm run deck:example 重新生成已提交的示例——字节级比较可以防止已提交的构建逐渐偏离事实。
门禁在此技能自己的演示文稿中发现的问题
以下都是模板或示例中的真实缺陷,由门禁发现、修复并重新测量。这正是上述循环采用该顺序的原因。
- 手机上什么都看不到。 在 375px 下运行
check integrity:"DOM 中有 4 个文本块,但几乎没有内容被绘制出来(墨迹比例为 0.05%)"。将 1280px 的舞台作为超大网格项居中,会导致中间部分位于屏幕外;现在通过变换来居中舞台。 - 每张幻灯片都被裁掉了 114px。 固定盒子带内边距时,百分比高度并不等于盒子的高度:画框现在是弹性列布局,正文是弹性子项。对于文字明明完全位于画框内的幻灯片,门禁却报告为
clipped-content。 - 较长的项目符号列表上下都被截断。 分栏布局设置
align-items: center时,高度超过画框的正文会向上下溢出,而文案门禁会把这些行视为用户看不到的内容。现在溢出会向下延伸,便于测量;如果图形旁的正文超过约 430 个字符,构建器会发出警告。 - 清单与渲染结果不一致。 清单根据渲染后的标记生成,而非剥除标记后的 Markdown:包含一行
```d2strips to something the page never says.
What the copy manifest covers
copy.txt is generated from the Markdown prose only — each slide's
heading, bullets, pull quotes and paragraphs, as the page renders them
(markers gone, entities decoded). Three things are therefore not in it:
- A figure's labels. Text inside a ```d2 fence is drawn as SVG, and the manifest never contains it. So a string the deck must be seen to say belongs in prose; if it only appears in a figure, the copy gate cannot vouch for it — hold the figure to a fact sheet instead (step 7).
- Speaker notes. `` is printed and never on screen, and it is not in the manifest either.
- The chrome. The footer's deck title and page number are not manifest lines.
For a reader, the build also writes slides.json: per slide, its heading,
its text in reading order, its figures and the words those figures draw. That
is the sheet deck-review scores against, and the reason it is a separate file
from copy.txt is that a manifest cannot express order.
A bullet, quote or paragraph wrapped over several lines is rejoined into one line, as Markdown does, so wrapping at 80 columns is safe and one sentence is one manifest line. A blank line ends the block: an indented line after one is a paragraph of its own.
Failure modes
✗ slide 6-1: the d2 figure does not compilewithreserved keywords are prohibited in edges→ an id collided with a D2 keyword.width,height,label,style,shape,icon,near,top,left,direction,class,link,constraint,layers,stepsandscenariosare the ones that bite; rename the box (cols,verdict) and keep the words in its label.copy-invisible (reason: unknown)on a line that is plainly on the slide → a vlmkit gate limitation, not your deck: a manifest line whose rendered text is assembled from several inline children can read as invisible. Reproduced minimally inexamples/d2-slides/README.md; three bullets carrying<code>and<strong>fail, the same three lines without markup pass. Run with--allow-invisible unknown(every accepted line is listed, so the suppression stays auditable) and check themissingcount, which is the part that matters.check a11y contrastinspected only four elements → you pointed it atindex.html. One slide is on screen at a time;print.htmlis the stacked view. Do not read the absolute count as a threshold: it tracks how many text nodes the deck has, so a terse deck legitimately sits near 7 a slide. The comparison between the two views is the signal.near-misalignment(a warn) on a base64-looking selector likeg.KGdhdGV3YXkgLSZndDsgY2hlY2tvdXQpWzBd→ that is inside a figure's SVG: the gate has found two of TALA's own edge-label groups sitting 5.5px apart, which is the layout engine's business and not the deck's. It is awarn, so the verdict readsNO DEFECTS, n WARNand the command exits 0 — that is done, leave it. There is no stable way to exempt it either:--allowmatches the finding's own selector by substring, and d2 derives that class from the edge's text, so the string changes with the label. If a run has to be silent,--rule near-misalignment=offis the only lever, and it gives up the rule for the slide chrome too. Whether it fires at all depends on the layout, not on having labelled edges: the same deck's figures can be CLEAN after an edit.page-overflow-xonprint.htmlat 768 or 375 → expected: the print view is a column of 1280px frames. Check it at--viewports 1280.- A figure is enormous or unreadable → it is a D2 diagram like any other:
take it to
d2-diagram, where the width levers, the fact check and the terminal render live. A slide figure has about 700×560 to live in. - CJK labels render correctly in SVG, so a Japanese deck is fine; the
terminal-render caveats in
d2-diagramdo not apply here.
Deliver
- The deck:
built/index.html, opened in a browser. Arrows and space move,ois the overview grid,pprints,fis fullscreen, and the URL carries#/4, so one slide can be linked. - A PDF: open
print.htmland print it, orindex.htmland pressp. Speaker notes appear in print and never on screen. - In a repo: commit
deck.mdand the figures'.svg; the HTML is a build output.examples/d2-slides/is the worked example — its deck, its build, and the gate runs above. - One slide as an image: the figure is already
built/slide-NN.svg.