mirror of
https://github.com/typst/typst
synced 2025-05-13 20:46:23 +08:00
Add HTML export format
This commit is contained in:
parent
885c7d96ee
commit
e0122a5b50
16
Cargo.lock
generated
16
Cargo.lock
generated
@ -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",
|
||||
@ -2787,6 +2789,20 @@ dependencies = [
|
||||
"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]]
|
||||
name = "typst-ide"
|
||||
version = "0.12.0"
|
||||
|
@ -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" }
|
||||
|
@ -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 }
|
||||
|
@ -512,6 +512,7 @@ pub enum OutputFormat {
|
||||
Pdf,
|
||||
Png,
|
||||
Svg,
|
||||
Html,
|
||||
}
|
||||
|
||||
impl Display for OutputFormat {
|
||||
|
@ -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!(),
|
||||
}
|
||||
}
|
||||
|
||||
|
26
crates/typst-html/Cargo.toml
Normal file
26
crates/typst-html/Cargo.toml
Normal 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
|
104
crates/typst-html/src/encode.rs
Normal file
104
crates/typst-html/src/encode.rs
Normal 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("&"),
|
||||
'<' => w.buf.push_str("<"),
|
||||
'>' => w.buf.push_str(">"),
|
||||
'"' => w.buf.push_str("""),
|
||||
'\'' => w.buf.push_str("'"),
|
||||
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);
|
||||
}
|
315
crates/typst-html/src/lib.rs
Normal file
315
crates/typst-html/src/lib.rs
Normal 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>),
|
||||
}
|
@ -37,7 +37,7 @@ pub fn analyze_expr(
|
||||
}
|
||||
}
|
||||
|
||||
return typst::trace(world.upcast(), node.span());
|
||||
return typst::trace::<PagedDocument>(world.upcast(), node.span());
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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 })
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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),
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
Root(&'a mut DocumentInfo),
|
||||
LayoutDocument(&'a mut DocumentInfo),
|
||||
/// 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.
|
||||
Math,
|
||||
}
|
||||
|
@ -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 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];
|
||||
|
||||
/// 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 });
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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 }
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user