和你的代理一起做主题
给制作 MarsDawn 预览主题的 AI 代理的参考。代理写出一个 theme.json,用 marsdawn 命令行工具检查并渲染,再把链接交给你。你在打造一个主题页面用眼睛调整,然后投稿。下面的字段表直接读自 kit 的源代码,并有检查确保两者一致。
写出一个 theme.json,反复运行 marsdawn theme validate theme.json --require-complete --json 直到退出代码为 0,用 marsdawn theme preview 分别渲染浅色与深色,再把链接交给用户。主题只有参数,不含任何 CSS。
主题是什么
- 颜色,浅色与深色各一组:基本色板,以及可选的语法与图表颜色。每个颜色都是
#RRGGBB。 - 字体设计:
sans、serif或rounded。 - 一组封闭的样式选项:数字落在固定范围内,选择来自固定列表。样式选项里的颜色一律是像
accent这样的色板角色,不能是颜色字面值。 - 显示字符串:名称与摘要,以及可选的作者与许可证。
不在下面表格里的键是错误,同一个键写两次也是。文件最大 16,384 字节。
检查与渲染
在放有 theme.json 的文件夹里运行这些命令。需要 marsdawn 工具:见给 AI 代理的 marsdawn 参考。
marsdawn theme validate theme.json --require-complete --json
marsdawn theme css theme.json --json
marsdawn theme preview theme.json --appearance light -o light.png
marsdawn theme preview theme.json --appearance dark -o dark.png
- 退出代码:0 表示有效,1 表示无效并列出规则 id,2 表示找不到文件,64 表示用法错误。
preview另外有 4(PNG 已存在,传入--force或换个文件名)和 5(渲染失败)。 - 加上
--json时,无效的主题会把问题列在issues下,每一项都有ruleid、path和message。修正后再运行一次。 --require-complete是要发布的主题用的检查:它还要求两组色板都有syntax与diagram颜色。css打印预览使用的 CSS,preview把范例文档渲染成 PNG。两者都会先验证,所以无效的主题什么都不会渲染。- 交出主题之前,先看过浅色与深色两张 PNG。
从内置主题开始
复制四个内置主题之一再修改。每一个都是完整且有效的文件:
/assets/theme-sim/kit/themes/dawn.json: 默认主题,也是下面的范例/assets/theme-sim/kit/themes/classic.json: 较素雅、像文档的色板/assets/theme-sim/kit/themes/modern.json: 较冷调、较现代的色板/assets/theme-sim/kit/themes/vivid.json: 较明亮、对比较高的色板
范例:Dawn
这是 dawn.json,一个字节都没改。它唯一的样式选项是 hr 的一条细的强调色分隔线。
{
"schemaVersion": 1,
"id": "dawn",
"version": "1.0.0",
"name": { "en": "Dawn" },
"summary": { "en": "Warm Martian sunrise" },
"fontDesign": "sans",
"scenarios": ["agent-review"],
"light": {
"background": "#FFFDFB", "surface": "#F6F0EC", "text": "#26211F", "muted": "#6F6660",
"border": "#EADFD8", "heading": "#26211F", "accent": "#C8471B", "link": "#B03C0C", "quote": "#D97A4A",
"syntax": { "keyword": "#B03C0C", "string": "#2F6F5E", "comment": "#746B66", "number": "#9A5B00", "function": "#5B3E8C", "type": "#1F6FB2" },
"diagram": { "node": "#FBE9E0", "nodeBorder": "#CA6C3C", "text": "#26211F", "line": "#8A6A5C", "secondary": "#E6F1EE", "tertiary": "#FFF6F1", "note": "#FFF1D6" }
},
"dark": {
"background": "#1C1A1F", "surface": "#262229", "text": "#EBE4DF", "muted": "#A39992",
"border": "#3A343A", "heading": "#F4EEEA", "accent": "#FF8A50", "link": "#FF9E6B", "quote": "#E08A5C",
"syntax": { "keyword": "#FF9E6B", "string": "#7FD1B9", "comment": "#938A84", "number": "#F2C572", "function": "#C7A6F5", "type": "#6CB6FF" },
"diagram": { "node": "#3A2A26", "nodeBorder": "#E08A5C", "text": "#EBE4DF", "line": "#B8A69C", "secondary": "#23332F", "tertiary": "#2B2427", "note": "#3D3322" }
},
"style": {
"hr": { "style": { "type": "line", "color": "accent", "thickness": 1 } }
}
}
字段
下面每个对象都用同样的方式读取。required 表示这个键必须存在,没有列出的键都是错误。
类型:string、integer、number、boolean、map(语言标签对应文字)、enum(列出的值之一)、role(色板角色)和 object(连到它的表格)。类型后面的 [] 表示列表。
theme.json
| 字段 | 类型 | 必填与否 | 值与备注 |
|---|---|---|---|
schemaVersion | integer | required | 固定是 1。 |
id | string | required | 1 到 32 个字符:小写字母与数字,以单一连字符连接成段,例如 calm-green。 |
version | string | required | MAJOR.MINOR.PATCH,例如 1.0.0。 |
name | map | required | 语言标签对应文字。en 必填,其他键是像 zh-Hant 的语言标签。每段文字最多 48 个字符,不可含链接或换行。 |
summary | map | required | 和 name 的形式相同,每段文字最多 120 个字符。 |
fontDesign | enum | required | sans, serif, rounded |
scenarios | enum[] | required | agent-review, technical-docs, formal-output, notes-sharing — 一个或两个,不可重复。它们决定主题必须达到多高的对比。 |
author | object | optional | author |
license | string | optional | SPDX 标识符,例如 Apache-2.0。投稿的主题以 Apache-2.0 发布。 |
light | object | required | light, dark |
dark | object | required | light, dark |
style | object | optional | style |
author
| 字段 | 类型 | 必填与否 | 值与备注 |
|---|---|---|---|
name | string | required | 最多 64 个字符,不可含链接。 |
github | string | optional | GitHub 账号。投稿时请填投稿者自己的账号。 |
light, dark
| 字段 | 类型 | 必填与否 | 值与备注 |
|---|---|---|---|
background | string | required | # 加六个十六进制数字,例如 #1C1C1C。 |
surface | string | required | # 加六个十六进制数字,例如 #1C1C1C。 |
text | string | required | # 加六个十六进制数字,例如 #1C1C1C。 |
muted | string | required | # 加六个十六进制数字,例如 #1C1C1C。 |
border | string | required | # 加六个十六进制数字,例如 #1C1C1C。 |
heading | string | required | # 加六个十六进制数字,例如 #1C1C1C。 |
accent | string | required | # 加六个十六进制数字,例如 #1C1C1C。 |
link | string | required | # 加六个十六进制数字,例如 #1C1C1C。 |
quote | string | required | # 加六个十六进制数字,例如 #1C1C1C。 |
syntax | object | optional (发布时必填,用 --require-complete 检查) | syntax |
diagram | object | optional (发布时必填,用 --require-complete 检查) | diagram |
syntax
| 字段 | 类型 | 必填与否 | 值与备注 |
|---|---|---|---|
keyword | string | required | # 加六个十六进制数字,例如 #1C1C1C。 |
string | string | required | # 加六个十六进制数字,例如 #1C1C1C。 |
comment | string | required | # 加六个十六进制数字,例如 #1C1C1C。 |
number | string | required | # 加六个十六进制数字,例如 #1C1C1C。 |
function | string | required | # 加六个十六进制数字,例如 #1C1C1C。 |
type | string | required | # 加六个十六进制数字,例如 #1C1C1C。 |
diagram
| 字段 | 类型 | 必填与否 | 值与备注 |
|---|---|---|---|
node | string | required | # 加六个十六进制数字,例如 #1C1C1C。 |
nodeBorder | string | required | # 加六个十六进制数字,例如 #1C1C1C。 |
text | string | required | # 加六个十六进制数字,例如 #1C1C1C。 |
line | string | required | # 加六个十六进制数字,例如 #1C1C1C。 |
secondary | string | required | # 加六个十六进制数字,例如 #1C1C1C。 |
tertiary | string | required | # 加六个十六进制数字,例如 #1C1C1C。 |
note | string | required | # 加六个十六进制数字,例如 #1C1C1C。 |
style
| 字段 | 类型 | 必填与否 | 值与备注 |
|---|---|---|---|
bodySize | number | optional | 范围 |
lineHeight | number | optional | 范围 |
headingWeight | number | optional | 范围 |
radius | number | optional | 范围 |
maxWidth | number | optional | 范围 |
h1 | object | optional | style.h1 |
h2 | object | optional | style.h2 |
blockquote | object | optional | style.blockquote |
hr | object | optional | style.hr |
table | object | optional | style.table |
listMarker | role | optional | 色板角色 |
inlineCode | role | optional | 色板角色 — 只能是 text 或语法角色:keyword、string、comment、number、function 或 type。 |
link | object | optional | style.link |
syntax | object | optional | style.syntax |
style.h1
| 字段 | 类型 | 必填与否 | 值与备注 |
|---|---|---|---|
size | number | optional | 范围 |
letterSpacing | number | optional | 范围 |
align | enum | optional | left, center |
decoration | object | optional | type |
style.h2
| 字段 | 类型 | 必填与否 | 值与备注 |
|---|---|---|---|
letterSpacing | number | optional | 范围 |
decoration | object | optional | type |
italic | boolean | optional | true, false |
style.blockquote
| 字段 | 类型 | 必填与否 | 值与备注 |
|---|---|---|---|
style | object | optional | type |
italic | boolean | optional | true, false |
style.hr
| 字段 | 类型 | 必填与否 | 值与备注 |
|---|---|---|---|
style | object | optional | type |
style.table
| 字段 | 类型 | 必填与否 | 值与备注 |
|---|---|---|---|
header | object | optional | type |
verticalRules | boolean | optional | true, false |
rounded | boolean | optional | true, false |
style.link
| 字段 | 类型 | 必填与否 | 值与备注 |
|---|---|---|---|
underline | boolean | optional | true, false |
style.syntax
| 字段 | 类型 | 必填与否 | 值与备注 |
|---|---|---|---|
boldKeywords | boolean | optional | true, false |
带 type 的选项
这些样式选项是带有 type 的对象。每个 type 只接受为它列出的字段,其他都不行。
| 选项 | type | 字段 | 必填与否 | 类型 |
|---|---|---|---|---|
style.h1.decoration | rule | — | — | — |
style.h1.decoration | none | — | — | — |
style.h1.decoration | shortRule | color | required | role |
style.h1.decoration | gradientBar | from | required | role |
style.h1.decoration | gradientBar | to | required | role |
style.h2.decoration | rule | — | — | — |
style.h2.decoration | none | — | — | — |
style.h2.decoration | dot | color | required | role |
style.blockquote.style | bar | width | optional | number 范围 |
style.blockquote.style | panel | — | — | — |
style.hr.style | line | color | optional | role |
style.hr.style | line | thickness | optional | number 范围 |
style.hr.style | shortCentered | color | required | role |
style.hr.style | gradient | colors | required | role[] 两个或三个角色。 |
style.table.header | surface | — | — | — |
style.table.header | accentRule | color | required | role |
style.table.header | filled | background | required | role |
style.table.header | filled | text | required | role |
style.table.header | filled | border | optional | role |
色板角色
色板角色是下列名称之一:
background, surface, text, muted, border, heading, accent, link, quote, keyword, string, comment, number, function, type
数字
数字会先对齐到最接近的间隔倍数,再检查是否落在最小值与最大值之间,否则规则是 number.range。
| 字段 | 最小值 | 最大值 | 间隔 |
|---|---|---|---|
style.bodySize | 14 | 18 | 1 |
style.lineHeight | 1.4 | 1.9 | 0.05 |
style.headingWeight | 400 | 900 | 50 |
style.radius | 0 | 16 | 1 |
style.maxWidth | 600 | 1000 | 1 |
style.h1.size | 1.8 | 2.4 | 0.1 |
style.h1.letterSpacing | -0.03 | 0.03 | 0.005 |
style.h2.letterSpacing | -0.03 | 0.03 | 0.005 |
style.blockquote.style.width | 2 | 4 | 1 |
style.hr.style.thickness | 1 | 4 | 1 |
还会检查什么
- 对比。预览画出的每一组颜色配对,例如
background上的text,文字要达到 4.5:1,线条与其他非文字要达到 3:1。你列出的场景会把部分颜色提高到 4.5:1。消息会指出是哪一组配对、它的比值和需要的比值。 - 字符串。名称、摘要或作者名称若是空的、太长,或含有链接、控制字符、不可见或双向格式字符,都会被拒绝。
- 规则 id。每个问题都带一个,例如
contrast.pair、color.hex、number.range、id.pattern、schema.unknownKey或palette.incomplete。按 id 与path的说明修正文件。
用链接交给人
打造一个主题页面可以从链接打开主题,让人用眼睛调整。链接是该页的网址,接着 #theme=,再接文件字节的 base64url(不含补位)。文件最大 16,384 字节。下面这一行命令在 macOS 与 Linux 的 zsh 和 bash 都能生成链接:
printf 'https://marsdawn.southern-light.dev/zh-hans/themes/new/#theme=%s\n' "$(base64 < theme.json | tr -d '\n=' | tr '+/' '-_')"
主题跟在 # 后面,浏览器不会把它发送给网站,网站也不会把它发送到任何地方。链接确实会留在你的 shell 历史与浏览器历史里,你贴给谁,谁就能读到这个主题。
投稿
代理准备文件与链接,由人来投稿。两条路都会要求 Developer Certificate of Origin 与 Apache-2.0 许可声明,这个声明由人本人做出:代理不会替他勾选方框或签署。
- Issue。人打开链接,在“打造一个主题”页面点按 Submit,或是打开 issue 表单,把 JSON 贴进 Theme。接着有工作流检查主题并回复。
- Pull request。只改
themes/<id>/theme.json,把author.github设为 pull request 作者的账号,每个 commit 都用git commit -s签署。规则在 CONTRIBUTING.md。
投稿主题的作者名称、名称、摘要与 GitHub 账号会永久公开,所以请把 author.name 设成人愿意公开的名字。
已发布的主题
想看已发布的主题长什么样子,可以逛逛主题图库。