Add HTML export format

This commit is contained in:
Laurenz 2024-12-02 14:19:52 +01:00
parent 885c7d96ee
commit e0122a5b50
22 changed files with 824 additions and 110 deletions

16
Cargo.lock generated
View File

@ -2674,6 +2674,7 @@ dependencies = [
"comemo", "comemo",
"ecow", "ecow",
"typst-eval", "typst-eval",
"typst-html",
"typst-layout", "typst-layout",
"typst-library", "typst-library",
"typst-macros", "typst-macros",
@ -2720,6 +2721,7 @@ dependencies = [
"toml", "toml",
"typst", "typst",
"typst-eval", "typst-eval",
"typst-html",
"typst-kit", "typst-kit",
"typst-macros", "typst-macros",
"typst-pdf", "typst-pdf",
@ -2787,6 +2789,20 @@ dependencies = [
"typst-syntax", "typst-syntax",
] ]
[[package]]
name = "typst-html"
version = "0.12.0"
dependencies = [
"comemo",
"ecow",
"typst-library",
"typst-macros",
"typst-svg",
"typst-syntax",
"typst-timing",
"typst-utils",
]
[[package]] [[package]]
name = "typst-ide" name = "typst-ide"
version = "0.12.0" version = "0.12.0"

View File

@ -19,6 +19,7 @@ readme = "README.md"
typst = { path = "crates/typst", version = "0.12.0" } typst = { path = "crates/typst", version = "0.12.0" }
typst-cli = { path = "crates/typst-cli", version = "0.12.0" } typst-cli = { path = "crates/typst-cli", version = "0.12.0" }
typst-eval = { path = "crates/typst-eval", version = "0.12.0" } typst-eval = { path = "crates/typst-eval", version = "0.12.0" }
typst-html = { path = "crates/typst-html", version = "0.12.0" }
typst-ide = { path = "crates/typst-ide", version = "0.12.0" } typst-ide = { path = "crates/typst-ide", version = "0.12.0" }
typst-kit = { path = "crates/typst-kit", version = "0.12.0" } typst-kit = { path = "crates/typst-kit", version = "0.12.0" }
typst-layout = { path = "crates/typst-layout", version = "0.12.0" } typst-layout = { path = "crates/typst-layout", version = "0.12.0" }

View File

@ -20,6 +20,7 @@ doc = false
[dependencies] [dependencies]
typst = { workspace = true } typst = { workspace = true }
typst-eval = { workspace = true } typst-eval = { workspace = true }
typst-html = { workspace = true }
typst-kit = { workspace = true } typst-kit = { workspace = true }
typst-macros = { workspace = true } typst-macros = { workspace = true }
typst-pdf = { workspace = true } typst-pdf = { workspace = true }

View File

@ -512,6 +512,7 @@ pub enum OutputFormat {
Pdf, Pdf,
Png, Png,
Svg, Svg,
Html,
} }
impl Display for OutputFormat { impl Display for OutputFormat {

View File

@ -12,6 +12,7 @@ use typst::diag::{
bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned, bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned,
}; };
use typst::foundations::{Datetime, Smart}; use typst::foundations::{Datetime, Smart};
use typst::html::HtmlDocument;
use typst::layout::{Frame, Page, PageRanges, PagedDocument}; use typst::layout::{Frame, Page, PageRanges, PagedDocument};
use typst::syntax::{FileId, Source, Span}; use typst::syntax::{FileId, Source, Span};
use typst::WorldExt; use typst::WorldExt;
@ -41,6 +42,7 @@ impl CompileCommand {
OutputFormat::Pdf => "pdf", OutputFormat::Pdf => "pdf",
OutputFormat::Png => "png", OutputFormat::Png => "png",
OutputFormat::Svg => "svg", OutputFormat::Svg => "svg",
OutputFormat::Html => "html",
}, },
)) ))
}) })
@ -57,6 +59,7 @@ impl CompileCommand {
Some(ext) if ext.eq_ignore_ascii_case("pdf") => OutputFormat::Pdf, Some(ext) if ext.eq_ignore_ascii_case("pdf") => OutputFormat::Pdf,
Some(ext) if ext.eq_ignore_ascii_case("png") => OutputFormat::Png, Some(ext) if ext.eq_ignore_ascii_case("png") => OutputFormat::Png,
Some(ext) if ext.eq_ignore_ascii_case("svg") => OutputFormat::Svg, Some(ext) if ext.eq_ignore_ascii_case("svg") => OutputFormat::Svg,
Some(ext) if ext.eq_ignore_ascii_case("html") => OutputFormat::Html,
_ => bail!( _ => bail!(
"could not infer output format for path {}.\n\ "could not infer output format for path {}.\n\
consider providing the format manually with `--format/-f`", consider providing the format manually with `--format/-f`",
@ -95,9 +98,6 @@ impl CompileCommand {
/// Execute a compilation command. /// Execute a compilation command.
pub fn compile(mut timer: Timer, mut command: CompileCommand) -> StrResult<()> { pub fn compile(mut timer: Timer, mut command: CompileCommand) -> StrResult<()> {
// Only meant for input validation
_ = command.output_format()?;
let mut world = let mut world =
SystemWorld::new(&command.common).map_err(|err| eco_format!("{err}"))?; SystemWorld::new(&command.common).map_err(|err| eco_format!("{err}"))?;
timer.record(&mut world, |world| compile_once(world, &mut command, false))??; timer.record(&mut world, |world| compile_once(world, &mut command, false))??;
@ -113,15 +113,16 @@ pub fn compile_once(
command: &mut CompileCommand, command: &mut CompileCommand,
watching: bool, watching: bool,
) -> StrResult<()> { ) -> StrResult<()> {
_ = command.output_format()?;
let start = std::time::Instant::now(); let start = std::time::Instant::now();
if watching { if watching {
Status::Compiling.print(command).unwrap(); Status::Compiling.print(command).unwrap();
} }
let Warned { output, warnings } = typst::compile(world); let Warned { output, warnings } = compile_and_export(world, command, watching);
let result = output.and_then(|document| export(world, &document, command, watching));
match result { match output {
// Export the PDF / PNG. // Export the PDF / PNG.
Ok(()) => { Ok(()) => {
let duration = start.elapsed(); let duration = start.elapsed();
@ -167,14 +168,43 @@ pub fn compile_once(
Ok(()) Ok(())
} }
fn compile_and_export(
world: &mut SystemWorld,
command: &mut CompileCommand,
watching: bool,
) -> Warned<SourceResult<()>> {
let format = command.output_format().unwrap();
match format {
OutputFormat::Html => {
let Warned { output, warnings } = typst::compile::<HtmlDocument>(world);
let result = output.and_then(|document| {
command
.output()
.write(typst_html::html(&document)?.as_bytes())
.map_err(|err| eco_format!("failed to write HTML file ({err})"))
.at(Span::detached())
});
Warned { output: result, warnings }
}
_ => {
let Warned { output, warnings } = typst::compile::<PagedDocument>(world);
let result = output
.and_then(|document| export_paged(world, &document, command, watching));
Warned { output: result, warnings }
}
}
}
/// Export into the target format. /// Export into the target format.
fn export( fn export_paged(
world: &mut SystemWorld, world: &mut SystemWorld,
document: &PagedDocument, document: &PagedDocument,
command: &CompileCommand, command: &CompileCommand,
watching: bool, watching: bool,
) -> SourceResult<()> { ) -> SourceResult<()> {
match command.output_format().at(Span::detached())? { match command.output_format().at(Span::detached())? {
OutputFormat::Pdf => export_pdf(document, command),
OutputFormat::Png => { OutputFormat::Png => {
export_image(world, document, command, watching, ImageExportFormat::Png) export_image(world, document, command, watching, ImageExportFormat::Png)
.at(Span::detached()) .at(Span::detached())
@ -183,7 +213,7 @@ fn export(
export_image(world, document, command, watching, ImageExportFormat::Svg) export_image(world, document, command, watching, ImageExportFormat::Svg)
.at(Span::detached()) .at(Span::detached())
} }
OutputFormat::Pdf => export_pdf(document, command), OutputFormat::Html => unreachable!(),
} }
} }

View File

@ -0,0 +1,26 @@
[package]
name = "typst-html"
description = "Typst's HTML exporter."
version = { workspace = true }
rust-version = { workspace = true }
authors = { workspace = true }
edition = { workspace = true }
homepage = { workspace = true }
repository = { workspace = true }
license = { workspace = true }
categories = { workspace = true }
keywords = { workspace = true }
readme = { workspace = true }
[dependencies]
typst-library = { workspace = true }
typst-macros = { workspace = true }
typst-syntax = { workspace = true }
typst-timing = { workspace = true }
typst-utils = { workspace = true }
typst-svg = { workspace = true }
comemo = { workspace = true }
ecow = { workspace = true }
[lints]
workspace = true

View File

@ -0,0 +1,104 @@
use std::fmt::Write;
use typst_library::diag::{bail, At, SourceResult, StrResult};
use typst_library::foundations::Repr;
use typst_library::html::{charsets, tag, HtmlDocument, HtmlElement, HtmlNode};
use typst_library::layout::Frame;
use typst_syntax::Span;
/// Encodes an HTML document into a string.
pub fn html(document: &HtmlDocument) -> SourceResult<String> {
let mut w = Writer { buf: String::new() };
w.buf.push_str("<!DOCTYPE html>");
write_element(&mut w, &document.root)?;
Ok(w.buf)
}
struct Writer {
buf: String,
}
/// Encode an HTML node into the writer.
fn write_node(w: &mut Writer, node: &HtmlNode) -> SourceResult<()> {
match node {
HtmlNode::Tag(_) => {}
HtmlNode::Text(text, span) => write_text(w, text, *span)?,
HtmlNode::Element(element) => write_element(w, element)?,
HtmlNode::Frame(frame) => write_frame(w, frame),
}
Ok(())
}
/// Encode plain text into the writer.
fn write_text(w: &mut Writer, text: &str, span: Span) -> SourceResult<()> {
for c in text.chars() {
if charsets::is_valid_in_normal_element_text(c) {
w.buf.push(c);
} else {
write_escape(w, c).at(span)?;
}
}
Ok(())
}
/// Encode one element into the write.
fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
w.buf.push('<');
w.buf.push_str(&element.tag.resolve());
for (attr, value) in &element.attrs.0 {
w.buf.push(' ');
w.buf.push_str(&attr.resolve());
w.buf.push('=');
w.buf.push('"');
for c in value.chars() {
if charsets::is_valid_in_attribute_value(c) {
w.buf.push(c);
} else {
write_escape(w, c).at(element.span)?;
}
}
w.buf.push('"');
}
w.buf.push('>');
if tag::is_void(element.tag) {
return Ok(());
}
for node in &element.children {
write_node(w, node)?;
}
w.buf.push_str("</");
w.buf.push_str(&element.tag.resolve());
w.buf.push('>');
Ok(())
}
/// Escape a character.
fn write_escape(w: &mut Writer, c: char) -> StrResult<()> {
// See <https://html.spec.whatwg.org/multipage/syntax.html#syntax-charref>
match c {
'&' => w.buf.push_str("&amp;"),
'<' => w.buf.push_str("&lt;"),
'>' => w.buf.push_str("&gt;"),
'"' => w.buf.push_str("&quot;"),
'\'' => w.buf.push_str("&apos;"),
c if charsets::is_w3c_text_char(c) && c != '\r' => {
write!(w.buf, "&#x{:x};", c as u32).unwrap()
}
_ => bail!("the character {} cannot be encoded in HTML", c.repr()),
}
Ok(())
}
/// Encode a laid out frame into the writer.
fn write_frame(w: &mut Writer, frame: &Frame) {
// FIXME: This string replacement is obviously a hack.
let svg = typst_svg::svg_frame(frame)
.replace("<svg class", "<svg style=\"overflow: visible;\" class");
w.buf.push_str(&svg);
}

View File

@ -0,0 +1,315 @@
//! Typst's HTML exporter.
mod encode;
pub use self::encode::html;
use comemo::{Track, Tracked, TrackedMut};
use typst_library::diag::{bail, warning, At, SourceResult};
use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{Content, StyleChain, Target, TargetElem};
use typst_library::html::{
attr, tag, FrameElem, HtmlDocument, HtmlElem, HtmlElement, HtmlNode,
};
use typst_library::introspection::{
Introspector, Locator, LocatorLink, SplitLocator, TagElem,
};
use typst_library::layout::{Abs, Axes, BoxElem, Region, Size};
use typst_library::model::{DocumentInfo, ParElem};
use typst_library::routines::{Arenas, Pair, RealizationKind, Routines};
use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem};
use typst_library::World;
use typst_syntax::Span;
/// Produce an HTML document from content.
///
/// This first performs root-level realization and then turns the resulting
/// elements into HTML.
#[typst_macros::time(name = "html document")]
pub fn html_document(
engine: &mut Engine,
content: &Content,
styles: StyleChain,
) -> SourceResult<HtmlDocument> {
html_document_impl(
engine.routines,
engine.world,
engine.introspector,
engine.traced,
TrackedMut::reborrow_mut(&mut engine.sink),
engine.route.track(),
content,
styles,
)
}
/// The internal implementation of `html_document`.
#[comemo::memoize]
#[allow(clippy::too_many_arguments)]
fn html_document_impl(
routines: &Routines,
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
traced: Tracked<Traced>,
sink: TrackedMut<Sink>,
route: Tracked<Route>,
content: &Content,
styles: StyleChain,
) -> SourceResult<HtmlDocument> {
let mut locator = Locator::root().split();
let mut engine = Engine {
routines,
world,
introspector,
traced,
sink,
route: Route::extend(route).unnested(),
};
// Mark the external styles as "outside" so that they are valid at the page
// level.
let styles = styles.to_map().outside();
let styles = StyleChain::new(&styles);
let arenas = Arenas::default();
let mut info = DocumentInfo::default();
let children = (engine.routines.realize)(
RealizationKind::HtmlDocument(&mut info),
&mut engine,
&mut locator,
&arenas,
content,
styles,
)?;
let output = handle_list(&mut engine, &mut locator, children.iter().copied())?;
let root = root_element(output, &info)?;
let introspector = Introspector::html(&root);
Ok(HtmlDocument { info, root, introspector })
}
/// Produce HTML nodes from content.
#[typst_macros::time(name = "html fragment")]
pub fn html_fragment(
engine: &mut Engine,
content: &Content,
locator: Locator,
styles: StyleChain,
) -> SourceResult<Vec<HtmlNode>> {
html_fragment_impl(
engine.routines,
engine.world,
engine.introspector,
engine.traced,
TrackedMut::reborrow_mut(&mut engine.sink),
engine.route.track(),
content,
locator.track(),
styles,
)
}
/// The cached, internal implementation of [`html_fragment`].
#[comemo::memoize]
#[allow(clippy::too_many_arguments)]
fn html_fragment_impl(
routines: &Routines,
world: Tracked<dyn World + '_>,
introspector: Tracked<Introspector>,
traced: Tracked<Traced>,
sink: TrackedMut<Sink>,
route: Tracked<Route>,
content: &Content,
locator: Tracked<Locator>,
styles: StyleChain,
) -> SourceResult<Vec<HtmlNode>> {
let link = LocatorLink::new(locator);
let mut locator = Locator::link(&link).split();
let mut engine = Engine {
routines,
world,
introspector,
traced,
sink,
route: Route::extend(route),
};
engine.route.check_html_depth().at(content.span())?;
let arenas = Arenas::default();
let children = (engine.routines.realize)(
RealizationKind::HtmlFragment,
&mut engine,
&mut locator,
&arenas,
content,
styles,
)?;
handle_list(&mut engine, &mut locator, children.iter().copied())
}
/// Convert children into HTML nodes.
fn handle_list<'a>(
engine: &mut Engine,
locator: &mut SplitLocator,
children: impl IntoIterator<Item = Pair<'a>>,
) -> SourceResult<Vec<HtmlNode>> {
let mut output = Vec::new();
for (child, styles) in children {
handle(engine, child, locator, styles, &mut output)?;
}
Ok(output)
}
/// Convert a child into HTML node(s).
fn handle(
engine: &mut Engine,
child: &Content,
locator: &mut SplitLocator,
styles: StyleChain,
output: &mut Vec<HtmlNode>,
) -> SourceResult<()> {
if let Some(elem) = child.to_packed::<TagElem>() {
output.push(HtmlNode::Tag(elem.tag.clone()));
} else if let Some(elem) = child.to_packed::<HtmlElem>() {
let mut children = vec![];
if let Some(body) = elem.body(styles) {
children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
}
if tag::is_void(elem.tag) && !children.is_empty() {
bail!(elem.span(), "HTML void elements may not have children");
}
let element = HtmlElement {
tag: elem.tag,
attrs: elem.attrs(styles).clone(),
children,
span: elem.span(),
};
output.push(element.into());
} else if let Some(elem) = child.to_packed::<ParElem>() {
let children = handle_list(engine, locator, elem.children.iter(&styles))?;
output.push(
HtmlElement::new(tag::p)
.with_children(children)
.spanned(elem.span())
.into(),
);
} else if let Some(elem) = child.to_packed::<BoxElem>() {
// FIXME: Very incomplete and hacky, but makes boxes kind fulfill their
// purpose for now.
if let Some(body) = elem.body(styles) {
let children =
html_fragment(engine, body, locator.next(&elem.span()), styles)?;
output.extend(children);
}
} else if child.is::<SpaceElem>() {
output.push(HtmlNode::text(' ', child.span()));
} else if let Some(elem) = child.to_packed::<TextElem>() {
output.push(HtmlNode::text(elem.text.clone(), elem.span()));
} else if let Some(elem) = child.to_packed::<LinebreakElem>() {
output.push(HtmlElement::new(tag::br).spanned(elem.span()).into());
} else if let Some(elem) = child.to_packed::<SmartQuoteElem>() {
output.push(HtmlNode::text(
if elem.double(styles) { '"' } else { '\'' },
child.span(),
));
} else if let Some(elem) = child.to_packed::<FrameElem>() {
let locator = locator.next(&elem.span());
let style = TargetElem::set_target(Target::Paged).wrap();
let frame = (engine.routines.layout_frame)(
engine,
&elem.body,
locator,
styles.chain(&style),
Region::new(Size::splat(Abs::inf()), Axes::splat(false)),
)?;
output.push(HtmlNode::Frame(frame));
} else {
engine.sink.warn(warning!(
child.span(),
"{} was ignored during HTML export",
child.elem().name()
));
}
Ok(())
}
/// Wrap the nodes in `<html>` and `<body>` if they are not yet rooted,
/// supplying a suitable `<head>`.
fn root_element(output: Vec<HtmlNode>, info: &DocumentInfo) -> SourceResult<HtmlElement> {
let body = match classify_output(output)? {
OutputKind::Html(element) => return Ok(element),
OutputKind::Body(body) => body,
OutputKind::Leafs(leafs) => HtmlElement::new(tag::body).with_children(leafs),
};
Ok(HtmlElement::new(tag::html)
.with_children(vec![head_element(info).into(), body.into()]))
}
/// Generate a `<head>` element.
fn head_element(info: &DocumentInfo) -> HtmlElement {
let mut children = vec![];
children.push(HtmlElement::new(tag::meta).with_attr(attr::charset, "utf-8").into());
children.push(
HtmlElement::new(tag::meta)
.with_attr(attr::name, "viewport")
.with_attr(attr::content, "width=device-width, initial-scale=1")
.into(),
);
if let Some(title) = &info.title {
children.push(
HtmlElement::new(tag::title)
.with_children(vec![HtmlNode::Text(title.clone(), Span::detached())])
.into(),
);
}
if let Some(description) = &info.description {
children.push(
HtmlElement::new(tag::meta)
.with_attr(attr::name, "description")
.with_attr(attr::content, description.clone())
.into(),
);
}
HtmlElement::new(tag::head).with_children(children)
}
/// Determine which kind of output the user generated.
fn classify_output(mut output: Vec<HtmlNode>) -> SourceResult<OutputKind> {
let len = output.len();
for node in &mut output {
let HtmlNode::Element(elem) = node else { continue };
let tag = elem.tag;
let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html));
match (tag, len) {
(tag::html, 1) => return Ok(OutputKind::Html(take())),
(tag::body, 1) => return Ok(OutputKind::Body(take())),
(tag::html | tag::body, _) => bail!(
elem.span,
"`{}` element must be the only element in the document",
elem.tag
),
_ => {}
}
}
Ok(OutputKind::Leafs(output))
}
/// What kinds of output the user generated.
enum OutputKind {
/// The user generated their own `<html>` element. We do not need to supply
/// one.
Html(HtmlElement),
/// The user generate their own `<body>` element. We do not need to supply
/// one, but need supply the `<html>` element.
Body(HtmlElement),
/// The user generated leafs which we wrap in a `<body>` and `<html>`.
Leafs(Vec<HtmlNode>),
}

View File

@ -37,7 +37,7 @@ pub fn analyze_expr(
} }
} }
return typst::trace(world.upcast(), node.span()); return typst::trace::<PagedDocument>(world.upcast(), node.span());
} }
}; };

View File

@ -5,7 +5,7 @@ use std::hash::Hash;
use bumpalo::boxed::Box as BumpBox; use bumpalo::boxed::Box as BumpBox;
use bumpalo::Bump; use bumpalo::Bump;
use comemo::{Track, Tracked, TrackedMut}; use comemo::{Track, Tracked, TrackedMut};
use typst_library::diag::{bail, SourceResult}; use typst_library::diag::{bail, warning, SourceResult};
use typst_library::engine::{Engine, Route, Sink, Traced}; use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{Packed, Resolve, Smart, StyleChain}; use typst_library::foundations::{Packed, Resolve, Smart, StyleChain};
use typst_library::introspection::{ use typst_library::introspection::{
@ -83,7 +83,11 @@ impl<'a> Collector<'a, '_, '_> {
hint: "try using a `#colbreak()` instead", hint: "try using a `#colbreak()` instead",
); );
} else { } else {
bail!(child.span(), "{} is not allowed here", child.func().name()); self.engine.sink.warn(warning!(
child.span(),
"{} was ignored during paged export",
child.func().name()
));
} }
} }

View File

@ -142,7 +142,7 @@ fn layout_fragment_impl(
let arenas = Arenas::default(); let arenas = Arenas::default();
let children = (engine.routines.realize)( let children = (engine.routines.realize)(
RealizationKind::Container, RealizationKind::LayoutFragment,
&mut engine, &mut engine,
&mut locator, &mut locator,
&arenas, &arenas,

View File

@ -75,7 +75,7 @@ fn layout_document_impl(
let arenas = Arenas::default(); let arenas = Arenas::default();
let mut info = DocumentInfo::default(); let mut info = DocumentInfo::default();
let mut children = (engine.routines.realize)( let mut children = (engine.routines.realize)(
RealizationKind::Root(&mut info), RealizationKind::LayoutDocument(&mut info),
&mut engine, &mut engine,
&mut locator, &mut locator,
&arenas, &arenas,
@ -84,7 +84,7 @@ fn layout_document_impl(
)?; )?;
let pages = layout_pages(&mut engine, &mut children, locator, styles)?; let pages = layout_pages(&mut engine, &mut children, locator, styles)?;
let introspector = Introspector::new(&pages); let introspector = Introspector::paged(&pages);
Ok(PagedDocument { pages, info, introspector }) Ok(PagedDocument { pages, info, introspector })
} }

View File

@ -301,6 +301,9 @@ impl Route<'_> {
/// The maximum layout nesting depth. /// The maximum layout nesting depth.
const MAX_LAYOUT_DEPTH: usize = 72; const MAX_LAYOUT_DEPTH: usize = 72;
/// The maximum HTML nesting depth.
const MAX_HTML_DEPTH: usize = 72;
/// The maximum function call nesting depth. /// The maximum function call nesting depth.
const MAX_CALL_DEPTH: usize = 80; const MAX_CALL_DEPTH: usize = 80;
@ -326,6 +329,17 @@ impl Route<'_> {
Ok(()) Ok(())
} }
/// Ensures that we are within the maximum HTML depth.
pub fn check_html_depth(&self) -> HintedStrResult<()> {
if !self.within(Route::MAX_HTML_DEPTH) {
bail!(
"maximum HTML depth exceeded";
hint: "try to reduce the amount of nesting of your HTML",
);
}
Ok(())
}
/// Ensures that we are within the maximum function call depth. /// Ensures that we are within the maximum function call depth.
pub fn check_call_depth(&self) -> StrResult<()> { pub fn check_call_depth(&self) -> StrResult<()> {
if !self.within(Route::MAX_CALL_DEPTH) { if !self.within(Route::MAX_CALL_DEPTH) {

View File

@ -10,6 +10,7 @@ use typst_utils::NonZeroExt;
use crate::diag::{bail, StrResult}; use crate::diag::{bail, StrResult};
use crate::foundations::{Content, Label, Repr, Selector}; use crate::foundations::{Content, Label, Repr, Selector};
use crate::html::{HtmlElement, HtmlNode};
use crate::introspection::{Location, Tag}; use crate::introspection::{Location, Tag};
use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform}; use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform};
use crate::model::Numbering; use crate::model::Numbering;
@ -47,9 +48,15 @@ type Pair = (Content, Position);
impl Introspector { impl Introspector {
/// Creates an introspector for a page list. /// Creates an introspector for a page list.
#[typst_macros::time(name = "introspect")] #[typst_macros::time(name = "introspect pages")]
pub fn new(pages: &[Page]) -> Self { pub fn paged(pages: &[Page]) -> Self {
IntrospectorBuilder::new().build(pages) IntrospectorBuilder::new().build_paged(pages)
}
/// Creates an introspector for HTML.
#[typst_macros::time(name = "introspect html")]
pub fn html(root: &HtmlElement) -> Self {
IntrospectorBuilder::new().build_html(root)
} }
/// Iterates over all locatable elements. /// Iterates over all locatable elements.
@ -346,6 +353,7 @@ impl Clone for QueryCache {
/// Builds the introspector. /// Builds the introspector.
#[derive(Default)] #[derive(Default)]
struct IntrospectorBuilder { struct IntrospectorBuilder {
pages: usize,
page_numberings: Vec<Option<Numbering>>, page_numberings: Vec<Option<Numbering>>,
page_supplements: Vec<Content>, page_supplements: Vec<Content>,
seen: HashSet<Location>, seen: HashSet<Location>,
@ -361,46 +369,37 @@ impl IntrospectorBuilder {
Self::default() Self::default()
} }
/// Build the introspector. /// Build an introspector for a page list.
fn build(mut self, pages: &[Page]) -> Introspector { fn build_paged(mut self, pages: &[Page]) -> Introspector {
self.pages = pages.len();
self.page_numberings.reserve(pages.len()); self.page_numberings.reserve(pages.len());
self.page_supplements.reserve(pages.len()); self.page_supplements.reserve(pages.len());
// Discover all elements. // Discover all elements.
let mut root = Vec::new(); let mut elems = Vec::new();
for (i, page) in pages.iter().enumerate() { for (i, page) in pages.iter().enumerate() {
self.page_numberings.push(page.numbering.clone()); self.page_numberings.push(page.numbering.clone());
self.page_supplements.push(page.supplement.clone()); self.page_supplements.push(page.supplement.clone());
self.discover( self.discover_in_frame(
&mut root, &mut elems,
&page.frame, &page.frame,
NonZeroUsize::new(1 + i).unwrap(), NonZeroUsize::new(1 + i).unwrap(),
Transform::identity(), Transform::identity(),
); );
} }
self.locations.reserve(self.seen.len()); self.finalize(elems)
// Save all pairs and their descendants in the correct order.
let mut elems = Vec::with_capacity(self.seen.len());
for pair in root {
self.visit(&mut elems, pair);
} }
Introspector { /// Build an introspector for an HTML document.
pages: pages.len(), fn build_html(mut self, root: &HtmlElement) -> Introspector {
page_numberings: self.page_numberings, let mut elems = Vec::new();
page_supplements: self.page_supplements, self.discover_in_html(&mut elems, root);
elems, self.finalize(elems)
keys: self.keys,
locations: self.locations,
labels: self.labels,
queries: QueryCache::default(),
}
} }
/// Processes the tags in the frame. /// Processes the tags in the frame.
fn discover( fn discover_in_frame(
&mut self, &mut self,
sink: &mut Vec<Pair>, sink: &mut Vec<Pair>,
frame: &Frame, frame: &Frame,
@ -416,27 +415,83 @@ impl IntrospectorBuilder {
if let Some(parent) = group.parent { if let Some(parent) = group.parent {
let mut nested = vec![]; let mut nested = vec![];
self.discover(&mut nested, &group.frame, page, ts); self.discover_in_frame(&mut nested, &group.frame, page, ts);
self.insertions.insert(parent, nested); self.insertions.insert(parent, nested);
} else { } else {
self.discover(sink, &group.frame, page, ts); self.discover_in_frame(sink, &group.frame, page, ts);
} }
} }
FrameItem::Tag(Tag::Start(elem)) => { FrameItem::Tag(tag) => {
let loc = elem.location().unwrap(); self.discover_in_tag(
if self.seen.insert(loc) { sink,
let point = pos.transform(ts); tag,
sink.push((elem.clone(), Position { page, point })); Position { page, point: pos.transform(ts) },
} );
}
FrameItem::Tag(Tag::End(loc, key)) => {
self.keys.insert(*key, *loc);
} }
_ => {} _ => {}
} }
} }
} }
/// Processes the tags in the HTML element.
fn discover_in_html(&mut self, sink: &mut Vec<Pair>, elem: &HtmlElement) {
for child in &elem.children {
match child {
HtmlNode::Tag(tag) => self.discover_in_tag(
sink,
tag,
Position { page: NonZeroUsize::ONE, point: Point::zero() },
),
HtmlNode::Text(_, _) => {}
HtmlNode::Element(elem) => self.discover_in_html(sink, elem),
HtmlNode::Frame(frame) => self.discover_in_frame(
sink,
frame,
NonZeroUsize::ONE,
Transform::identity(),
),
}
}
}
/// Handle a tag.
fn discover_in_tag(&mut self, sink: &mut Vec<Pair>, tag: &Tag, position: Position) {
match tag {
Tag::Start(elem) => {
let loc = elem.location().unwrap();
if self.seen.insert(loc) {
sink.push((elem.clone(), position));
}
}
Tag::End(loc, key) => {
self.keys.insert(*key, *loc);
}
}
}
/// Build a complete introspector with all acceleration structures from a
/// list of top-level pairs.
fn finalize(mut self, root: Vec<Pair>) -> Introspector {
self.locations.reserve(self.seen.len());
// Save all pairs and their descendants in the correct order.
let mut elems = Vec::with_capacity(self.seen.len());
for pair in root {
self.visit(&mut elems, pair);
}
Introspector {
pages: self.pages,
page_numberings: self.page_numberings,
page_supplements: self.page_supplements,
elems,
keys: self.keys,
locations: self.locations,
labels: self.labels,
queries: QueryCache::default(),
}
}
/// Saves a pair and all its descendants into `elems` and populates the /// Saves a pair and all its descendants into `elems` and populates the
/// acceleration structures. /// acceleration structures.
fn visit(&mut self, elems: &mut Vec<Pair>, pair: Pair) { fn visit(&mut self, elems: &mut Vec<Pair>, pair: Pair) {

View File

@ -520,8 +520,7 @@ pub enum FrameItem {
Image(Image, Size, Span), Image(Image, Size, Span),
/// An internal or external link to a destination. /// An internal or external link to a destination.
Link(Destination, Size), Link(Destination, Size),
/// An introspectable element that produced something within this frame /// An introspectable element that produced something within this frame.
/// alongside its key.
Tag(Tag), Tag(Tag),
} }

View File

@ -86,13 +86,6 @@ routines! {
styles: StyleChain<'a>, styles: StyleChain<'a>,
) -> SourceResult<Vec<Pair<'a>>> ) -> SourceResult<Vec<Pair<'a>>>
/// Layout content into a document.
fn layout_document(
engine: &mut Engine,
content: &Content,
styles: StyleChain,
) -> SourceResult<PagedDocument>
/// Lays out content into multiple regions. /// Lays out content into multiple regions.
fn layout_fragment( fn layout_fragment(
engine: &mut Engine, engine: &mut Engine,
@ -343,11 +336,16 @@ pub enum EvalMode {
/// Defines what kind of realization we are performing. /// Defines what kind of realization we are performing.
pub enum RealizationKind<'a> { pub enum RealizationKind<'a> {
/// This the root realization for the document. Requires a mutable reference /// This the root realization for layout. Requires a mutable reference
/// to document metadata that will be filled from `set document` rules. /// to document metadata that will be filled from `set document` rules.
Root(&'a mut DocumentInfo), LayoutDocument(&'a mut DocumentInfo),
/// A nested realization in a container (e.g. a `block`). /// A nested realization in a container (e.g. a `block`).
Container, LayoutFragment,
/// This the root realization for HTML. Requires a mutable reference
/// to document metadata that will be filled from `set document` rules.
HtmlDocument(&'a mut DocumentInfo),
/// A nested realization in a container (e.g. a `block`).
HtmlFragment,
/// A realization within math. /// A realization within math.
Math, Math,
} }

View File

@ -18,6 +18,7 @@ use typst_library::foundations::{
SequenceElem, Show, ShowSet, Style, StyleChain, StyleVec, StyledElem, Styles, SequenceElem, Show, ShowSet, Style, StyleChain, StyleVec, StyledElem, Styles,
Synthesize, Transformation, Synthesize, Transformation,
}; };
use typst_library::html::{tag, HtmlElem};
use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem}; use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem};
use typst_library::layout::{ use typst_library::layout::{
AlignElem, BoxElem, HElem, InlineElem, PageElem, PagebreakElem, VElem, AlignElem, BoxElem, HElem, InlineElem, PageElem, PagebreakElem, VElem,
@ -47,12 +48,16 @@ pub fn realize<'a>(
locator, locator,
arenas, arenas,
rules: match kind { rules: match kind {
RealizationKind::Root(_) | RealizationKind::Container => NORMAL_RULES, RealizationKind::LayoutDocument(_) | RealizationKind::LayoutFragment => {
LAYOUT_RULES
}
RealizationKind::HtmlDocument(_) => HTML_DOCUMENT_RULES,
RealizationKind::HtmlFragment => HTML_FRAGMENT_RULES,
RealizationKind::Math => MATH_RULES, RealizationKind::Math => MATH_RULES,
}, },
sink: vec![], sink: vec![],
groupings: ArrayVec::new(), groupings: ArrayVec::new(),
outside: matches!(kind, RealizationKind::Root(_)), outside: matches!(kind, RealizationKind::LayoutDocument(_)),
may_attach: false, may_attach: false,
kind, kind,
}; };
@ -105,10 +110,10 @@ struct GroupingRule {
/// be visible to `finish`. /// be visible to `finish`.
tags: bool, tags: bool,
/// Defines which kinds of elements start and make up this kind of grouping. /// Defines which kinds of elements start and make up this kind of grouping.
trigger: fn(Element) -> bool, trigger: fn(&Content, &RealizationKind) -> bool,
/// Defines elements that may appear in the interior of the grouping, but /// Defines elements that may appear in the interior of the grouping, but
/// not at the edges. /// not at the edges.
inner: fn(Element) -> bool, inner: fn(&Content) -> bool,
/// Defines whether styles for this kind of element interrupt the grouping. /// Defines whether styles for this kind of element interrupt the grouping.
interrupt: fn(Element) -> bool, interrupt: fn(Element) -> bool,
/// Should convert the accumulated elements in `s.sink[start..]` into /// Should convert the accumulated elements in `s.sink[start..]` into
@ -555,14 +560,16 @@ fn visit_styled<'a>(
for style in local.iter() { for style in local.iter() {
let Some(elem) = style.element() else { continue }; let Some(elem) = style.element() else { continue };
if elem == DocumentElem::elem() { if elem == DocumentElem::elem() {
let RealizationKind::Root(info) = &mut s.kind else { match &mut s.kind {
let span = style.span(); RealizationKind::LayoutDocument(info)
bail!(span, "document set rules are not allowed inside of containers"); | RealizationKind::HtmlDocument(info) => info.populate(&local),
}; _ => bail!(
style.span(),
info.populate(&local); "document set rules are not allowed inside of containers"
),
}
} else if elem == PageElem::elem() { } else if elem == PageElem::elem() {
let RealizationKind::Root(_) = s.kind else { let RealizationKind::LayoutDocument(_) = s.kind else {
let span = style.span(); let span = style.span();
bail!(span, "page configuration is not allowed inside of containers"); bail!(span, "page configuration is not allowed inside of containers");
}; };
@ -618,8 +625,7 @@ fn visit_grouping_rules<'a>(
content: &'a Content, content: &'a Content,
styles: StyleChain<'a>, styles: StyleChain<'a>,
) -> SourceResult<bool> { ) -> SourceResult<bool> {
let elem = content.elem(); let matching = s.rules.iter().find(|&rule| (rule.trigger)(content, &s.kind));
let matching = s.rules.iter().find(|&rule| (rule.trigger)(elem));
// Try to continue or finish an existing grouping. // Try to continue or finish an existing grouping.
while let Some(active) = s.groupings.last() { while let Some(active) = s.groupings.last() {
@ -629,7 +635,7 @@ fn visit_grouping_rules<'a>(
} }
// If the element can be added to the active grouping, do it. // If the element can be added to the active grouping, do it.
if (active.rule.trigger)(elem) || (active.rule.inner)(elem) { if (active.rule.trigger)(content, &s.kind) || (active.rule.inner)(content) {
s.sink.push((content, styles)); s.sink.push((content, styles));
return Ok(true); return Ok(true);
} }
@ -655,7 +661,9 @@ fn visit_filter_rules<'a>(
content: &'a Content, content: &'a Content,
styles: StyleChain<'a>, styles: StyleChain<'a>,
) -> SourceResult<bool> { ) -> SourceResult<bool> {
if content.is::<SpaceElem>() && !matches!(s.kind, RealizationKind::Math) { if content.is::<SpaceElem>()
&& !matches!(s.kind, RealizationKind::Math | RealizationKind::HtmlFragment)
{
// Outside of maths, spaces that were not collected by the paragraph // Outside of maths, spaces that were not collected by the paragraph
// grouper don't interest us. // grouper don't interest us.
return Ok(true); return Ok(true);
@ -730,7 +738,7 @@ fn finish_innermost_grouping(s: &mut State) -> SourceResult<()> {
let Grouping { start, rule } = s.groupings.pop().unwrap(); let Grouping { start, rule } = s.groupings.pop().unwrap();
// Trim trailing non-trigger elements. // Trim trailing non-trigger elements.
let trimmed = s.sink[start..].trim_end_matches(|(c, _)| !(rule.trigger)(c.elem())); let trimmed = s.sink[start..].trim_end_matches(|(c, _)| !(rule.trigger)(c, &s.kind));
let end = start + trimmed.len(); let end = start + trimmed.len();
let tail = s.store_slice(&s.sink[end..]); let tail = s.store_slice(&s.sink[end..]);
s.sink.truncate(end); s.sink.truncate(end);
@ -768,22 +776,30 @@ fn finish_innermost_grouping(s: &mut State) -> SourceResult<()> {
/// number of unique priority levels. /// number of unique priority levels.
const MAX_GROUP_NESTING: usize = 3; const MAX_GROUP_NESTING: usize = 3;
/// Grouping rules used in normal realizations. /// Grouping rules used in layout realization.
static NORMAL_RULES: &[&GroupingRule] = &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS]; static LAYOUT_RULES: &[&GroupingRule] = &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS];
/// Grouping rules used in math realization. /// Grouping rules used in HTML root realization.
static HTML_DOCUMENT_RULES: &[&GroupingRule] =
&[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS];
/// Grouping rules used in HTML fragment realization.
static HTML_FRAGMENT_RULES: &[&GroupingRule] = &[&TEXTUAL, &CITES, &LIST, &ENUM, &TERMS];
/// Grouping rules used in math realizatio.
static MATH_RULES: &[&GroupingRule] = &[&CITES, &LIST, &ENUM, &TERMS]; static MATH_RULES: &[&GroupingRule] = &[&CITES, &LIST, &ENUM, &TERMS];
/// Groups adjacent textual elements for text show rule application. /// Groups adjacent textual elements for text show rule application.
static TEXTUAL: GroupingRule = GroupingRule { static TEXTUAL: GroupingRule = GroupingRule {
priority: 3, priority: 3,
tags: true, tags: true,
trigger: |elem| { trigger: |content, _| {
let elem = content.elem();
elem == TextElem::elem() elem == TextElem::elem()
|| elem == LinebreakElem::elem() || elem == LinebreakElem::elem()
|| elem == SmartQuoteElem::elem() || elem == SmartQuoteElem::elem()
}, },
inner: |elem| elem == SpaceElem::elem(), inner: |content| content.elem() == SpaceElem::elem(),
// Any kind of style interrupts this kind of grouping since regex show // Any kind of style interrupts this kind of grouping since regex show
// rules cannot match over style changes anyway. // rules cannot match over style changes anyway.
interrupt: |_| true, interrupt: |_| true,
@ -794,15 +810,22 @@ static TEXTUAL: GroupingRule = GroupingRule {
static PAR: GroupingRule = GroupingRule { static PAR: GroupingRule = GroupingRule {
priority: 1, priority: 1,
tags: true, tags: true,
trigger: |elem| { trigger: |content, kind| {
let elem = content.elem();
elem == TextElem::elem() elem == TextElem::elem()
|| elem == HElem::elem() || elem == HElem::elem()
|| elem == LinebreakElem::elem() || elem == LinebreakElem::elem()
|| elem == SmartQuoteElem::elem() || elem == SmartQuoteElem::elem()
|| elem == InlineElem::elem() || elem == InlineElem::elem()
|| elem == BoxElem::elem() || elem == BoxElem::elem()
|| (matches!(
kind,
RealizationKind::HtmlDocument(_) | RealizationKind::HtmlFragment
) && content
.to_packed::<HtmlElem>()
.is_some_and(|elem| tag::is_inline(elem.tag)))
}, },
inner: |elem| elem == SpaceElem::elem(), inner: |content| content.elem() == SpaceElem::elem(),
interrupt: |elem| elem == ParElem::elem() || elem == AlignElem::elem(), interrupt: |elem| elem == ParElem::elem() || elem == AlignElem::elem(),
finish: finish_par, finish: finish_par,
}; };
@ -811,8 +834,8 @@ static PAR: GroupingRule = GroupingRule {
static CITES: GroupingRule = GroupingRule { static CITES: GroupingRule = GroupingRule {
priority: 2, priority: 2,
tags: false, tags: false,
trigger: |elem| elem == CiteElem::elem(), trigger: |content, _| content.elem() == CiteElem::elem(),
inner: |elem| elem == SpaceElem::elem(), inner: |content| content.elem() == SpaceElem::elem(),
interrupt: |elem| elem == CiteGroup::elem(), interrupt: |elem| elem == CiteGroup::elem(),
finish: finish_cites, finish: finish_cites,
}; };
@ -831,8 +854,11 @@ const fn list_like_grouping<T: ListLike>() -> GroupingRule {
GroupingRule { GroupingRule {
priority: 2, priority: 2,
tags: false, tags: false,
trigger: |elem| elem == T::Item::elem(), trigger: |content, _| content.elem() == T::Item::elem(),
inner: |elem| elem == SpaceElem::elem() || elem == ParbreakElem::elem(), inner: |content| {
let elem = content.elem();
elem == SpaceElem::elem() || elem == ParbreakElem::elem()
},
interrupt: |elem| elem == T::elem(), interrupt: |elem| elem == T::elem(),
finish: finish_list_like::<T>, finish: finish_list_like::<T>,
} }
@ -867,7 +893,7 @@ fn finish_textual(Grouped { s, mut start }: Grouped) -> SourceResult<()> {
// 1. We are already in a paragraph group. In this case, the elements just // 1. We are already in a paragraph group. In this case, the elements just
// transparently become part of it. // transparently become part of it.
// 2. There is no group at all. In this case, we create one. // 2. There is no group at all. In this case, we create one.
if s.groupings.is_empty() { if s.groupings.is_empty() && s.rules.iter().any(|&rule| std::ptr::eq(rule, &PAR)) {
s.groupings.push(Grouping { start, rule: &PAR }); s.groupings.push(Grouping { start, rule: &PAR });
} }

View File

@ -32,6 +32,17 @@ pub fn svg(page: &Page) -> String {
renderer.finalize() renderer.finalize()
} }
/// Export a frame into a SVG file.
#[typst_macros::time(name = "svg frame")]
pub fn svg_frame(frame: &Frame) -> String {
let mut renderer = SVGRenderer::new();
renderer.write_header(frame.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. /// Export a document with potentially multiple pages into a single SVG file.
/// ///
/// The padding will be added around and between the individual frames. /// The padding will be added around and between the individual frames.

View File

@ -14,6 +14,7 @@ readme = { workspace = true }
[dependencies] [dependencies]
typst-eval = { workspace = true } typst-eval = { workspace = true }
typst-html = { workspace = true }
typst-layout = { workspace = true } typst-layout = { workspace = true }
typst-library = { workspace = true } typst-library = { workspace = true }
typst-macros = { workspace = true } typst-macros = { workspace = true }

View File

@ -13,11 +13,11 @@
//! order-independent and thus much better suited for further processing than //! order-independent and thus much better suited for further processing than
//! the raw markup. //! the raw markup.
//! - **Layouting:** //! - **Layouting:**
//! Next, the content is [laid out] into a [document] containing one [frame] //! Next, the content is [laid out] into a [`PagedDocument`] containing one
//! per page with items at fixed positions. //! [frame] per page with items at fixed positions.
//! - **Exporting:** //! - **Exporting:**
//! These frames can finally be exported into an output format (currently PDF, //! These frames can finally be exported into an output format (currently PDF,
//! PNG, or SVG). //! PNG, SVG, and HTML).
//! //!
//! [tokens]: typst_syntax::SyntaxKind //! [tokens]: typst_syntax::SyntaxKind
//! [parsed]: typst_syntax::parse //! [parsed]: typst_syntax::parse
@ -43,23 +43,32 @@ use std::collections::HashSet;
use comemo::{Track, Tracked, Validate}; use comemo::{Track, Tracked, Validate};
use ecow::{eco_format, eco_vec, EcoString, EcoVec}; use ecow::{eco_format, eco_vec, EcoString, EcoVec};
use typst_library::diag::{warning, FileError, SourceDiagnostic, SourceResult, Warned}; use typst_library::diag::{
bail, warning, FileError, SourceDiagnostic, SourceResult, Warned,
};
use typst_library::engine::{Engine, Route, Sink, Traced}; use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{StyleChain, Styles, Value}; use typst_library::foundations::{StyleChain, Styles, Value};
use typst_library::html::HtmlDocument;
use typst_library::introspection::Introspector; use typst_library::introspection::Introspector;
use typst_library::layout::PagedDocument; use typst_library::layout::PagedDocument;
use typst_library::routines::Routines; use typst_library::routines::Routines;
use typst_syntax::{FileId, Span}; use typst_syntax::{FileId, Span};
use typst_timing::{timed, TimingScope}; use typst_timing::{timed, TimingScope};
use crate::foundations::{Target, TargetElem};
use crate::model::DocumentInfo;
/// Compile sources into a fully layouted document. /// Compile sources into a fully layouted document.
/// ///
/// - Returns `Ok(document)` if there were no fatal errors. /// - Returns `Ok(document)` if there were no fatal errors.
/// - Returns `Err(errors)` if there were fatal errors. /// - Returns `Err(errors)` if there were fatal errors.
#[typst_macros::time] #[typst_macros::time]
pub fn compile(world: &dyn World) -> Warned<SourceResult<PagedDocument>> { pub fn compile<D>(world: &dyn World) -> Warned<SourceResult<D>>
where
D: Document,
{
let mut sink = Sink::new(); let mut sink = Sink::new();
let output = compile_impl(world.track(), Traced::default().track(), &mut sink) let output = compile_impl::<D>(world.track(), Traced::default().track(), &mut sink)
.map_err(deduplicate); .map_err(deduplicate);
Warned { output, warnings: sink.warnings() } Warned { output, warnings: sink.warnings() }
} }
@ -67,22 +76,32 @@ pub fn compile(world: &dyn World) -> Warned<SourceResult<PagedDocument>> {
/// Compiles sources and returns all values and styles observed at the given /// Compiles sources and returns all values and styles observed at the given
/// `span` during compilation. /// `span` during compilation.
#[typst_macros::time] #[typst_macros::time]
pub fn trace(world: &dyn World, span: Span) -> EcoVec<(Value, Option<Styles>)> { pub fn trace<D>(world: &dyn World, span: Span) -> EcoVec<(Value, Option<Styles>)>
where
D: Document,
{
let mut sink = Sink::new(); let mut sink = Sink::new();
let traced = Traced::new(span); let traced = Traced::new(span);
compile_impl(world.track(), traced.track(), &mut sink).ok(); compile_impl::<D>(world.track(), traced.track(), &mut sink).ok();
sink.values() sink.values()
} }
/// The internal implementation of `compile` with a bit lower-level interface /// The internal implementation of `compile` with a bit lower-level interface
/// that is also used by `trace`. /// that is also used by `trace`.
fn compile_impl( fn compile_impl<D: Document>(
world: Tracked<dyn World + '_>, world: Tracked<dyn World + '_>,
traced: Tracked<Traced>, traced: Tracked<Traced>,
sink: &mut Sink, sink: &mut Sink,
) -> SourceResult<PagedDocument> { ) -> SourceResult<D> {
if D::TARGET == Target::Html {
warn_or_error_for_html(world, sink)?;
}
let library = world.library(); let library = world.library();
let styles = StyleChain::new(&library.styles); let base = StyleChain::new(&library.styles);
let target = TargetElem::set_target(D::TARGET).wrap();
let styles = base.chain(&target);
let empty_introspector = Introspector::default();
// Fetch the main source file once. // Fetch the main source file once.
let main = world.main(); let main = world.main();
@ -103,7 +122,8 @@ fn compile_impl(
let mut iter = 0; let mut iter = 0;
let mut subsink; let mut subsink;
let mut document = PagedDocument::default(); let mut introspector = &empty_introspector;
let mut document: D;
// Relayout until all introspections stabilize. // Relayout until all introspections stabilize.
// If that doesn't happen within five attempts, we give up. // If that doesn't happen within five attempts, we give up.
@ -118,7 +138,7 @@ fn compile_impl(
let constraint = <Introspector as Validate>::Constraint::new(); let constraint = <Introspector as Validate>::Constraint::new();
let mut engine = Engine { let mut engine = Engine {
world, world,
introspector: document.introspector.track_with(&constraint), introspector: introspector.track_with(&constraint),
traced, traced,
sink: subsink.track_mut(), sink: subsink.track_mut(),
route: Route::default(), route: Route::default(),
@ -126,10 +146,11 @@ fn compile_impl(
}; };
// Layout! // Layout!
document = (engine.routines.layout_document)(&mut engine, &content, styles)?; document = D::create(&mut engine, &content, styles)?;
introspector = document.introspector();
iter += 1; iter += 1;
if timed!("check stabilized", document.introspector.validate(&constraint)) { if timed!("check stabilized", introspector.validate(&constraint)) {
break; break;
} }
@ -208,6 +229,97 @@ fn hint_invalid_main_file(
eco_vec![diagnostic] eco_vec![diagnostic]
} }
/// HTML export will warn or error depending on whether the feature flag is enabled.
fn warn_or_error_for_html(
world: Tracked<dyn World + '_>,
sink: &mut Sink,
) -> SourceResult<()> {
if world.library().features.is_enabled(Feature::Html) {
sink.warn(warning!(
Span::detached(),
"html export is under active development and incomplete";
hint: "its behaviour may change at any time";
hint: "do not rely on this feature for production use cases"
));
} else {
bail!(
Span::detached(),
"html export is only available when `--feature html` is passed";
hint: "html export is under active development and incomplete"
);
}
Ok(())
}
/// A document is what results from compilation.
pub trait Document: sealed::Sealed {
/// Get the document's metadata.
fn info(&self) -> &DocumentInfo;
/// Get the document's introspector.
fn introspector(&self) -> &Introspector;
}
impl Document for PagedDocument {
fn info(&self) -> &DocumentInfo {
&self.info
}
fn introspector(&self) -> &Introspector {
&self.introspector
}
}
impl Document for HtmlDocument {
fn info(&self) -> &DocumentInfo {
&self.info
}
fn introspector(&self) -> &Introspector {
&self.introspector
}
}
mod sealed {
use typst_library::foundations::{Content, Target};
use super::*;
pub trait Sealed: Sized {
const TARGET: Target;
fn create(
engine: &mut Engine,
content: &Content,
styles: StyleChain,
) -> SourceResult<Self>;
}
impl Sealed for PagedDocument {
const TARGET: Target = Target::Paged;
fn create(
engine: &mut Engine,
content: &Content,
styles: StyleChain,
) -> SourceResult<Self> {
typst_layout::layout_document(engine, content, styles)
}
}
impl Sealed for HtmlDocument {
const TARGET: Target = Target::Html;
fn create(
engine: &mut Engine,
content: &Content,
styles: StyleChain,
) -> SourceResult<Self> {
typst_html::html_document(engine, content, styles)
}
}
}
/// Defines implementation of various Typst compiler routines as a table of /// Defines implementation of various Typst compiler routines as a table of
/// function pointers. /// function pointers.
/// ///
@ -216,7 +328,6 @@ pub static ROUTINES: Routines = Routines {
eval_string: typst_eval::eval_string, eval_string: typst_eval::eval_string,
eval_closure: typst_eval::eval_closure, eval_closure: typst_eval::eval_closure,
realize: typst_realize::realize, realize: typst_realize::realize,
layout_document: typst_layout::layout_document,
layout_fragment: typst_layout::layout_fragment, layout_fragment: typst_layout::layout_fragment,
layout_frame: typst_layout::layout_frame, layout_frame: typst_layout::layout_frame,
layout_inline: typst_layout::layout_inline, layout_inline: typst_layout::layout_inline,

View File

@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
use typed_arena::Arena; use typed_arena::Arena;
use typst::diag::{FileError, FileResult, StrResult}; use typst::diag::{FileError, FileResult, StrResult};
use typst::foundations::{Bytes, Datetime}; use typst::foundations::{Bytes, Datetime};
use typst::layout::{Abs, Point, Size}; use typst::layout::{Abs, PagedDocument, Point, Size};
use typst::syntax::{FileId, Source, VirtualPath}; use typst::syntax::{FileId, Source, VirtualPath};
use typst::text::{Font, FontBook}; use typst::text::{Font, FontBook};
use typst::utils::LazyHash; use typst::utils::LazyHash;
@ -419,7 +419,7 @@ fn code_block(resolver: &dyn Resolver, lang: &str, text: &str) -> Html {
let source = Source::new(id, compile); let source = Source::new(id, compile);
let world = DocWorld(source); let world = DocWorld(source);
let mut document = match typst::compile(&world).output { let mut document = match typst::compile::<PagedDocument>(&world).output {
Ok(doc) => doc, Ok(doc) => doc,
Err(err) => { Err(err) => {
let msg = &err[0].message; let msg = &err[0].message;

View File

@ -3,6 +3,7 @@
use libfuzzer_sys::fuzz_target; use libfuzzer_sys::fuzz_target;
use typst::diag::{FileError, FileResult}; use typst::diag::{FileError, FileResult};
use typst::foundations::{Bytes, Datetime}; use typst::foundations::{Bytes, Datetime};
use typst::layout::PagedDocument;
use typst::syntax::{FileId, Source}; use typst::syntax::{FileId, Source};
use typst::text::{Font, FontBook}; use typst::text::{Font, FontBook};
use typst::utils::LazyHash; use typst::utils::LazyHash;
@ -65,7 +66,7 @@ impl World for FuzzWorld {
fuzz_target!(|text: &str| { fuzz_target!(|text: &str| {
let world = FuzzWorld::new(text); let world = FuzzWorld::new(text);
if let Ok(document) = typst::compile(&world).output { if let Ok(document) = typst::compile::<PagedDocument>(&world).output {
if let Some(page) = document.pages.first() { if let Some(page) = document.pages.first() {
std::hint::black_box(typst_render::render(page, 1.0)); std::hint::black_box(typst_render::render(page, 1.0));
} }