SVG
把图表渲染成 SVG,用 CSS 变量设置样式,处理不可信的输入。
SVG 后端 limu::scene::svg svg 把场景渲染成 SVG 文档,效果与 Pele 绘制的一致。和 limu 的其他部分一样,它只用到标准库。
1use limu::scene::style::{Look, Theme};2use limu::scene::svg;34let scene = limu::layout(src, &Default::default())?;5let out = svg::render(6 &scene,7 &svg::Options {8 look: Look::new(Theme::KOHU),9 font_family: Some("Arial, Helvetica, sans-serif".into()),10 ..Default::default()11 },12);1sc, err := limu.Layout(src, nil)2if err != nil {3 return err4}5out := svg.Render(sc, &svg.Options{6 Look: style.Look{Theme: style.Kohu()},7 FontFamily: "Arial, Helvetica, sans-serif",8})选项
| 字段 | 作用 |
|---|---|
look Look | 设置主题和覆盖。参见主题。 |
literal Literal | 把主题颜色写成普通颜色,而不是 CSS 变量。 |
font_family, mono_family FontFamily, MonoFamily | 指定测量场景时所用的字体。默认是 sans-serif 和 monospace。 |
link_schemes, image_schemes LinkSchemes, ImageSchemes | 设置链接和图片在相对 URL 之外可以使用的 URL scheme。参见不可信的图表。 |
icon_markup IconMarkup | 按名称返回图标的 SVG 代码。参见图标。 |
render_with Draw | 代替默认方式写出每个图元。在 Rust 中,它是一个单独的函数,而不是选项。参见自定义绘制。 |
字体
除非你提供别的测量器,布局都用 Arial 的宽度测量文本。SVG 必须使用测量文本时的字体,否则标签会放不进它的形状。使用默认测量器时,就用 Arial 或宽度相同的字体。
1svg::render(&scene, &svg::Options {2 font_family: Some("Arial, Helvetica, sans-serif".into()),3 ..Default::default()4})1svg.Render(sc, &svg.Options{FontFamily: "Arial, Helvetica, sans-serif"})如果页面设置了 --limu-font 或 --limu-font-mono,它们优先生效。用自己的字体测量文本的页面,可以用这两个变量让 SVG 也使用该字体。
主题变量
SVG 把每种主题颜色写成 CSS 变量,并以主题的颜色作为回退值,比如 var(--limu-surface,#e5eeec)。SVG 内联在页面中时,页面设置的变量优先。这样页面无需重新渲染图表就能改变颜色,比如在浅色和深色主题之间切换。
1.diagram {2 --limu-bg: #f1f6f5;3 --limu-fg: #0f262a;4 --limu-line: #23393d;5 --limu-surface: #e5eeec;6 --limu-border: #76939a;7 --limu-accent: #a92e4a;8}910@media (prefers-color-scheme: dark) {11 .diagram {12 --limu-bg: #0c1618;13 --limu-fg: #d0dddc;14 --limu-line: #b4c7c7;15 --limu-surface: #142428;16 --limu-border: #557176;17 --limu-accent: #ee8494;18 }19}| 变量 | 令牌 |
|---|---|
--limu-bg | bg Bg |
--limu-fg | fg Fg |
--limu-muted | muted Muted |
--limu-line | line Line |
--limu-surface | surface Surface |
--limu-surface-alt | surface_alt SurfaceAlt |
--limu-border | border Border |
--limu-accent | accent Accent |
--limu-critical | critical Critical |
--limu-series-1 … --limu-series-8 | series(0) … series(7) Series(0) … Series(7) |
--limu-font, --limu-font-mono | 上文介绍的字体 |
主题列出了 limu 各主题的取值。不来自主题的颜色,比如图表作者设置的颜色,会写成普通颜色,不受变量影响。
通过 <img> 显示或保存成文件的 SVG 看不到页面的变量,所以使用回退值。如果读取 SVG 的程序不支持 CSS 变量,就设置 literal Literal。
类名
每个图元所在的分组都带有描述它的类名,页面的 CSS 可以据此选中它们。
| 类名 | 元素 |
|---|---|
limu, limu-<type> | 根 <svg>,比如 limu-flowchart |
limu-node, limu-group, limu-edge, limu-text, limu-mark | 图元的分组,按种类区分 |
limu-<role> | 图元的分组,按角色区分,角色中的点换成短横线,比如 limu-flowchart-node |
limu-shape-<shape> | 图元的分组,按形状区分,比如 limu-shape-diam |
| 图元的 tag 和类 | 图表记录的额外信息,比如甘特图任务的 done,以及作者指定的类 |
limu-marker | 边端点的标记,比如箭头 |
limu-icon | 图标 |
有 id 的图元还会带上 data-id 属性。
1.limu-flowchart-node[data-id="scene"] rect {2 stroke: var(--limu-accent);3 stroke-width: 2;4}根元素的 width、height 和 viewBox 与场景的尺寸一致。要让它在窄页面上缩小,就给它设置 max-width: 100% 和 height: auto。
为了辅助技术,图表的 accTitle 和 accDescr 会成为它的 <title> 和 <desc>,根元素的角色是 graphics-document。
图标
标签中可以包含 fa:fa-car 这样的图标,流程图的节点也可以显示图标。图标代码选项 icon_markup IconMarkup 返回放进图标 <svg> 中的 SVG 代码,坐标系是 24 × 24 的网格。这段代码应该用 currentColor 绘制,它被设为图元的文本颜色。布局只为你通过布局的图标选项 layout::Options::icons layout.Options.Icons 报告的图标留出空间,所以两处要给同一组图标。
1use std::collections::HashMap;23let icons = HashMap::from([(4 "fa:fa-car",5 r#"<path d="M5 17h14v-5l-2-5H7l-2 5z" fill="currentColor"/>"#,6)]);7let has = |name: &str| icons.contains_key(name);8let scene = limu::layout(9 src,10 &limu::layout::Options { icons: Some(&has), ..Default::default() },11)?;12let out = svg::render(13 &scene,14 &svg::Options {15 icon_markup: Some(Box::new(|name| icons.get(name).map(|m| m.to_string()))),16 ..Default::default()17 },18);1icons := map[string]string{2 "fa:fa-car": `<path d="M5 17h14v-5l-2-5H7l-2 5z" fill="currentColor"/>`,3}4sc, err := limu.Layout(src, &layout.Options{5 Icons: func(name string) bool { _, ok := icons[name]; return ok },6})7out := svg.Render(sc, &svg.Options{8 IconMarkup: func(name string) string { return icons[name] },9})没有图标代码时,图标会被省略。架构图和树视图还有内置图标,不需要提供代码。
自定义绘制
svg::render_with 用你的函数代替默认方式写出每个图元。函数会收到一个 Painter、图元和它解析后的样式。painter 的 item 按默认方式写出图元,parts、ends、picture 和 label 写出默认的各个部分。它的画布 canvas() 是 svg::Writer,用 raw 写入你自己的 SVG 代码,用 escaped 写入转义后的文本,用 num 写入数字,用 path_data 写入路径。解析后的样式保留了每种颜色的来源,所以 paint 和 paint_attr 仍然能把主题颜色写成 CSS 变量。
Draw 代替默认方式写出每个图元,next 按默认方式写出它。它收到的 svg.Writer 可以用 Item、Parts、Ends、Picture 和 Label 写出默认的各个部分,用 Raw 写入你自己的 SVG 代码,用 Text 写入转义后的文本,用 Num 写入数字。它收到的样式已经按外观层叠,但还没有解析成颜色,所以 Paint 和 PaintAttr 仍然能把主题颜色写成 CSS 变量。
1use limu::scene::Kind;23let o = svg::Options { look, ..Default::default() };4svg::render_with(&scene, &o, |p, it, s| {5 if !matches!(it.kind, Kind::Node) {6 p.item(it, s);7 return;8 }9 // 把节点画成我们自己的方框,标签按默认方式绘制。10 let b = it.bounds;11 let w = p.canvas();12 w.raw(r#"<rect rx="8" x=""#);13 w.num(b.x);14 w.raw(r#"" y=""#);15 w.num(b.y);16 w.raw(r#"" width=""#);17 w.num(b.w);18 w.raw(r#"" height=""#);19 w.num(b.h);20 w.raw(r#"""#);21 w.paint_attr("fill", &s.fill);22 w.raw("/>");23 p.label(it, s);24})1svg.Render(sc, &svg.Options{2 Look: look,3 Draw: func(w *svg.Writer, it *scene.Item, s style.Style, next func()) {4 if it.Kind != scene.Node {5 next()6 return7 }8 // 把节点画成我们自己的方框,标签按默认方式绘制。9 b := it.Box10 w.Raw(`<rect rx="8" x="`)11 w.Num(b.X)12 w.Raw(`" y="`)13 w.Num(b.Y)14 w.Raw(`" width="`)15 w.Num(b.W)16 w.Raw(`" height="`)17 w.Num(b.H)18 w.Raw(`"`)19 w.PaintAttr("fill", s.Fill)20 w.Raw(`/>`)21 w.Label(it, s)22 },23})不可信的图表
图表文本有时来自你不信任的人,比如用户在你的应用里分享的笔记。limu 会转义它写出的所有文本,从不写出脚本、事件处理器或 HTML。调用函数的 click 会被丢弃。其余的事由你的应用负责。
链接和图片
图表可以把某个部分链接到 URL,也可以显示来自 URL 的图片。只有当 URL 是相对路径,或者使用你允许的 scheme 时,布局才会保留链接或图片。
| 用途 | 默认允许的 scheme |
|---|---|
| 链接 | http、https、mailto、tel |
| 图片 | http、https |
javascript:、data: 和 vbscript: 始终被拦截,//host/share 这样的网络路径也是。显示 SVG 时,图片会发起网络请求。要丢弃所有链接或所有图片,在布局选项中设置 links: false images: false NoLinks NoImages。
scheme 会检查两次。第一次由布局按它的链接和图片 scheme 检查,第二次由 SVG 后端按它自己的选项做最后检查。要允许另一个 scheme,两处都要加上。
1let schemes = ["http", "https", "mailto", "obsidian"];2let scene = limu::layout(3 src,4 &limu::layout::Options { link_schemes: &schemes, ..Default::default() },5)?;6let out = svg::render(7 &scene,8 &svg::Options {9 link_schemes: Some(schemes.map(String::from).to_vec()),10 ..Default::default()11 },12);1schemes := []string{"http", "https", "mailto", "obsidian"}2sc, err := limu.Layout(src, &layout.Options{LinkSchemes: schemes})3out := svg.Render(sc, &svg.Options{LinkSchemes: schemes})链接写成不带 target 的 <a href>。SVG 内联时,链接如何打开由页面决定,和页面自己的链接一样。
限制
默认情况下,图表文本最长 50,000 个 UTF-16 码元,流程图和 Agent 流程图最多 5,000 条边。参见限制。布局一旦开始,limu 就无法中止,所以对不可信的文本要保留这些限制。
应用要负责的事
SVG 后端会原样插入图标的 SVG 代码。只从你信任的图标集中返回代码,未知的名称不返回任何内容,也不要把名称本身放进代码。
错误信息可能引用图表的文本,所以要把它当作纯文本显示,不要当作 HTML 或 SVG 解析。
limu 逐个属性读取作者的样式,并写成属性,同时丢弃可能引用图表之外内容的值,比如 url()。不要让图表作者设置你的选项,比如解析图表时用的配置或允许的 scheme。