summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-12-02 14:19:52 +0100
committerLaurenz <laurmaedje@gmail.com>2024-12-04 10:12:07 +0100
commite0122a5b509d151b7e0197d37a120fd965a055d5 (patch)
tree1045c37c53dc2e08fedc8802cdfec1b244e10dd0
parent885c7d96eea73f478faea9877f0dbc40c00b0d7b (diff)
Add HTML export format
-rw-r--r--Cargo.lock16
-rw-r--r--Cargo.toml1
-rw-r--r--crates/typst-cli/Cargo.toml1
-rw-r--r--crates/typst-cli/src/args.rs1
-rw-r--r--crates/typst-cli/src/compile.rs46
-rw-r--r--crates/typst-html/Cargo.toml26
-rw-r--r--crates/typst-html/src/encode.rs104
-rw-r--r--crates/typst-html/src/lib.rs315
-rw-r--r--crates/typst-ide/src/analyze.rs2
-rw-r--r--crates/typst-layout/src/flow/collect.rs8
-rw-r--r--crates/typst-layout/src/flow/mod.rs2
-rw-r--r--crates/typst-layout/src/pages/mod.rs4
-rw-r--r--crates/typst-library/src/engine.rs14
-rw-r--r--crates/typst-library/src/introspection/introspector.rs129
-rw-r--r--crates/typst-library/src/layout/frame.rs3
-rw-r--r--crates/typst-library/src/routines.rs18
-rw-r--r--crates/typst-realize/src/lib.rs82
-rw-r--r--crates/typst-svg/src/lib.rs11
-rw-r--r--crates/typst/Cargo.toml1
-rw-r--r--crates/typst/src/lib.rs143
-rw-r--r--docs/src/html.rs4
-rw-r--r--tests/fuzz/src/compile.rs3
22 files changed, 824 insertions, 110 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 0afce462..2b902057 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2674,6 +2674,7 @@ dependencies = [
"comemo",
"ecow",
"typst-eval",
+ "typst-html",
"typst-layout",
"typst-library",
"typst-macros",
@@ -2720,6 +2721,7 @@ dependencies = [
"toml",
"typst",
"typst-eval",
+ "typst-html",
"typst-kit",
"typst-macros",
"typst-pdf",
@@ -2788,6 +2790,20 @@ dependencies = [
]
[[package]]
+name = "typst-html"
+version = "0.12.0"
+dependencies = [
+ "comemo",
+ "ecow",
+ "typst-library",
+ "typst-macros",
+ "typst-svg",
+ "typst-syntax",
+ "typst-timing",
+ "typst-utils",
+]
+
+[[package]]
name = "typst-ide"
version = "0.12.0"
dependencies = [
diff --git a/Cargo.toml b/Cargo.toml
index 66885bcf..639136ce 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,6 +19,7 @@ readme = "README.md"
typst = { path = "crates/typst", 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-html = { path = "crates/typst-html", 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-layout = { path = "crates/typst-layout", version = "0.12.0" }
diff --git a/crates/typst-cli/Cargo.toml b/crates/typst-cli/Cargo.toml
index 4b4abc55..12cf94a7 100644
--- a/crates/typst-cli/Cargo.toml
+++ b/crates/typst-cli/Cargo.toml
@@ -20,6 +20,7 @@ doc = false
[dependencies]
typst = { workspace = true }
typst-eval = { workspace = true }
+typst-html = { workspace = true }
typst-kit = { workspace = true }
typst-macros = { workspace = true }
typst-pdf = { workspace = true }
diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs
index bc3d1225..d9ecc921 100644
--- a/crates/typst-cli/src/args.rs
+++ b/crates/typst-cli/src/args.rs
@@ -512,6 +512,7 @@ pub enum OutputFormat {
Pdf,
Png,
Svg,
+ Html,
}
impl Display for OutputFormat {
diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs
index c6d37ffe..2a319424 100644
--- a/crates/typst-cli/src/compile.rs
+++ b/crates/typst-cli/src/compile.rs
@@ -12,6 +12,7 @@ use typst::diag::{
bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned,
};
use typst::foundations::{Datetime, Smart};
+use typst::html::HtmlDocument;
use typst::layout::{Frame, Page, PageRanges, PagedDocument};
use typst::syntax::{FileId, Source, Span};
use typst::WorldExt;
@@ -41,6 +42,7 @@ impl CompileCommand {
OutputFormat::Pdf => "pdf",
OutputFormat::Png => "png",
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("png") => OutputFormat::Png,
Some(ext) if ext.eq_ignore_ascii_case("svg") => OutputFormat::Svg,
+ Some(ext) if ext.eq_ignore_ascii_case("html") => OutputFormat::Html,
_ => bail!(
"could not infer output format for path {}.\n\
consider providing the format manually with `--format/-f`",
@@ -95,9 +98,6 @@ impl CompileCommand {
/// Execute a compilation command.
pub fn compile(mut timer: Timer, mut command: CompileCommand) -> StrResult<()> {
- // Only meant for input validation
- _ = command.output_format()?;
-
let mut world =
SystemWorld::new(&command.common).map_err(|err| eco_format!("{err}"))?;
timer.record(&mut world, |world| compile_once(world, &mut command, false))??;
@@ -113,15 +113,16 @@ pub fn compile_once(
command: &mut CompileCommand,
watching: bool,
) -> StrResult<()> {
+ _ = command.output_format()?;
+
let start = std::time::Instant::now();
if watching {
Status::Compiling.print(command).unwrap();
}
- let Warned { output, warnings } = typst::compile(world);
- let result = output.and_then(|document| export(world, &document, command, watching));
+ let Warned { output, warnings } = compile_and_export(world, command, watching);
- match result {
+ match output {
// Export the PDF / PNG.
Ok(()) => {
let duration = start.elapsed();
@@ -167,14 +168,43 @@ pub fn compile_once(
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.
-fn export(
+fn export_paged(
world: &mut SystemWorld,
document: &PagedDocument,
command: &CompileCommand,
watching: bool,
) -> SourceResult<()> {
match command.output_format().at(Span::detached())? {
+ OutputFormat::Pdf => export_pdf(document, command),
OutputFormat::Png => {
export_image(world, document, command, watching, ImageExportFormat::Png)
.at(Span::detached())
@@ -183,7 +213,7 @@ fn export(
export_image(world, document, command, watching, ImageExportFormat::Svg)
.at(Span::detached())
}
- OutputFormat::Pdf => export_pdf(document, command),
+ OutputFormat::Html => unreachable!(),
}
}
diff --git a/crates/typst-html/Cargo.toml b/crates/typst-html/Cargo.toml
new file mode 100644
index 00000000..534848f9
--- /dev/null
+++ b/crates/typst-html/Cargo.toml
@@ -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
diff --git a/crates/typst-html/src/encode.rs b/crates/typst-html/src/encode.rs
new file mode 100644
index 00000000..d4ff83d6
--- /dev/null
+++ b/crates/typst-html/src/encode.rs
@@ -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);
+}
diff --git a/crates/typst-html/src/lib.rs b/crates/typst-html/src/lib.rs
new file mode 100644
index 00000000..ffd8e250
--- /dev/null
+++ b/crates/typst-html/src/lib.rs
@@ -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>),
+}
diff --git a/crates/typst-ide/src/analyze.rs b/crates/typst-ide/src/analyze.rs
index 0b41fb68..7ee83e70 100644
--- a/crates/typst-ide/src/analyze.rs
+++ b/crates/typst-ide/src/analyze.rs
@@ -37,7 +37,7 @@ pub fn analyze_expr(
}
}
- return typst::trace(world.upcast(), node.span());
+ return typst::trace::<PagedDocument>(world.upcast(), node.span());
}
};
diff --git a/crates/typst-layout/src/flow/collect.rs b/crates/typst-layout/src/flow/collect.rs
index 49461e80..12cfa152 100644
--- a/crates/typst-layout/src/flow/collect.rs
+++ b/crates/typst-layout/src/flow/collect.rs
@@ -5,7 +5,7 @@ use std::hash::Hash;
use bumpalo::boxed::Box as BumpBox;
use bumpalo::Bump;
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::foundations::{Packed, Resolve, Smart, StyleChain};
use typst_library::introspection::{
@@ -83,7 +83,11 @@ impl<'a> Collector<'a, '_, '_> {
hint: "try using a `#colbreak()` instead",
);
} 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()
+ ));
}
}
diff --git a/crates/typst-layout/src/flow/mod.rs b/crates/typst-layout/src/flow/mod.rs
index 7cbec59a..df716b33 100644
--- a/crates/typst-layout/src/flow/mod.rs
+++ b/crates/typst-layout/src/flow/mod.rs
@@ -142,7 +142,7 @@ fn layout_fragment_impl(
let arenas = Arenas::default();
let children = (engine.routines.realize)(
- RealizationKind::Container,
+ RealizationKind::LayoutFragment,
&mut engine,
&mut locator,
&arenas,
diff --git a/crates/typst-layout/src/pages/mod.rs b/crates/typst-layout/src/pages/mod.rs
index 667e16b3..27002a6c 100644
--- a/crates/typst-layout/src/pages/mod.rs
+++ b/crates/typst-layout/src/pages/mod.rs
@@ -75,7 +75,7 @@ fn layout_document_impl(
let arenas = Arenas::default();
let mut info = DocumentInfo::default();
let mut children = (engine.routines.realize)(
- RealizationKind::Root(&mut info),
+ RealizationKind::LayoutDocument(&mut info),
&mut engine,
&mut locator,
&arenas,
@@ -84,7 +84,7 @@ fn layout_document_impl(
)?;
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 })
}
diff --git a/crates/typst-library/src/engine.rs b/crates/typst-library/src/engine.rs
index e532172e..cd25ec48 100644
--- a/crates/typst-library/src/engine.rs
+++ b/crates/typst-library/src/engine.rs
@@ -301,6 +301,9 @@ impl Route<'_> {
/// The maximum layout nesting depth.
const MAX_LAYOUT_DEPTH: usize = 72;
+ /// The maximum HTML nesting depth.
+ const MAX_HTML_DEPTH: usize = 72;
+
/// The maximum function call nesting depth.
const MAX_CALL_DEPTH: usize = 80;
@@ -326,6 +329,17 @@ impl Route<'_> {
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.
pub fn check_call_depth(&self) -> StrResult<()> {
if !self.within(Route::MAX_CALL_DEPTH) {
diff --git a/crates/typst-library/src/introspection/introspector.rs b/crates/typst-library/src/introspection/introspector.rs
index 388d1f00..8cbaea89 100644
--- a/crates/typst-library/src/introspection/introspector.rs
+++ b/crates/typst-library/src/introspection/introspector.rs
@@ -10,6 +10,7 @@ use typst_utils::NonZeroExt;
use crate::diag::{bail, StrResult};
use crate::foundations::{Content, Label, Repr, Selector};
+use crate::html::{HtmlElement, HtmlNode};
use crate::introspection::{Location, Tag};
use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform};
use crate::model::Numbering;
@@ -47,9 +48,15 @@ type Pair = (Content, Position);
impl Introspector {
/// Creates an introspector for a page list.
- #[typst_macros::time(name = "introspect")]
- pub fn new(pages: &[Page]) -> Self {
- IntrospectorBuilder::new().build(pages)
+ #[typst_macros::time(name = "introspect pages")]
+ pub fn paged(pages: &[Page]) -> Self {
+ 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.
@@ -346,6 +353,7 @@ impl Clone for QueryCache {
/// Builds the introspector.
#[derive(Default)]
struct IntrospectorBuilder {
+ pages: usize,
page_numberings: Vec<Option<Numbering>>,
page_supplements: Vec<Content>,
seen: HashSet<Location>,
@@ -361,46 +369,37 @@ impl IntrospectorBuilder {
Self::default()
}
- /// Build the introspector.
- fn build(mut self, pages: &[Page]) -> Introspector {
+ /// Build an introspector for a page list.
+ fn build_paged(mut self, pages: &[Page]) -> Introspector {
+ self.pages = pages.len();
self.page_numberings.reserve(pages.len());
self.page_supplements.reserve(pages.len());
// Discover all elements.
- let mut root = Vec::new();
+ let mut elems = Vec::new();
for (i, page) in pages.iter().enumerate() {
self.page_numberings.push(page.numbering.clone());
self.page_supplements.push(page.supplement.clone());
- self.discover(
- &mut root,
+ self.discover_in_frame(
+ &mut elems,
&page.frame,
NonZeroUsize::new(1 + i).unwrap(),
Transform::identity(),
);
}
- 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);
- }
+ self.finalize(elems)
+ }
- Introspector {
- pages: pages.len(),
- page_numberings: self.page_numberings,
- page_supplements: self.page_supplements,
- elems,
- keys: self.keys,
- locations: self.locations,
- labels: self.labels,
- queries: QueryCache::default(),
- }
+ /// Build an introspector for an HTML document.
+ fn build_html(mut self, root: &HtmlElement) -> Introspector {
+ let mut elems = Vec::new();
+ self.discover_in_html(&mut elems, root);
+ self.finalize(elems)
}
/// Processes the tags in the frame.
- fn discover(
+ fn discover_in_frame(
&mut self,
sink: &mut Vec<Pair>,
frame: &Frame,
@@ -416,27 +415,83 @@ impl IntrospectorBuilder {
if let Some(parent) = group.parent {
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);
} else {
- self.discover(sink, &group.frame, page, ts);
+ self.discover_in_frame(sink, &group.frame, page, ts);
}
}
- FrameItem::Tag(Tag::Start(elem)) => {
- let loc = elem.location().unwrap();
- if self.seen.insert(loc) {
- let point = pos.transform(ts);
- sink.push((elem.clone(), Position { page, point }));
- }
- }
- FrameItem::Tag(Tag::End(loc, key)) => {
- self.keys.insert(*key, *loc);
+ FrameItem::Tag(tag) => {
+ self.discover_in_tag(
+ sink,
+ tag,
+ Position { page, point: pos.transform(ts) },
+ );
}
_ => {}
}
}
}
+ /// 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
/// acceleration structures.
fn visit(&mut self, elems: &mut Vec<Pair>, pair: Pair) {
diff --git a/crates/typst-library/src/layout/frame.rs b/crates/typst-library/src/layout/frame.rs
index 204584fc..fc8634e8 100644
--- a/crates/typst-library/src/layout/frame.rs
+++ b/crates/typst-library/src/layout/frame.rs
@@ -520,8 +520,7 @@ pub enum FrameItem {
Image(Image, Size, Span),
/// An internal or external link to a destination.
Link(Destination, Size),
- /// An introspectable element that produced something within this frame
- /// alongside its key.
+ /// An introspectable element that produced something within this frame.
Tag(Tag),
}
diff --git a/crates/typst-library/src/routines.rs b/crates/typst-library/src/routines.rs
index 000b3bba..aa92012b 100644
--- a/crates/typst-library/src/routines.rs
+++ b/crates/typst-library/src/routines.rs
@@ -86,13 +86,6 @@ routines! {
styles: StyleChain<'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.
fn layout_fragment(
engine: &mut Engine,
@@ -343,11 +336,16 @@ pub enum EvalMode {
/// Defines what kind of realization we are performing.
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.
+ LayoutDocument(&'a mut DocumentInfo),
+ /// A nested realization in a container (e.g. a `block`).
+ LayoutFragment,
+ /// This the root realization for HTML. Requires a mutable reference
/// to document metadata that will be filled from `set document` rules.
- Root(&'a mut DocumentInfo),
+ HtmlDocument(&'a mut DocumentInfo),
/// A nested realization in a container (e.g. a `block`).
- Container,
+ HtmlFragment,
/// A realization within math.
Math,
}
diff --git a/crates/typst-realize/src/lib.rs b/crates/typst-realize/src/lib.rs
index ec7ee0ae..c46a1535 100644
--- a/crates/typst-realize/src/lib.rs
+++ b/crates/typst-realize/src/lib.rs
@@ -18,6 +18,7 @@ use typst_library::foundations::{
SequenceElem, Show, ShowSet, Style, StyleChain, StyleVec, StyledElem, Styles,
Synthesize, Transformation,
};
+use typst_library::html::{tag, HtmlElem};
use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem};
use typst_library::layout::{
AlignElem, BoxElem, HElem, InlineElem, PageElem, PagebreakElem, VElem,
@@ -47,12 +48,16 @@ pub fn realize<'a>(
locator,
arenas,
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,
},
sink: vec![],
groupings: ArrayVec::new(),
- outside: matches!(kind, RealizationKind::Root(_)),
+ outside: matches!(kind, RealizationKind::LayoutDocument(_)),
may_attach: false,
kind,
};
@@ -105,10 +110,10 @@ struct GroupingRule {
/// be visible to `finish`.
tags: bool,
/// 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
/// not at the edges.
- inner: fn(Element) -> bool,
+ inner: fn(&Content) -> bool,
/// Defines whether styles for this kind of element interrupt the grouping.
interrupt: fn(Element) -> bool,
/// Should convert the accumulated elements in `s.sink[start..]` into
@@ -555,14 +560,16 @@ fn visit_styled<'a>(
for style in local.iter() {
let Some(elem) = style.element() else { continue };
if elem == DocumentElem::elem() {
- let RealizationKind::Root(info) = &mut s.kind else {
- let span = style.span();
- bail!(span, "document set rules are not allowed inside of containers");
- };
-
- info.populate(&local);
+ match &mut s.kind {
+ RealizationKind::LayoutDocument(info)
+ | RealizationKind::HtmlDocument(info) => info.populate(&local),
+ _ => bail!(
+ style.span(),
+ "document set rules are not allowed inside of containers"
+ ),
+ }
} else if elem == PageElem::elem() {
- let RealizationKind::Root(_) = s.kind else {
+ let RealizationKind::LayoutDocument(_) = s.kind else {
let span = style.span();
bail!(span, "page configuration is not allowed inside of containers");
};
@@ -618,8 +625,7 @@ fn visit_grouping_rules<'a>(
content: &'a Content,
styles: StyleChain<'a>,
) -> SourceResult<bool> {
- let elem = content.elem();
- let matching = s.rules.iter().find(|&rule| (rule.trigger)(elem));
+ let matching = s.rules.iter().find(|&rule| (rule.trigger)(content, &s.kind));
// Try to continue or finish an existing grouping.
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 (active.rule.trigger)(elem) || (active.rule.inner)(elem) {
+ if (active.rule.trigger)(content, &s.kind) || (active.rule.inner)(content) {
s.sink.push((content, styles));
return Ok(true);
}
@@ -655,7 +661,9 @@ fn visit_filter_rules<'a>(
content: &'a Content,
styles: StyleChain<'a>,
) -> 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
// grouper don't interest us.
return Ok(true);
@@ -730,7 +738,7 @@ fn finish_innermost_grouping(s: &mut State) -> SourceResult<()> {
let Grouping { start, rule } = s.groupings.pop().unwrap();
// 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 tail = s.store_slice(&s.sink[end..]);
s.sink.truncate(end);
@@ -768,22 +776,30 @@ fn finish_innermost_grouping(s: &mut State) -> SourceResult<()> {
/// number of unique priority levels.
const MAX_GROUP_NESTING: usize = 3;
-/// Grouping rules used in normal realizations.
-static NORMAL_RULES: &[&GroupingRule] = &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS];
+/// Grouping rules used in layout realization.
+static LAYOUT_RULES: &[&GroupingRule] = &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS];
+
+/// Grouping rules used in HTML root realization.
+static HTML_DOCUMENT_RULES: &[&GroupingRule] =
+ &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS];
-/// Grouping rules used in math realization.
+/// 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];
/// Groups adjacent textual elements for text show rule application.
static TEXTUAL: GroupingRule = GroupingRule {
priority: 3,
tags: true,
- trigger: |elem| {
+ trigger: |content, _| {
+ let elem = content.elem();
elem == TextElem::elem()
|| elem == LinebreakElem::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
// rules cannot match over style changes anyway.
interrupt: |_| true,
@@ -794,15 +810,22 @@ static TEXTUAL: GroupingRule = GroupingRule {
static PAR: GroupingRule = GroupingRule {
priority: 1,
tags: true,
- trigger: |elem| {
+ trigger: |content, kind| {
+ let elem = content.elem();
elem == TextElem::elem()
|| elem == HElem::elem()
|| elem == LinebreakElem::elem()
|| elem == SmartQuoteElem::elem()
|| elem == InlineElem::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(),
finish: finish_par,
};
@@ -811,8 +834,8 @@ static PAR: GroupingRule = GroupingRule {
static CITES: GroupingRule = GroupingRule {
priority: 2,
tags: false,
- trigger: |elem| elem == CiteElem::elem(),
- inner: |elem| elem == SpaceElem::elem(),
+ trigger: |content, _| content.elem() == CiteElem::elem(),
+ inner: |content| content.elem() == SpaceElem::elem(),
interrupt: |elem| elem == CiteGroup::elem(),
finish: finish_cites,
};
@@ -831,8 +854,11 @@ const fn list_like_grouping<T: ListLike>() -> GroupingRule {
GroupingRule {
priority: 2,
tags: false,
- trigger: |elem| elem == T::Item::elem(),
- inner: |elem| elem == SpaceElem::elem() || elem == ParbreakElem::elem(),
+ trigger: |content, _| content.elem() == T::Item::elem(),
+ inner: |content| {
+ let elem = content.elem();
+ elem == SpaceElem::elem() || elem == ParbreakElem::elem()
+ },
interrupt: |elem| elem == T::elem(),
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
// transparently become part of it.
// 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 });
}
diff --git a/crates/typst-svg/src/lib.rs b/crates/typst-svg/src/lib.rs
index f9ce4b86..b21fe4fb 100644
--- a/crates/typst-svg/src/lib.rs
+++ b/crates/typst-svg/src/lib.rs
@@ -32,6 +32,17 @@ pub fn svg(page: &Page) -> String {
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.
///
/// The padding will be added around and between the individual frames.
diff --git a/crates/typst/Cargo.toml b/crates/typst/Cargo.toml
index 6158d791..3eb6dea9 100644
--- a/crates/typst/Cargo.toml
+++ b/crates/typst/Cargo.toml
@@ -14,6 +14,7 @@ readme = { workspace = true }
[dependencies]
typst-eval = { workspace = true }
+typst-html = { workspace = true }
typst-layout = { workspace = true }
typst-library = { workspace = true }
typst-macros = { workspace = true }
diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs
index feb17ba9..2fb0a730 100644
--- a/crates/typst/src/lib.rs
+++ b/crates/typst/src/lib.rs
@@ -13,11 +13,11 @@
//! order-independent and thus much better suited for further processing than
//! the raw markup.
//! - **Layouting:**
-//! Next, the content is [laid out] into a [document] containing one [frame]
-//! per page with items at fixed positions.
+//! Next, the content is [laid out] into a [`PagedDocument`] containing one
+//! [frame] per page with items at fixed positions.
//! - **Exporting:**
//! These frames can finally be exported into an output format (currently PDF,
-//! PNG, or SVG).
+//! PNG, SVG, and HTML).
//!
//! [tokens]: typst_syntax::SyntaxKind
//! [parsed]: typst_syntax::parse
@@ -43,23 +43,32 @@ use std::collections::HashSet;
use comemo::{Track, Tracked, Validate};
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::foundations::{StyleChain, Styles, Value};
+use typst_library::html::HtmlDocument;
use typst_library::introspection::Introspector;
use typst_library::layout::PagedDocument;
use typst_library::routines::Routines;
use typst_syntax::{FileId, Span};
use typst_timing::{timed, TimingScope};
+use crate::foundations::{Target, TargetElem};
+use crate::model::DocumentInfo;
+
/// Compile sources into a fully layouted document.
///
/// - Returns `Ok(document)` if there were no fatal errors.
/// - Returns `Err(errors)` if there were fatal errors.
#[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 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);
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
/// `span` during compilation.
#[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 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()
}
/// The internal implementation of `compile` with a bit lower-level interface
/// that is also used by `trace`.
-fn compile_impl(
+fn compile_impl<D: Document>(
world: Tracked<dyn World + '_>,
traced: Tracked<Traced>,
sink: &mut Sink,
-) -> SourceResult<PagedDocument> {
+) -> SourceResult<D> {
+ if D::TARGET == Target::Html {
+ warn_or_error_for_html(world, sink)?;
+ }
+
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.
let main = world.main();
@@ -103,7 +122,8 @@ fn compile_impl(
let mut iter = 0;
let mut subsink;
- let mut document = PagedDocument::default();
+ let mut introspector = &empty_introspector;
+ let mut document: D;
// Relayout until all introspections stabilize.
// 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 mut engine = Engine {
world,
- introspector: document.introspector.track_with(&constraint),
+ introspector: introspector.track_with(&constraint),
traced,
sink: subsink.track_mut(),
route: Route::default(),
@@ -126,10 +146,11 @@ fn compile_impl(
};
// Layout!
- document = (engine.routines.layout_document)(&mut engine, &content, styles)?;
+ document = D::create(&mut engine, &content, styles)?;
+ introspector = document.introspector();
iter += 1;
- if timed!("check stabilized", document.introspector.validate(&constraint)) {
+ if timed!("check stabilized", introspector.validate(&constraint)) {
break;
}
@@ -208,6 +229,97 @@ fn hint_invalid_main_file(
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
/// function pointers.
///
@@ -216,7 +328,6 @@ pub static ROUTINES: Routines = Routines {
eval_string: typst_eval::eval_string,
eval_closure: typst_eval::eval_closure,
realize: typst_realize::realize,
- layout_document: typst_layout::layout_document,
layout_fragment: typst_layout::layout_fragment,
layout_frame: typst_layout::layout_frame,
layout_inline: typst_layout::layout_inline,
diff --git a/docs/src/html.rs b/docs/src/html.rs
index c9ed7a7f..a1206032 100644
--- a/docs/src/html.rs
+++ b/docs/src/html.rs
@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
use typed_arena::Arena;
use typst::diag::{FileError, FileResult, StrResult};
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::text::{Font, FontBook};
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 world = DocWorld(source);
- let mut document = match typst::compile(&world).output {
+ let mut document = match typst::compile::<PagedDocument>(&world).output {
Ok(doc) => doc,
Err(err) => {
let msg = &err[0].message;
diff --git a/tests/fuzz/src/compile.rs b/tests/fuzz/src/compile.rs
index fa939781..37e21deb 100644
--- a/tests/fuzz/src/compile.rs
+++ b/tests/fuzz/src/compile.rs
@@ -3,6 +3,7 @@
use libfuzzer_sys::fuzz_target;
use typst::diag::{FileError, FileResult};
use typst::foundations::{Bytes, Datetime};
+use typst::layout::PagedDocument;
use typst::syntax::{FileId, Source};
use typst::text::{Font, FontBook};
use typst::utils::LazyHash;
@@ -65,7 +66,7 @@ impl World for FuzzWorld {
fuzz_target!(|text: &str| {
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() {
std::hint::black_box(typst_render::render(page, 1.0));
}