mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +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",
|
"comemo",
|
||||||
"ecow",
|
"ecow",
|
||||||
"typst-eval",
|
"typst-eval",
|
||||||
|
"typst-html",
|
||||||
"typst-layout",
|
"typst-layout",
|
||||||
"typst-library",
|
"typst-library",
|
||||||
"typst-macros",
|
"typst-macros",
|
||||||
@ -2720,6 +2721,7 @@ dependencies = [
|
|||||||
"toml",
|
"toml",
|
||||||
"typst",
|
"typst",
|
||||||
"typst-eval",
|
"typst-eval",
|
||||||
|
"typst-html",
|
||||||
"typst-kit",
|
"typst-kit",
|
||||||
"typst-macros",
|
"typst-macros",
|
||||||
"typst-pdf",
|
"typst-pdf",
|
||||||
@ -2787,6 +2789,20 @@ dependencies = [
|
|||||||
"typst-syntax",
|
"typst-syntax",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typst-html"
|
||||||
|
version = "0.12.0"
|
||||||
|
dependencies = [
|
||||||
|
"comemo",
|
||||||
|
"ecow",
|
||||||
|
"typst-library",
|
||||||
|
"typst-macros",
|
||||||
|
"typst-svg",
|
||||||
|
"typst-syntax",
|
||||||
|
"typst-timing",
|
||||||
|
"typst-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typst-ide"
|
name = "typst-ide"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
|
@ -19,6 +19,7 @@ readme = "README.md"
|
|||||||
typst = { path = "crates/typst", version = "0.12.0" }
|
typst = { path = "crates/typst", version = "0.12.0" }
|
||||||
typst-cli = { path = "crates/typst-cli", version = "0.12.0" }
|
typst-cli = { path = "crates/typst-cli", version = "0.12.0" }
|
||||||
typst-eval = { path = "crates/typst-eval", version = "0.12.0" }
|
typst-eval = { path = "crates/typst-eval", version = "0.12.0" }
|
||||||
|
typst-html = { path = "crates/typst-html", version = "0.12.0" }
|
||||||
typst-ide = { path = "crates/typst-ide", version = "0.12.0" }
|
typst-ide = { path = "crates/typst-ide", version = "0.12.0" }
|
||||||
typst-kit = { path = "crates/typst-kit", version = "0.12.0" }
|
typst-kit = { path = "crates/typst-kit", version = "0.12.0" }
|
||||||
typst-layout = { path = "crates/typst-layout", version = "0.12.0" }
|
typst-layout = { path = "crates/typst-layout", version = "0.12.0" }
|
||||||
|
@ -20,6 +20,7 @@ doc = false
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
typst = { workspace = true }
|
typst = { workspace = true }
|
||||||
typst-eval = { workspace = true }
|
typst-eval = { workspace = true }
|
||||||
|
typst-html = { workspace = true }
|
||||||
typst-kit = { workspace = true }
|
typst-kit = { workspace = true }
|
||||||
typst-macros = { workspace = true }
|
typst-macros = { workspace = true }
|
||||||
typst-pdf = { workspace = true }
|
typst-pdf = { workspace = true }
|
||||||
|
@ -512,6 +512,7 @@ pub enum OutputFormat {
|
|||||||
Pdf,
|
Pdf,
|
||||||
Png,
|
Png,
|
||||||
Svg,
|
Svg,
|
||||||
|
Html,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for OutputFormat {
|
impl Display for OutputFormat {
|
||||||
|
@ -12,6 +12,7 @@ use typst::diag::{
|
|||||||
bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned,
|
bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned,
|
||||||
};
|
};
|
||||||
use typst::foundations::{Datetime, Smart};
|
use typst::foundations::{Datetime, Smart};
|
||||||
|
use typst::html::HtmlDocument;
|
||||||
use typst::layout::{Frame, Page, PageRanges, PagedDocument};
|
use typst::layout::{Frame, Page, PageRanges, PagedDocument};
|
||||||
use typst::syntax::{FileId, Source, Span};
|
use typst::syntax::{FileId, Source, Span};
|
||||||
use typst::WorldExt;
|
use typst::WorldExt;
|
||||||
@ -41,6 +42,7 @@ impl CompileCommand {
|
|||||||
OutputFormat::Pdf => "pdf",
|
OutputFormat::Pdf => "pdf",
|
||||||
OutputFormat::Png => "png",
|
OutputFormat::Png => "png",
|
||||||
OutputFormat::Svg => "svg",
|
OutputFormat::Svg => "svg",
|
||||||
|
OutputFormat::Html => "html",
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
@ -57,6 +59,7 @@ impl CompileCommand {
|
|||||||
Some(ext) if ext.eq_ignore_ascii_case("pdf") => OutputFormat::Pdf,
|
Some(ext) if ext.eq_ignore_ascii_case("pdf") => OutputFormat::Pdf,
|
||||||
Some(ext) if ext.eq_ignore_ascii_case("png") => OutputFormat::Png,
|
Some(ext) if ext.eq_ignore_ascii_case("png") => OutputFormat::Png,
|
||||||
Some(ext) if ext.eq_ignore_ascii_case("svg") => OutputFormat::Svg,
|
Some(ext) if ext.eq_ignore_ascii_case("svg") => OutputFormat::Svg,
|
||||||
|
Some(ext) if ext.eq_ignore_ascii_case("html") => OutputFormat::Html,
|
||||||
_ => bail!(
|
_ => bail!(
|
||||||
"could not infer output format for path {}.\n\
|
"could not infer output format for path {}.\n\
|
||||||
consider providing the format manually with `--format/-f`",
|
consider providing the format manually with `--format/-f`",
|
||||||
@ -95,9 +98,6 @@ impl CompileCommand {
|
|||||||
|
|
||||||
/// Execute a compilation command.
|
/// Execute a compilation command.
|
||||||
pub fn compile(mut timer: Timer, mut command: CompileCommand) -> StrResult<()> {
|
pub fn compile(mut timer: Timer, mut command: CompileCommand) -> StrResult<()> {
|
||||||
// Only meant for input validation
|
|
||||||
_ = command.output_format()?;
|
|
||||||
|
|
||||||
let mut world =
|
let mut world =
|
||||||
SystemWorld::new(&command.common).map_err(|err| eco_format!("{err}"))?;
|
SystemWorld::new(&command.common).map_err(|err| eco_format!("{err}"))?;
|
||||||
timer.record(&mut world, |world| compile_once(world, &mut command, false))??;
|
timer.record(&mut world, |world| compile_once(world, &mut command, false))??;
|
||||||
@ -113,15 +113,16 @@ pub fn compile_once(
|
|||||||
command: &mut CompileCommand,
|
command: &mut CompileCommand,
|
||||||
watching: bool,
|
watching: bool,
|
||||||
) -> StrResult<()> {
|
) -> StrResult<()> {
|
||||||
|
_ = command.output_format()?;
|
||||||
|
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
if watching {
|
if watching {
|
||||||
Status::Compiling.print(command).unwrap();
|
Status::Compiling.print(command).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let Warned { output, warnings } = typst::compile(world);
|
let Warned { output, warnings } = compile_and_export(world, command, watching);
|
||||||
let result = output.and_then(|document| export(world, &document, command, watching));
|
|
||||||
|
|
||||||
match result {
|
match output {
|
||||||
// Export the PDF / PNG.
|
// Export the PDF / PNG.
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let duration = start.elapsed();
|
let duration = start.elapsed();
|
||||||
@ -167,14 +168,43 @@ pub fn compile_once(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn compile_and_export(
|
||||||
|
world: &mut SystemWorld,
|
||||||
|
command: &mut CompileCommand,
|
||||||
|
watching: bool,
|
||||||
|
) -> Warned<SourceResult<()>> {
|
||||||
|
let format = command.output_format().unwrap();
|
||||||
|
|
||||||
|
match format {
|
||||||
|
OutputFormat::Html => {
|
||||||
|
let Warned { output, warnings } = typst::compile::<HtmlDocument>(world);
|
||||||
|
let result = output.and_then(|document| {
|
||||||
|
command
|
||||||
|
.output()
|
||||||
|
.write(typst_html::html(&document)?.as_bytes())
|
||||||
|
.map_err(|err| eco_format!("failed to write HTML file ({err})"))
|
||||||
|
.at(Span::detached())
|
||||||
|
});
|
||||||
|
Warned { output: result, warnings }
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let Warned { output, warnings } = typst::compile::<PagedDocument>(world);
|
||||||
|
let result = output
|
||||||
|
.and_then(|document| export_paged(world, &document, command, watching));
|
||||||
|
Warned { output: result, warnings }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Export into the target format.
|
/// Export into the target format.
|
||||||
fn export(
|
fn export_paged(
|
||||||
world: &mut SystemWorld,
|
world: &mut SystemWorld,
|
||||||
document: &PagedDocument,
|
document: &PagedDocument,
|
||||||
command: &CompileCommand,
|
command: &CompileCommand,
|
||||||
watching: bool,
|
watching: bool,
|
||||||
) -> SourceResult<()> {
|
) -> SourceResult<()> {
|
||||||
match command.output_format().at(Span::detached())? {
|
match command.output_format().at(Span::detached())? {
|
||||||
|
OutputFormat::Pdf => export_pdf(document, command),
|
||||||
OutputFormat::Png => {
|
OutputFormat::Png => {
|
||||||
export_image(world, document, command, watching, ImageExportFormat::Png)
|
export_image(world, document, command, watching, ImageExportFormat::Png)
|
||||||
.at(Span::detached())
|
.at(Span::detached())
|
||||||
@ -183,7 +213,7 @@ fn export(
|
|||||||
export_image(world, document, command, watching, ImageExportFormat::Svg)
|
export_image(world, document, command, watching, ImageExportFormat::Svg)
|
||||||
.at(Span::detached())
|
.at(Span::detached())
|
||||||
}
|
}
|
||||||
OutputFormat::Pdf => export_pdf(document, command),
|
OutputFormat::Html => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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::boxed::Box as BumpBox;
|
||||||
use bumpalo::Bump;
|
use bumpalo::Bump;
|
||||||
use comemo::{Track, Tracked, TrackedMut};
|
use comemo::{Track, Tracked, TrackedMut};
|
||||||
use typst_library::diag::{bail, SourceResult};
|
use typst_library::diag::{bail, warning, SourceResult};
|
||||||
use typst_library::engine::{Engine, Route, Sink, Traced};
|
use typst_library::engine::{Engine, Route, Sink, Traced};
|
||||||
use typst_library::foundations::{Packed, Resolve, Smart, StyleChain};
|
use typst_library::foundations::{Packed, Resolve, Smart, StyleChain};
|
||||||
use typst_library::introspection::{
|
use typst_library::introspection::{
|
||||||
@ -83,7 +83,11 @@ impl<'a> Collector<'a, '_, '_> {
|
|||||||
hint: "try using a `#colbreak()` instead",
|
hint: "try using a `#colbreak()` instead",
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
bail!(child.span(), "{} is not allowed here", child.func().name());
|
self.engine.sink.warn(warning!(
|
||||||
|
child.span(),
|
||||||
|
"{} was ignored during paged export",
|
||||||
|
child.func().name()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,7 +142,7 @@ fn layout_fragment_impl(
|
|||||||
|
|
||||||
let arenas = Arenas::default();
|
let arenas = Arenas::default();
|
||||||
let children = (engine.routines.realize)(
|
let children = (engine.routines.realize)(
|
||||||
RealizationKind::Container,
|
RealizationKind::LayoutFragment,
|
||||||
&mut engine,
|
&mut engine,
|
||||||
&mut locator,
|
&mut locator,
|
||||||
&arenas,
|
&arenas,
|
||||||
|
@ -75,7 +75,7 @@ fn layout_document_impl(
|
|||||||
let arenas = Arenas::default();
|
let arenas = Arenas::default();
|
||||||
let mut info = DocumentInfo::default();
|
let mut info = DocumentInfo::default();
|
||||||
let mut children = (engine.routines.realize)(
|
let mut children = (engine.routines.realize)(
|
||||||
RealizationKind::Root(&mut info),
|
RealizationKind::LayoutDocument(&mut info),
|
||||||
&mut engine,
|
&mut engine,
|
||||||
&mut locator,
|
&mut locator,
|
||||||
&arenas,
|
&arenas,
|
||||||
@ -84,7 +84,7 @@ fn layout_document_impl(
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
let pages = layout_pages(&mut engine, &mut children, locator, styles)?;
|
let pages = layout_pages(&mut engine, &mut children, locator, styles)?;
|
||||||
let introspector = Introspector::new(&pages);
|
let introspector = Introspector::paged(&pages);
|
||||||
|
|
||||||
Ok(PagedDocument { pages, info, introspector })
|
Ok(PagedDocument { pages, info, introspector })
|
||||||
}
|
}
|
||||||
|
@ -301,6 +301,9 @@ impl Route<'_> {
|
|||||||
/// The maximum layout nesting depth.
|
/// The maximum layout nesting depth.
|
||||||
const MAX_LAYOUT_DEPTH: usize = 72;
|
const MAX_LAYOUT_DEPTH: usize = 72;
|
||||||
|
|
||||||
|
/// The maximum HTML nesting depth.
|
||||||
|
const MAX_HTML_DEPTH: usize = 72;
|
||||||
|
|
||||||
/// The maximum function call nesting depth.
|
/// The maximum function call nesting depth.
|
||||||
const MAX_CALL_DEPTH: usize = 80;
|
const MAX_CALL_DEPTH: usize = 80;
|
||||||
|
|
||||||
@ -326,6 +329,17 @@ impl Route<'_> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ensures that we are within the maximum HTML depth.
|
||||||
|
pub fn check_html_depth(&self) -> HintedStrResult<()> {
|
||||||
|
if !self.within(Route::MAX_HTML_DEPTH) {
|
||||||
|
bail!(
|
||||||
|
"maximum HTML depth exceeded";
|
||||||
|
hint: "try to reduce the amount of nesting of your HTML",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Ensures that we are within the maximum function call depth.
|
/// Ensures that we are within the maximum function call depth.
|
||||||
pub fn check_call_depth(&self) -> StrResult<()> {
|
pub fn check_call_depth(&self) -> StrResult<()> {
|
||||||
if !self.within(Route::MAX_CALL_DEPTH) {
|
if !self.within(Route::MAX_CALL_DEPTH) {
|
||||||
|
@ -10,6 +10,7 @@ use typst_utils::NonZeroExt;
|
|||||||
|
|
||||||
use crate::diag::{bail, StrResult};
|
use crate::diag::{bail, StrResult};
|
||||||
use crate::foundations::{Content, Label, Repr, Selector};
|
use crate::foundations::{Content, Label, Repr, Selector};
|
||||||
|
use crate::html::{HtmlElement, HtmlNode};
|
||||||
use crate::introspection::{Location, Tag};
|
use crate::introspection::{Location, Tag};
|
||||||
use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform};
|
use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform};
|
||||||
use crate::model::Numbering;
|
use crate::model::Numbering;
|
||||||
@ -47,9 +48,15 @@ type Pair = (Content, Position);
|
|||||||
|
|
||||||
impl Introspector {
|
impl Introspector {
|
||||||
/// Creates an introspector for a page list.
|
/// Creates an introspector for a page list.
|
||||||
#[typst_macros::time(name = "introspect")]
|
#[typst_macros::time(name = "introspect pages")]
|
||||||
pub fn new(pages: &[Page]) -> Self {
|
pub fn paged(pages: &[Page]) -> Self {
|
||||||
IntrospectorBuilder::new().build(pages)
|
IntrospectorBuilder::new().build_paged(pages)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an introspector for HTML.
|
||||||
|
#[typst_macros::time(name = "introspect html")]
|
||||||
|
pub fn html(root: &HtmlElement) -> Self {
|
||||||
|
IntrospectorBuilder::new().build_html(root)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterates over all locatable elements.
|
/// Iterates over all locatable elements.
|
||||||
@ -346,6 +353,7 @@ impl Clone for QueryCache {
|
|||||||
/// Builds the introspector.
|
/// Builds the introspector.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct IntrospectorBuilder {
|
struct IntrospectorBuilder {
|
||||||
|
pages: usize,
|
||||||
page_numberings: Vec<Option<Numbering>>,
|
page_numberings: Vec<Option<Numbering>>,
|
||||||
page_supplements: Vec<Content>,
|
page_supplements: Vec<Content>,
|
||||||
seen: HashSet<Location>,
|
seen: HashSet<Location>,
|
||||||
@ -361,46 +369,37 @@ impl IntrospectorBuilder {
|
|||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the introspector.
|
/// Build an introspector for a page list.
|
||||||
fn build(mut self, pages: &[Page]) -> Introspector {
|
fn build_paged(mut self, pages: &[Page]) -> Introspector {
|
||||||
|
self.pages = pages.len();
|
||||||
self.page_numberings.reserve(pages.len());
|
self.page_numberings.reserve(pages.len());
|
||||||
self.page_supplements.reserve(pages.len());
|
self.page_supplements.reserve(pages.len());
|
||||||
|
|
||||||
// Discover all elements.
|
// Discover all elements.
|
||||||
let mut root = Vec::new();
|
let mut elems = Vec::new();
|
||||||
for (i, page) in pages.iter().enumerate() {
|
for (i, page) in pages.iter().enumerate() {
|
||||||
self.page_numberings.push(page.numbering.clone());
|
self.page_numberings.push(page.numbering.clone());
|
||||||
self.page_supplements.push(page.supplement.clone());
|
self.page_supplements.push(page.supplement.clone());
|
||||||
self.discover(
|
self.discover_in_frame(
|
||||||
&mut root,
|
&mut elems,
|
||||||
&page.frame,
|
&page.frame,
|
||||||
NonZeroUsize::new(1 + i).unwrap(),
|
NonZeroUsize::new(1 + i).unwrap(),
|
||||||
Transform::identity(),
|
Transform::identity(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.locations.reserve(self.seen.len());
|
self.finalize(elems)
|
||||||
|
|
||||||
// Save all pairs and their descendants in the correct order.
|
|
||||||
let mut elems = Vec::with_capacity(self.seen.len());
|
|
||||||
for pair in root {
|
|
||||||
self.visit(&mut elems, pair);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Introspector {
|
/// Build an introspector for an HTML document.
|
||||||
pages: pages.len(),
|
fn build_html(mut self, root: &HtmlElement) -> Introspector {
|
||||||
page_numberings: self.page_numberings,
|
let mut elems = Vec::new();
|
||||||
page_supplements: self.page_supplements,
|
self.discover_in_html(&mut elems, root);
|
||||||
elems,
|
self.finalize(elems)
|
||||||
keys: self.keys,
|
|
||||||
locations: self.locations,
|
|
||||||
labels: self.labels,
|
|
||||||
queries: QueryCache::default(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Processes the tags in the frame.
|
/// Processes the tags in the frame.
|
||||||
fn discover(
|
fn discover_in_frame(
|
||||||
&mut self,
|
&mut self,
|
||||||
sink: &mut Vec<Pair>,
|
sink: &mut Vec<Pair>,
|
||||||
frame: &Frame,
|
frame: &Frame,
|
||||||
@ -416,27 +415,83 @@ impl IntrospectorBuilder {
|
|||||||
|
|
||||||
if let Some(parent) = group.parent {
|
if let Some(parent) = group.parent {
|
||||||
let mut nested = vec![];
|
let mut nested = vec![];
|
||||||
self.discover(&mut nested, &group.frame, page, ts);
|
self.discover_in_frame(&mut nested, &group.frame, page, ts);
|
||||||
self.insertions.insert(parent, nested);
|
self.insertions.insert(parent, nested);
|
||||||
} else {
|
} else {
|
||||||
self.discover(sink, &group.frame, page, ts);
|
self.discover_in_frame(sink, &group.frame, page, ts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FrameItem::Tag(Tag::Start(elem)) => {
|
FrameItem::Tag(tag) => {
|
||||||
let loc = elem.location().unwrap();
|
self.discover_in_tag(
|
||||||
if self.seen.insert(loc) {
|
sink,
|
||||||
let point = pos.transform(ts);
|
tag,
|
||||||
sink.push((elem.clone(), Position { page, point }));
|
Position { page, point: pos.transform(ts) },
|
||||||
}
|
);
|
||||||
}
|
|
||||||
FrameItem::Tag(Tag::End(loc, key)) => {
|
|
||||||
self.keys.insert(*key, *loc);
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Processes the tags in the HTML element.
|
||||||
|
fn discover_in_html(&mut self, sink: &mut Vec<Pair>, elem: &HtmlElement) {
|
||||||
|
for child in &elem.children {
|
||||||
|
match child {
|
||||||
|
HtmlNode::Tag(tag) => self.discover_in_tag(
|
||||||
|
sink,
|
||||||
|
tag,
|
||||||
|
Position { page: NonZeroUsize::ONE, point: Point::zero() },
|
||||||
|
),
|
||||||
|
HtmlNode::Text(_, _) => {}
|
||||||
|
HtmlNode::Element(elem) => self.discover_in_html(sink, elem),
|
||||||
|
HtmlNode::Frame(frame) => self.discover_in_frame(
|
||||||
|
sink,
|
||||||
|
frame,
|
||||||
|
NonZeroUsize::ONE,
|
||||||
|
Transform::identity(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle a tag.
|
||||||
|
fn discover_in_tag(&mut self, sink: &mut Vec<Pair>, tag: &Tag, position: Position) {
|
||||||
|
match tag {
|
||||||
|
Tag::Start(elem) => {
|
||||||
|
let loc = elem.location().unwrap();
|
||||||
|
if self.seen.insert(loc) {
|
||||||
|
sink.push((elem.clone(), position));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Tag::End(loc, key) => {
|
||||||
|
self.keys.insert(*key, *loc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a complete introspector with all acceleration structures from a
|
||||||
|
/// list of top-level pairs.
|
||||||
|
fn finalize(mut self, root: Vec<Pair>) -> Introspector {
|
||||||
|
self.locations.reserve(self.seen.len());
|
||||||
|
|
||||||
|
// Save all pairs and their descendants in the correct order.
|
||||||
|
let mut elems = Vec::with_capacity(self.seen.len());
|
||||||
|
for pair in root {
|
||||||
|
self.visit(&mut elems, pair);
|
||||||
|
}
|
||||||
|
|
||||||
|
Introspector {
|
||||||
|
pages: self.pages,
|
||||||
|
page_numberings: self.page_numberings,
|
||||||
|
page_supplements: self.page_supplements,
|
||||||
|
elems,
|
||||||
|
keys: self.keys,
|
||||||
|
locations: self.locations,
|
||||||
|
labels: self.labels,
|
||||||
|
queries: QueryCache::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Saves a pair and all its descendants into `elems` and populates the
|
/// Saves a pair and all its descendants into `elems` and populates the
|
||||||
/// acceleration structures.
|
/// acceleration structures.
|
||||||
fn visit(&mut self, elems: &mut Vec<Pair>, pair: Pair) {
|
fn visit(&mut self, elems: &mut Vec<Pair>, pair: Pair) {
|
||||||
|
@ -520,8 +520,7 @@ pub enum FrameItem {
|
|||||||
Image(Image, Size, Span),
|
Image(Image, Size, Span),
|
||||||
/// An internal or external link to a destination.
|
/// An internal or external link to a destination.
|
||||||
Link(Destination, Size),
|
Link(Destination, Size),
|
||||||
/// An introspectable element that produced something within this frame
|
/// An introspectable element that produced something within this frame.
|
||||||
/// alongside its key.
|
|
||||||
Tag(Tag),
|
Tag(Tag),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,13 +86,6 @@ routines! {
|
|||||||
styles: StyleChain<'a>,
|
styles: StyleChain<'a>,
|
||||||
) -> SourceResult<Vec<Pair<'a>>>
|
) -> SourceResult<Vec<Pair<'a>>>
|
||||||
|
|
||||||
/// Layout content into a document.
|
|
||||||
fn layout_document(
|
|
||||||
engine: &mut Engine,
|
|
||||||
content: &Content,
|
|
||||||
styles: StyleChain,
|
|
||||||
) -> SourceResult<PagedDocument>
|
|
||||||
|
|
||||||
/// Lays out content into multiple regions.
|
/// Lays out content into multiple regions.
|
||||||
fn layout_fragment(
|
fn layout_fragment(
|
||||||
engine: &mut Engine,
|
engine: &mut Engine,
|
||||||
@ -343,11 +336,16 @@ pub enum EvalMode {
|
|||||||
|
|
||||||
/// Defines what kind of realization we are performing.
|
/// Defines what kind of realization we are performing.
|
||||||
pub enum RealizationKind<'a> {
|
pub enum RealizationKind<'a> {
|
||||||
/// This the root realization for the document. Requires a mutable reference
|
/// This the root realization for layout. Requires a mutable reference
|
||||||
/// to document metadata that will be filled from `set document` rules.
|
/// to document metadata that will be filled from `set document` rules.
|
||||||
Root(&'a mut DocumentInfo),
|
LayoutDocument(&'a mut DocumentInfo),
|
||||||
/// A nested realization in a container (e.g. a `block`).
|
/// A nested realization in a container (e.g. a `block`).
|
||||||
Container,
|
LayoutFragment,
|
||||||
|
/// This the root realization for HTML. Requires a mutable reference
|
||||||
|
/// to document metadata that will be filled from `set document` rules.
|
||||||
|
HtmlDocument(&'a mut DocumentInfo),
|
||||||
|
/// A nested realization in a container (e.g. a `block`).
|
||||||
|
HtmlFragment,
|
||||||
/// A realization within math.
|
/// A realization within math.
|
||||||
Math,
|
Math,
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ use typst_library::foundations::{
|
|||||||
SequenceElem, Show, ShowSet, Style, StyleChain, StyleVec, StyledElem, Styles,
|
SequenceElem, Show, ShowSet, Style, StyleChain, StyleVec, StyledElem, Styles,
|
||||||
Synthesize, Transformation,
|
Synthesize, Transformation,
|
||||||
};
|
};
|
||||||
|
use typst_library::html::{tag, HtmlElem};
|
||||||
use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem};
|
use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem};
|
||||||
use typst_library::layout::{
|
use typst_library::layout::{
|
||||||
AlignElem, BoxElem, HElem, InlineElem, PageElem, PagebreakElem, VElem,
|
AlignElem, BoxElem, HElem, InlineElem, PageElem, PagebreakElem, VElem,
|
||||||
@ -47,12 +48,16 @@ pub fn realize<'a>(
|
|||||||
locator,
|
locator,
|
||||||
arenas,
|
arenas,
|
||||||
rules: match kind {
|
rules: match kind {
|
||||||
RealizationKind::Root(_) | RealizationKind::Container => NORMAL_RULES,
|
RealizationKind::LayoutDocument(_) | RealizationKind::LayoutFragment => {
|
||||||
|
LAYOUT_RULES
|
||||||
|
}
|
||||||
|
RealizationKind::HtmlDocument(_) => HTML_DOCUMENT_RULES,
|
||||||
|
RealizationKind::HtmlFragment => HTML_FRAGMENT_RULES,
|
||||||
RealizationKind::Math => MATH_RULES,
|
RealizationKind::Math => MATH_RULES,
|
||||||
},
|
},
|
||||||
sink: vec![],
|
sink: vec![],
|
||||||
groupings: ArrayVec::new(),
|
groupings: ArrayVec::new(),
|
||||||
outside: matches!(kind, RealizationKind::Root(_)),
|
outside: matches!(kind, RealizationKind::LayoutDocument(_)),
|
||||||
may_attach: false,
|
may_attach: false,
|
||||||
kind,
|
kind,
|
||||||
};
|
};
|
||||||
@ -105,10 +110,10 @@ struct GroupingRule {
|
|||||||
/// be visible to `finish`.
|
/// be visible to `finish`.
|
||||||
tags: bool,
|
tags: bool,
|
||||||
/// Defines which kinds of elements start and make up this kind of grouping.
|
/// Defines which kinds of elements start and make up this kind of grouping.
|
||||||
trigger: fn(Element) -> bool,
|
trigger: fn(&Content, &RealizationKind) -> bool,
|
||||||
/// Defines elements that may appear in the interior of the grouping, but
|
/// Defines elements that may appear in the interior of the grouping, but
|
||||||
/// not at the edges.
|
/// not at the edges.
|
||||||
inner: fn(Element) -> bool,
|
inner: fn(&Content) -> bool,
|
||||||
/// Defines whether styles for this kind of element interrupt the grouping.
|
/// Defines whether styles for this kind of element interrupt the grouping.
|
||||||
interrupt: fn(Element) -> bool,
|
interrupt: fn(Element) -> bool,
|
||||||
/// Should convert the accumulated elements in `s.sink[start..]` into
|
/// Should convert the accumulated elements in `s.sink[start..]` into
|
||||||
@ -555,14 +560,16 @@ fn visit_styled<'a>(
|
|||||||
for style in local.iter() {
|
for style in local.iter() {
|
||||||
let Some(elem) = style.element() else { continue };
|
let Some(elem) = style.element() else { continue };
|
||||||
if elem == DocumentElem::elem() {
|
if elem == DocumentElem::elem() {
|
||||||
let RealizationKind::Root(info) = &mut s.kind else {
|
match &mut s.kind {
|
||||||
let span = style.span();
|
RealizationKind::LayoutDocument(info)
|
||||||
bail!(span, "document set rules are not allowed inside of containers");
|
| RealizationKind::HtmlDocument(info) => info.populate(&local),
|
||||||
};
|
_ => bail!(
|
||||||
|
style.span(),
|
||||||
info.populate(&local);
|
"document set rules are not allowed inside of containers"
|
||||||
|
),
|
||||||
|
}
|
||||||
} else if elem == PageElem::elem() {
|
} else if elem == PageElem::elem() {
|
||||||
let RealizationKind::Root(_) = s.kind else {
|
let RealizationKind::LayoutDocument(_) = s.kind else {
|
||||||
let span = style.span();
|
let span = style.span();
|
||||||
bail!(span, "page configuration is not allowed inside of containers");
|
bail!(span, "page configuration is not allowed inside of containers");
|
||||||
};
|
};
|
||||||
@ -618,8 +625,7 @@ fn visit_grouping_rules<'a>(
|
|||||||
content: &'a Content,
|
content: &'a Content,
|
||||||
styles: StyleChain<'a>,
|
styles: StyleChain<'a>,
|
||||||
) -> SourceResult<bool> {
|
) -> SourceResult<bool> {
|
||||||
let elem = content.elem();
|
let matching = s.rules.iter().find(|&rule| (rule.trigger)(content, &s.kind));
|
||||||
let matching = s.rules.iter().find(|&rule| (rule.trigger)(elem));
|
|
||||||
|
|
||||||
// Try to continue or finish an existing grouping.
|
// Try to continue or finish an existing grouping.
|
||||||
while let Some(active) = s.groupings.last() {
|
while let Some(active) = s.groupings.last() {
|
||||||
@ -629,7 +635,7 @@ fn visit_grouping_rules<'a>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If the element can be added to the active grouping, do it.
|
// If the element can be added to the active grouping, do it.
|
||||||
if (active.rule.trigger)(elem) || (active.rule.inner)(elem) {
|
if (active.rule.trigger)(content, &s.kind) || (active.rule.inner)(content) {
|
||||||
s.sink.push((content, styles));
|
s.sink.push((content, styles));
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
@ -655,7 +661,9 @@ fn visit_filter_rules<'a>(
|
|||||||
content: &'a Content,
|
content: &'a Content,
|
||||||
styles: StyleChain<'a>,
|
styles: StyleChain<'a>,
|
||||||
) -> SourceResult<bool> {
|
) -> SourceResult<bool> {
|
||||||
if content.is::<SpaceElem>() && !matches!(s.kind, RealizationKind::Math) {
|
if content.is::<SpaceElem>()
|
||||||
|
&& !matches!(s.kind, RealizationKind::Math | RealizationKind::HtmlFragment)
|
||||||
|
{
|
||||||
// Outside of maths, spaces that were not collected by the paragraph
|
// Outside of maths, spaces that were not collected by the paragraph
|
||||||
// grouper don't interest us.
|
// grouper don't interest us.
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
@ -730,7 +738,7 @@ fn finish_innermost_grouping(s: &mut State) -> SourceResult<()> {
|
|||||||
let Grouping { start, rule } = s.groupings.pop().unwrap();
|
let Grouping { start, rule } = s.groupings.pop().unwrap();
|
||||||
|
|
||||||
// Trim trailing non-trigger elements.
|
// Trim trailing non-trigger elements.
|
||||||
let trimmed = s.sink[start..].trim_end_matches(|(c, _)| !(rule.trigger)(c.elem()));
|
let trimmed = s.sink[start..].trim_end_matches(|(c, _)| !(rule.trigger)(c, &s.kind));
|
||||||
let end = start + trimmed.len();
|
let end = start + trimmed.len();
|
||||||
let tail = s.store_slice(&s.sink[end..]);
|
let tail = s.store_slice(&s.sink[end..]);
|
||||||
s.sink.truncate(end);
|
s.sink.truncate(end);
|
||||||
@ -768,22 +776,30 @@ fn finish_innermost_grouping(s: &mut State) -> SourceResult<()> {
|
|||||||
/// number of unique priority levels.
|
/// number of unique priority levels.
|
||||||
const MAX_GROUP_NESTING: usize = 3;
|
const MAX_GROUP_NESTING: usize = 3;
|
||||||
|
|
||||||
/// Grouping rules used in normal realizations.
|
/// Grouping rules used in layout realization.
|
||||||
static NORMAL_RULES: &[&GroupingRule] = &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS];
|
static LAYOUT_RULES: &[&GroupingRule] = &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS];
|
||||||
|
|
||||||
/// Grouping rules used in math realization.
|
/// Grouping rules used in HTML root realization.
|
||||||
|
static HTML_DOCUMENT_RULES: &[&GroupingRule] =
|
||||||
|
&[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS];
|
||||||
|
|
||||||
|
/// Grouping rules used in HTML fragment realization.
|
||||||
|
static HTML_FRAGMENT_RULES: &[&GroupingRule] = &[&TEXTUAL, &CITES, &LIST, &ENUM, &TERMS];
|
||||||
|
|
||||||
|
/// Grouping rules used in math realizatio.
|
||||||
static MATH_RULES: &[&GroupingRule] = &[&CITES, &LIST, &ENUM, &TERMS];
|
static MATH_RULES: &[&GroupingRule] = &[&CITES, &LIST, &ENUM, &TERMS];
|
||||||
|
|
||||||
/// Groups adjacent textual elements for text show rule application.
|
/// Groups adjacent textual elements for text show rule application.
|
||||||
static TEXTUAL: GroupingRule = GroupingRule {
|
static TEXTUAL: GroupingRule = GroupingRule {
|
||||||
priority: 3,
|
priority: 3,
|
||||||
tags: true,
|
tags: true,
|
||||||
trigger: |elem| {
|
trigger: |content, _| {
|
||||||
|
let elem = content.elem();
|
||||||
elem == TextElem::elem()
|
elem == TextElem::elem()
|
||||||
|| elem == LinebreakElem::elem()
|
|| elem == LinebreakElem::elem()
|
||||||
|| elem == SmartQuoteElem::elem()
|
|| elem == SmartQuoteElem::elem()
|
||||||
},
|
},
|
||||||
inner: |elem| elem == SpaceElem::elem(),
|
inner: |content| content.elem() == SpaceElem::elem(),
|
||||||
// Any kind of style interrupts this kind of grouping since regex show
|
// Any kind of style interrupts this kind of grouping since regex show
|
||||||
// rules cannot match over style changes anyway.
|
// rules cannot match over style changes anyway.
|
||||||
interrupt: |_| true,
|
interrupt: |_| true,
|
||||||
@ -794,15 +810,22 @@ static TEXTUAL: GroupingRule = GroupingRule {
|
|||||||
static PAR: GroupingRule = GroupingRule {
|
static PAR: GroupingRule = GroupingRule {
|
||||||
priority: 1,
|
priority: 1,
|
||||||
tags: true,
|
tags: true,
|
||||||
trigger: |elem| {
|
trigger: |content, kind| {
|
||||||
|
let elem = content.elem();
|
||||||
elem == TextElem::elem()
|
elem == TextElem::elem()
|
||||||
|| elem == HElem::elem()
|
|| elem == HElem::elem()
|
||||||
|| elem == LinebreakElem::elem()
|
|| elem == LinebreakElem::elem()
|
||||||
|| elem == SmartQuoteElem::elem()
|
|| elem == SmartQuoteElem::elem()
|
||||||
|| elem == InlineElem::elem()
|
|| elem == InlineElem::elem()
|
||||||
|| elem == BoxElem::elem()
|
|| elem == BoxElem::elem()
|
||||||
|
|| (matches!(
|
||||||
|
kind,
|
||||||
|
RealizationKind::HtmlDocument(_) | RealizationKind::HtmlFragment
|
||||||
|
) && content
|
||||||
|
.to_packed::<HtmlElem>()
|
||||||
|
.is_some_and(|elem| tag::is_inline(elem.tag)))
|
||||||
},
|
},
|
||||||
inner: |elem| elem == SpaceElem::elem(),
|
inner: |content| content.elem() == SpaceElem::elem(),
|
||||||
interrupt: |elem| elem == ParElem::elem() || elem == AlignElem::elem(),
|
interrupt: |elem| elem == ParElem::elem() || elem == AlignElem::elem(),
|
||||||
finish: finish_par,
|
finish: finish_par,
|
||||||
};
|
};
|
||||||
@ -811,8 +834,8 @@ static PAR: GroupingRule = GroupingRule {
|
|||||||
static CITES: GroupingRule = GroupingRule {
|
static CITES: GroupingRule = GroupingRule {
|
||||||
priority: 2,
|
priority: 2,
|
||||||
tags: false,
|
tags: false,
|
||||||
trigger: |elem| elem == CiteElem::elem(),
|
trigger: |content, _| content.elem() == CiteElem::elem(),
|
||||||
inner: |elem| elem == SpaceElem::elem(),
|
inner: |content| content.elem() == SpaceElem::elem(),
|
||||||
interrupt: |elem| elem == CiteGroup::elem(),
|
interrupt: |elem| elem == CiteGroup::elem(),
|
||||||
finish: finish_cites,
|
finish: finish_cites,
|
||||||
};
|
};
|
||||||
@ -831,8 +854,11 @@ const fn list_like_grouping<T: ListLike>() -> GroupingRule {
|
|||||||
GroupingRule {
|
GroupingRule {
|
||||||
priority: 2,
|
priority: 2,
|
||||||
tags: false,
|
tags: false,
|
||||||
trigger: |elem| elem == T::Item::elem(),
|
trigger: |content, _| content.elem() == T::Item::elem(),
|
||||||
inner: |elem| elem == SpaceElem::elem() || elem == ParbreakElem::elem(),
|
inner: |content| {
|
||||||
|
let elem = content.elem();
|
||||||
|
elem == SpaceElem::elem() || elem == ParbreakElem::elem()
|
||||||
|
},
|
||||||
interrupt: |elem| elem == T::elem(),
|
interrupt: |elem| elem == T::elem(),
|
||||||
finish: finish_list_like::<T>,
|
finish: finish_list_like::<T>,
|
||||||
}
|
}
|
||||||
@ -867,7 +893,7 @@ fn finish_textual(Grouped { s, mut start }: Grouped) -> SourceResult<()> {
|
|||||||
// 1. We are already in a paragraph group. In this case, the elements just
|
// 1. We are already in a paragraph group. In this case, the elements just
|
||||||
// transparently become part of it.
|
// transparently become part of it.
|
||||||
// 2. There is no group at all. In this case, we create one.
|
// 2. There is no group at all. In this case, we create one.
|
||||||
if s.groupings.is_empty() {
|
if s.groupings.is_empty() && s.rules.iter().any(|&rule| std::ptr::eq(rule, &PAR)) {
|
||||||
s.groupings.push(Grouping { start, rule: &PAR });
|
s.groupings.push(Grouping { start, rule: &PAR });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,6 +32,17 @@ pub fn svg(page: &Page) -> String {
|
|||||||
renderer.finalize()
|
renderer.finalize()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Export a frame into a SVG file.
|
||||||
|
#[typst_macros::time(name = "svg frame")]
|
||||||
|
pub fn svg_frame(frame: &Frame) -> String {
|
||||||
|
let mut renderer = SVGRenderer::new();
|
||||||
|
renderer.write_header(frame.size());
|
||||||
|
|
||||||
|
let state = State::new(frame.size(), Transform::identity());
|
||||||
|
renderer.render_frame(state, Transform::identity(), frame);
|
||||||
|
renderer.finalize()
|
||||||
|
}
|
||||||
|
|
||||||
/// Export a document with potentially multiple pages into a single SVG file.
|
/// Export a document with potentially multiple pages into a single SVG file.
|
||||||
///
|
///
|
||||||
/// The padding will be added around and between the individual frames.
|
/// The padding will be added around and between the individual frames.
|
||||||
|
@ -14,6 +14,7 @@ readme = { workspace = true }
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
typst-eval = { workspace = true }
|
typst-eval = { workspace = true }
|
||||||
|
typst-html = { workspace = true }
|
||||||
typst-layout = { workspace = true }
|
typst-layout = { workspace = true }
|
||||||
typst-library = { workspace = true }
|
typst-library = { workspace = true }
|
||||||
typst-macros = { workspace = true }
|
typst-macros = { workspace = true }
|
||||||
|
@ -13,11 +13,11 @@
|
|||||||
//! order-independent and thus much better suited for further processing than
|
//! order-independent and thus much better suited for further processing than
|
||||||
//! the raw markup.
|
//! the raw markup.
|
||||||
//! - **Layouting:**
|
//! - **Layouting:**
|
||||||
//! Next, the content is [laid out] into a [document] containing one [frame]
|
//! Next, the content is [laid out] into a [`PagedDocument`] containing one
|
||||||
//! per page with items at fixed positions.
|
//! [frame] per page with items at fixed positions.
|
||||||
//! - **Exporting:**
|
//! - **Exporting:**
|
||||||
//! These frames can finally be exported into an output format (currently PDF,
|
//! These frames can finally be exported into an output format (currently PDF,
|
||||||
//! PNG, or SVG).
|
//! PNG, SVG, and HTML).
|
||||||
//!
|
//!
|
||||||
//! [tokens]: typst_syntax::SyntaxKind
|
//! [tokens]: typst_syntax::SyntaxKind
|
||||||
//! [parsed]: typst_syntax::parse
|
//! [parsed]: typst_syntax::parse
|
||||||
@ -43,23 +43,32 @@ use std::collections::HashSet;
|
|||||||
|
|
||||||
use comemo::{Track, Tracked, Validate};
|
use comemo::{Track, Tracked, Validate};
|
||||||
use ecow::{eco_format, eco_vec, EcoString, EcoVec};
|
use ecow::{eco_format, eco_vec, EcoString, EcoVec};
|
||||||
use typst_library::diag::{warning, FileError, SourceDiagnostic, SourceResult, Warned};
|
use typst_library::diag::{
|
||||||
|
bail, warning, FileError, SourceDiagnostic, SourceResult, Warned,
|
||||||
|
};
|
||||||
use typst_library::engine::{Engine, Route, Sink, Traced};
|
use typst_library::engine::{Engine, Route, Sink, Traced};
|
||||||
use typst_library::foundations::{StyleChain, Styles, Value};
|
use typst_library::foundations::{StyleChain, Styles, Value};
|
||||||
|
use typst_library::html::HtmlDocument;
|
||||||
use typst_library::introspection::Introspector;
|
use typst_library::introspection::Introspector;
|
||||||
use typst_library::layout::PagedDocument;
|
use typst_library::layout::PagedDocument;
|
||||||
use typst_library::routines::Routines;
|
use typst_library::routines::Routines;
|
||||||
use typst_syntax::{FileId, Span};
|
use typst_syntax::{FileId, Span};
|
||||||
use typst_timing::{timed, TimingScope};
|
use typst_timing::{timed, TimingScope};
|
||||||
|
|
||||||
|
use crate::foundations::{Target, TargetElem};
|
||||||
|
use crate::model::DocumentInfo;
|
||||||
|
|
||||||
/// Compile sources into a fully layouted document.
|
/// Compile sources into a fully layouted document.
|
||||||
///
|
///
|
||||||
/// - Returns `Ok(document)` if there were no fatal errors.
|
/// - Returns `Ok(document)` if there were no fatal errors.
|
||||||
/// - Returns `Err(errors)` if there were fatal errors.
|
/// - Returns `Err(errors)` if there were fatal errors.
|
||||||
#[typst_macros::time]
|
#[typst_macros::time]
|
||||||
pub fn compile(world: &dyn World) -> Warned<SourceResult<PagedDocument>> {
|
pub fn compile<D>(world: &dyn World) -> Warned<SourceResult<D>>
|
||||||
|
where
|
||||||
|
D: Document,
|
||||||
|
{
|
||||||
let mut sink = Sink::new();
|
let mut sink = Sink::new();
|
||||||
let output = compile_impl(world.track(), Traced::default().track(), &mut sink)
|
let output = compile_impl::<D>(world.track(), Traced::default().track(), &mut sink)
|
||||||
.map_err(deduplicate);
|
.map_err(deduplicate);
|
||||||
Warned { output, warnings: sink.warnings() }
|
Warned { output, warnings: sink.warnings() }
|
||||||
}
|
}
|
||||||
@ -67,22 +76,32 @@ pub fn compile(world: &dyn World) -> Warned<SourceResult<PagedDocument>> {
|
|||||||
/// Compiles sources and returns all values and styles observed at the given
|
/// Compiles sources and returns all values and styles observed at the given
|
||||||
/// `span` during compilation.
|
/// `span` during compilation.
|
||||||
#[typst_macros::time]
|
#[typst_macros::time]
|
||||||
pub fn trace(world: &dyn World, span: Span) -> EcoVec<(Value, Option<Styles>)> {
|
pub fn trace<D>(world: &dyn World, span: Span) -> EcoVec<(Value, Option<Styles>)>
|
||||||
|
where
|
||||||
|
D: Document,
|
||||||
|
{
|
||||||
let mut sink = Sink::new();
|
let mut sink = Sink::new();
|
||||||
let traced = Traced::new(span);
|
let traced = Traced::new(span);
|
||||||
compile_impl(world.track(), traced.track(), &mut sink).ok();
|
compile_impl::<D>(world.track(), traced.track(), &mut sink).ok();
|
||||||
sink.values()
|
sink.values()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The internal implementation of `compile` with a bit lower-level interface
|
/// The internal implementation of `compile` with a bit lower-level interface
|
||||||
/// that is also used by `trace`.
|
/// that is also used by `trace`.
|
||||||
fn compile_impl(
|
fn compile_impl<D: Document>(
|
||||||
world: Tracked<dyn World + '_>,
|
world: Tracked<dyn World + '_>,
|
||||||
traced: Tracked<Traced>,
|
traced: Tracked<Traced>,
|
||||||
sink: &mut Sink,
|
sink: &mut Sink,
|
||||||
) -> SourceResult<PagedDocument> {
|
) -> SourceResult<D> {
|
||||||
|
if D::TARGET == Target::Html {
|
||||||
|
warn_or_error_for_html(world, sink)?;
|
||||||
|
}
|
||||||
|
|
||||||
let library = world.library();
|
let library = world.library();
|
||||||
let styles = StyleChain::new(&library.styles);
|
let base = StyleChain::new(&library.styles);
|
||||||
|
let target = TargetElem::set_target(D::TARGET).wrap();
|
||||||
|
let styles = base.chain(&target);
|
||||||
|
let empty_introspector = Introspector::default();
|
||||||
|
|
||||||
// Fetch the main source file once.
|
// Fetch the main source file once.
|
||||||
let main = world.main();
|
let main = world.main();
|
||||||
@ -103,7 +122,8 @@ fn compile_impl(
|
|||||||
|
|
||||||
let mut iter = 0;
|
let mut iter = 0;
|
||||||
let mut subsink;
|
let mut subsink;
|
||||||
let mut document = PagedDocument::default();
|
let mut introspector = &empty_introspector;
|
||||||
|
let mut document: D;
|
||||||
|
|
||||||
// Relayout until all introspections stabilize.
|
// Relayout until all introspections stabilize.
|
||||||
// If that doesn't happen within five attempts, we give up.
|
// If that doesn't happen within five attempts, we give up.
|
||||||
@ -118,7 +138,7 @@ fn compile_impl(
|
|||||||
let constraint = <Introspector as Validate>::Constraint::new();
|
let constraint = <Introspector as Validate>::Constraint::new();
|
||||||
let mut engine = Engine {
|
let mut engine = Engine {
|
||||||
world,
|
world,
|
||||||
introspector: document.introspector.track_with(&constraint),
|
introspector: introspector.track_with(&constraint),
|
||||||
traced,
|
traced,
|
||||||
sink: subsink.track_mut(),
|
sink: subsink.track_mut(),
|
||||||
route: Route::default(),
|
route: Route::default(),
|
||||||
@ -126,10 +146,11 @@ fn compile_impl(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Layout!
|
// Layout!
|
||||||
document = (engine.routines.layout_document)(&mut engine, &content, styles)?;
|
document = D::create(&mut engine, &content, styles)?;
|
||||||
|
introspector = document.introspector();
|
||||||
iter += 1;
|
iter += 1;
|
||||||
|
|
||||||
if timed!("check stabilized", document.introspector.validate(&constraint)) {
|
if timed!("check stabilized", introspector.validate(&constraint)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,6 +229,97 @@ fn hint_invalid_main_file(
|
|||||||
eco_vec![diagnostic]
|
eco_vec![diagnostic]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// HTML export will warn or error depending on whether the feature flag is enabled.
|
||||||
|
fn warn_or_error_for_html(
|
||||||
|
world: Tracked<dyn World + '_>,
|
||||||
|
sink: &mut Sink,
|
||||||
|
) -> SourceResult<()> {
|
||||||
|
if world.library().features.is_enabled(Feature::Html) {
|
||||||
|
sink.warn(warning!(
|
||||||
|
Span::detached(),
|
||||||
|
"html export is under active development and incomplete";
|
||||||
|
hint: "its behaviour may change at any time";
|
||||||
|
hint: "do not rely on this feature for production use cases"
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
bail!(
|
||||||
|
Span::detached(),
|
||||||
|
"html export is only available when `--feature html` is passed";
|
||||||
|
hint: "html export is under active development and incomplete"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A document is what results from compilation.
|
||||||
|
pub trait Document: sealed::Sealed {
|
||||||
|
/// Get the document's metadata.
|
||||||
|
fn info(&self) -> &DocumentInfo;
|
||||||
|
|
||||||
|
/// Get the document's introspector.
|
||||||
|
fn introspector(&self) -> &Introspector;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Document for PagedDocument {
|
||||||
|
fn info(&self) -> &DocumentInfo {
|
||||||
|
&self.info
|
||||||
|
}
|
||||||
|
|
||||||
|
fn introspector(&self) -> &Introspector {
|
||||||
|
&self.introspector
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Document for HtmlDocument {
|
||||||
|
fn info(&self) -> &DocumentInfo {
|
||||||
|
&self.info
|
||||||
|
}
|
||||||
|
|
||||||
|
fn introspector(&self) -> &Introspector {
|
||||||
|
&self.introspector
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod sealed {
|
||||||
|
use typst_library::foundations::{Content, Target};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub trait Sealed: Sized {
|
||||||
|
const TARGET: Target;
|
||||||
|
|
||||||
|
fn create(
|
||||||
|
engine: &mut Engine,
|
||||||
|
content: &Content,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sealed for PagedDocument {
|
||||||
|
const TARGET: Target = Target::Paged;
|
||||||
|
|
||||||
|
fn create(
|
||||||
|
engine: &mut Engine,
|
||||||
|
content: &Content,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<Self> {
|
||||||
|
typst_layout::layout_document(engine, content, styles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sealed for HtmlDocument {
|
||||||
|
const TARGET: Target = Target::Html;
|
||||||
|
|
||||||
|
fn create(
|
||||||
|
engine: &mut Engine,
|
||||||
|
content: &Content,
|
||||||
|
styles: StyleChain,
|
||||||
|
) -> SourceResult<Self> {
|
||||||
|
typst_html::html_document(engine, content, styles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Defines implementation of various Typst compiler routines as a table of
|
/// Defines implementation of various Typst compiler routines as a table of
|
||||||
/// function pointers.
|
/// function pointers.
|
||||||
///
|
///
|
||||||
@ -216,7 +328,6 @@ pub static ROUTINES: Routines = Routines {
|
|||||||
eval_string: typst_eval::eval_string,
|
eval_string: typst_eval::eval_string,
|
||||||
eval_closure: typst_eval::eval_closure,
|
eval_closure: typst_eval::eval_closure,
|
||||||
realize: typst_realize::realize,
|
realize: typst_realize::realize,
|
||||||
layout_document: typst_layout::layout_document,
|
|
||||||
layout_fragment: typst_layout::layout_fragment,
|
layout_fragment: typst_layout::layout_fragment,
|
||||||
layout_frame: typst_layout::layout_frame,
|
layout_frame: typst_layout::layout_frame,
|
||||||
layout_inline: typst_layout::layout_inline,
|
layout_inline: typst_layout::layout_inline,
|
||||||
|
@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use typed_arena::Arena;
|
use typed_arena::Arena;
|
||||||
use typst::diag::{FileError, FileResult, StrResult};
|
use typst::diag::{FileError, FileResult, StrResult};
|
||||||
use typst::foundations::{Bytes, Datetime};
|
use typst::foundations::{Bytes, Datetime};
|
||||||
use typst::layout::{Abs, Point, Size};
|
use typst::layout::{Abs, PagedDocument, Point, Size};
|
||||||
use typst::syntax::{FileId, Source, VirtualPath};
|
use typst::syntax::{FileId, Source, VirtualPath};
|
||||||
use typst::text::{Font, FontBook};
|
use typst::text::{Font, FontBook};
|
||||||
use typst::utils::LazyHash;
|
use typst::utils::LazyHash;
|
||||||
@ -419,7 +419,7 @@ fn code_block(resolver: &dyn Resolver, lang: &str, text: &str) -> Html {
|
|||||||
let source = Source::new(id, compile);
|
let source = Source::new(id, compile);
|
||||||
let world = DocWorld(source);
|
let world = DocWorld(source);
|
||||||
|
|
||||||
let mut document = match typst::compile(&world).output {
|
let mut document = match typst::compile::<PagedDocument>(&world).output {
|
||||||
Ok(doc) => doc,
|
Ok(doc) => doc,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let msg = &err[0].message;
|
let msg = &err[0].message;
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
use libfuzzer_sys::fuzz_target;
|
use libfuzzer_sys::fuzz_target;
|
||||||
use typst::diag::{FileError, FileResult};
|
use typst::diag::{FileError, FileResult};
|
||||||
use typst::foundations::{Bytes, Datetime};
|
use typst::foundations::{Bytes, Datetime};
|
||||||
|
use typst::layout::PagedDocument;
|
||||||
use typst::syntax::{FileId, Source};
|
use typst::syntax::{FileId, Source};
|
||||||
use typst::text::{Font, FontBook};
|
use typst::text::{Font, FontBook};
|
||||||
use typst::utils::LazyHash;
|
use typst::utils::LazyHash;
|
||||||
@ -65,7 +66,7 @@ impl World for FuzzWorld {
|
|||||||
|
|
||||||
fuzz_target!(|text: &str| {
|
fuzz_target!(|text: &str| {
|
||||||
let world = FuzzWorld::new(text);
|
let world = FuzzWorld::new(text);
|
||||||
if let Ok(document) = typst::compile(&world).output {
|
if let Ok(document) = typst::compile::<PagedDocument>(&world).output {
|
||||||
if let Some(page) = document.pages.first() {
|
if let Some(page) = document.pages.first() {
|
||||||
std::hint::black_box(typst_render::render(page, 1.0));
|
std::hint::black_box(typst_render::render(page, 1.0));
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user