From 9a6268050fb769e18c4889fa5f59d4150e8878d6 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 15 Jul 2025 16:48:31 +0200 Subject: [PATCH] HTML frame improvements (#6605) --- crates/typst-html/src/convert.rs | 5 +--- crates/typst-html/src/dom.rs | 10 ++++++- crates/typst-html/src/encode.rs | 11 ++------ crates/typst-svg/src/lib.rs | 48 +++++++++++++++++++++++++++++--- tests/ref/html/html-frame.html | 11 ++++++++ tests/suite/html/frame.typ | 5 ++-- 6 files changed, 70 insertions(+), 20 deletions(-) create mode 100644 tests/ref/html/html-frame.html diff --git a/crates/typst-html/src/convert.rs b/crates/typst-html/src/convert.rs index 171b4cb7e..817b0f025 100644 --- a/crates/typst-html/src/convert.rs +++ b/crates/typst-html/src/convert.rs @@ -109,10 +109,7 @@ fn handle( styles.chain(&style), Region::new(Size::splat(Abs::inf()), Axes::splat(false)), )?; - output.push(HtmlNode::Frame(HtmlFrame { - inner: frame, - text_size: styles.resolve(TextElem::size), - })); + output.push(HtmlNode::Frame(HtmlFrame::new(frame, styles))); } else { engine.sink.warn(warning!( child.span(), diff --git a/crates/typst-html/src/dom.rs b/crates/typst-html/src/dom.rs index cf74e1bfc..d7287d42d 100644 --- a/crates/typst-html/src/dom.rs +++ b/crates/typst-html/src/dom.rs @@ -2,10 +2,11 @@ use std::fmt::{self, Debug, Display, Formatter}; use ecow::{EcoString, EcoVec}; use typst_library::diag::{bail, HintedStrResult, StrResult}; -use typst_library::foundations::{cast, Dict, Repr, Str}; +use typst_library::foundations::{cast, Dict, Repr, Str, StyleChain}; use typst_library::introspection::{Introspector, Tag}; use typst_library::layout::{Abs, Frame}; use typst_library::model::DocumentInfo; +use typst_library::text::TextElem; use typst_syntax::Span; use typst_utils::{PicoStr, ResolvedPicoStr}; @@ -279,3 +280,10 @@ pub struct HtmlFrame { /// consistently. pub text_size: Abs, } + +impl HtmlFrame { + /// Wraps a laid-out frame. + pub fn new(inner: Frame, styles: StyleChain) -> Self { + Self { inner, text_size: styles.resolve(TextElem::size) } + } +} diff --git a/crates/typst-html/src/encode.rs b/crates/typst-html/src/encode.rs index be8137399..4447186b8 100644 --- a/crates/typst-html/src/encode.rs +++ b/crates/typst-html/src/encode.rs @@ -121,6 +121,7 @@ fn write_children(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> { let pretty_inside = allows_pretty_inside(element.tag) && element.children.iter().any(|node| match node { HtmlNode::Element(child) => wants_pretty_around(child.tag), + HtmlNode::Frame(_) => true, _ => false, }); @@ -305,14 +306,6 @@ fn write_escape(w: &mut Writer, c: char) -> StrResult<()> { /// Encode a laid out frame into the writer. fn write_frame(w: &mut Writer, frame: &HtmlFrame) { - // FIXME: This string replacement is obviously a hack. - let svg = typst_svg::svg_frame(&frame.inner).replace( - " String { renderer.finalize() } +/// Export a frame into an SVG suitable for embedding into HTML. +#[typst_macros::time(name = "svg html frame")] +pub fn svg_html_frame(frame: &Frame, text_size: Abs) -> String { + let mut renderer = SVGRenderer::with_options(xmlwriter::Options { + indent: xmlwriter::Indent::None, + ..Default::default() + }); + renderer.write_header_with_custom_attrs(frame.size(), |xml| { + xml.write_attribute("class", "typst-frame"); + xml.write_attribute_fmt( + "style", + format_args!( + "overflow: visible; width: {}em; height: {}em;", + frame.width() / text_size, + frame.height() / text_size, + ), + ); + }); + + let state = State::new(frame.size(), Transform::identity()); + renderer.render_frame(state, Transform::identity(), frame); + renderer.finalize() +} + /// Export a document with potentially multiple pages into a single SVG file. /// /// The padding will be added around and between the individual frames. @@ -158,8 +182,13 @@ impl State { impl SVGRenderer { /// Create a new SVG renderer with empty glyph and clip path. fn new() -> Self { + Self::with_options(Default::default()) + } + + /// Create a new SVG renderer with the given configuration. + fn with_options(options: xmlwriter::Options) -> Self { SVGRenderer { - xml: XmlWriter::new(xmlwriter::Options::default()), + xml: XmlWriter::new(options), glyphs: Deduplicator::new('g'), clip_paths: Deduplicator::new('c'), gradient_refs: Deduplicator::new('g'), @@ -170,11 +199,22 @@ impl SVGRenderer { } } - /// Write the SVG header, including the `viewBox` and `width` and `height` - /// attributes. + /// Write the default SVG header, including a `typst-doc` class, the + /// `viewBox` and `width` and `height` attributes. fn write_header(&mut self, size: Size) { + self.write_header_with_custom_attrs(size, |xml| { + xml.write_attribute("class", "typst-doc"); + }); + } + + /// Write the SVG header with additional attributes and standard attributes. + fn write_header_with_custom_attrs( + &mut self, + size: Size, + write_custom_attrs: impl FnOnce(&mut XmlWriter), + ) { self.xml.start_element("svg"); - self.xml.write_attribute("class", "typst-doc"); + write_custom_attrs(&mut self.xml); self.xml.write_attribute_fmt( "viewBox", format_args!("0 0 {} {}", size.x.to_pt(), size.y.to_pt()), diff --git a/tests/ref/html/html-frame.html b/tests/ref/html/html-frame.html new file mode 100644 index 000000000..f19ef4b88 --- /dev/null +++ b/tests/ref/html/html-frame.html @@ -0,0 +1,11 @@ + + + + + + + +

A rectangle:

+ + + diff --git a/tests/suite/html/frame.typ b/tests/suite/html/frame.typ index 711933d76..e389dc5d6 100644 --- a/tests/suite/html/frame.typ +++ b/tests/suite/html/frame.typ @@ -1,5 +1,6 @@ -// No proper HTML tests here yet because we don't want to test SVG export just -// yet. We'll definitely add tests at some point. +--- html-frame html --- +A rectangle: +#html.frame(rect()) --- html-frame-in-layout --- // Ensure that HTML frames are transparent in layout. This is less important for