typography and documents: formula and diagram SVG for export; a diagram label without a font is an error
CI / platforms (windows-latest) (push) Failing after 8s
CI / site (push) Successful in 22s
CI / crate (push) Failing after 12m32s
CI / platforms (ubuntu-24.04) (push) Canceled after 0s

This commit is contained in:
ZacharyZhang-NY committed 2026-10-12 09:56:38 -04:00
1 parent 7ed4ea8323
commit cec9689f26
5 files changed
+100 -3

No files matched your search

+78 -1
View File
@@ -1,6 +1,6 @@
use std::{
panic::{AssertUnwindSafe, catch_unwind},
sync::Arc,
sync::{Arc, OnceLock},
};
use gpui::{
@@ -95,6 +95,72 @@ fn svg(chart: &Chart) -> Result<String, SharedString> {
}
}
/// `source` as light SVG of plain paths, for export.
pub fn diagram_svg(source: &str) -> Result<String, SharedString> {
let chart = Chart {
source: source.to_owned().into(),
dark: false,
ground: "#ffffff".into(),
ink: "#000000".into(),
scale: 100,
};
let drawn = svg(&chart)?;
// Markers and text become paths any reader draws.
let options = usvg::Options {
fontdb: fonts(),
..usvg::Options::default()
};
let tree = usvg::Tree::from_data(drawn.as_bytes(), &options)
.map_err(|error| SharedString::from(format!("{error}")))?;
// A label without a font vanishes: say so.
let (written, kept) = (labels(&drawn)?, texts(tree.root()));
if kept < written {
log::error!("diagram: {kept} of {written} labels found a font");
return Err("no installed font could set this diagram's labels".into());
}
Ok(tree.to_string(&usvg::WriteOptions::default()))
}
/// The `<text>` elements holding words.
fn labels(svg: &str) -> Result<usize, SharedString> {
let doc = usvg::roxmltree::Document::parse(svg)
.map_err(|error| SharedString::from(format!("{error}")))?;
let worded = |node: &usvg::roxmltree::Node| {
node.descendants()
.any(|word| word.text().is_some_and(|text| !text.trim().is_empty()))
};
Ok(doc
.descendants()
.filter(|node| node.has_tag_name("text") && worded(node))
.count())
}
/// Text nodes usvg set.
fn texts(group: &usvg::Group) -> usize {
group
.children()
.iter()
.map(|node| match node {
usvg::Node::Group(inner) => texts(inner),
usvg::Node::Text(_) => 1,
_ => 0,
})
.sum()
}
/// The system's fonts, read once.
fn fonts() -> Arc<usvg::fontdb::Database> {
static FONTS: OnceLock<Arc<usvg::fontdb::Database>> = OnceLock::new();
FONTS
.get_or_init(|| {
let mut fonts = usvg::fontdb::Database::new();
fonts.load_system_fonts();
log::info!("diagram: {} system font faces for export", fonts.len());
Arc::new(fonts)
})
.clone()
}
/// `#rrggbb` of a theme color.
fn hex(color: Hsla) -> SharedString {
let rgb = color.to_rgb();
@@ -182,6 +248,17 @@ mod tests {
assert!(!why.is_empty());
}
#[test]
fn an_exported_diagram_is_plain_paths() {
let svg = diagram_svg(
"flowchart LR
A[Start] --> B",
)
.expect("draws");
assert!(svg.contains("<path"), "{svg}");
assert!(!svg.contains("<marker") && !svg.contains("<text"), "{svg}");
}
#[test]
fn colors_write_as_hex() {
assert_eq!(hex(gpui::rgb(0x2c2c2c).into()), "#2c2c2c");
+1 -1
View File
@@ -33,7 +33,7 @@ pub(crate) use blocks::source;
pub use blocks::{Align, BlockData, BlockEditor, BlockEvent, BlockKind, Media};
pub use contents::outline;
pub use counts::{ReadingTime, WordCount};
pub use diagram::Diagram;
pub use diagram::{Diagram, diagram_svg};
pub use document::{
Attachment, DocumentEvent, DocumentHeading, DocumentLook, LARGEST_READ, MarkdownDocument,
TableEdit, TableRefusal,
+12
View File
@@ -116,6 +116,18 @@ impl Asset for Typesetter {
}
}
/// `source` as black outlined SVG, for export.
pub fn formula_svg(source: &str, display: bool) -> Result<String, MathError> {
let formula = Formula {
source: source.to_owned().into(),
display,
rgba: 0x0000_00ff,
font: 1600,
scale: 100,
};
svg(&formula).map(|(svg, _)| svg)
}
/// The formula as outlined SVG holding all ink; its size.
fn svg(formula: &Formula) -> Result<(String, Size<Pixels>), MathError> {
let ast = parsed(&formula.source)?;
+8
View File
@@ -177,3 +177,11 @@ fn a_shared_formula_stays_until_its_last_holder_leaves(cx: &mut gpui::TestAppCon
flush(cx);
assert!(!cached("c^2", cx), "the last holder freed it");
}
#[test]
fn an_exported_formula_is_black_outlines() {
let svg = formula_svg(r"\sqrt{x}", true).expect("typesets");
assert!(svg.starts_with("<svg") && !svg.contains("<text"), "{svg}");
assert!(svg.contains("0,0,0"), "black: {svg}");
assert!(formula_svg(r"\frac{", false).is_err());
}
+1 -1
View File
@@ -26,7 +26,7 @@ pub use formatted::{
pub use inline::{Blockquote, Code, Highlight, Kbd, KbdCombo};
pub use link::{ExternalLink, Link};
pub(crate) use math::failed;
pub use math::{Latex, MathError};
pub use math::{Latex, MathError, formula_svg};
pub use rolling::AnimatedNumber;
pub use select::SelectableText;
pub use text::{Caption, Heading, Label, Overline, Paragraph, Subtitle, Title, literal, tabular};