diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2354de582..70518860e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,3 +103,15 @@ jobs: - uses: Swatinem/rust-cache@v2 - run: cargo install --locked cargo-fuzz@0.12.0 - run: cd tests/fuzz && cargo fuzz build --dev + + miri: + name: Check unsafe code + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + components: miri + toolchain: nightly-2024-10-29 + - uses: Swatinem/rust-cache@v2 + - run: cargo miri test -p typst-library test_miri diff --git a/Cargo.lock b/Cargo.lock index 509cfd0da..23402ba53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -413,7 +413,7 @@ dependencies = [ [[package]] name = "codex" version = "0.1.1" -source = "git+https://github.com/typst/codex?rev=2f7efc3#2f7efc3b824632bcc917cebf4ae91caeca224fbc" +source = "git+https://github.com/typst/codex?rev=775d828#775d82873c3f74ce95ec2621f8541de1b48778a7" [[package]] name = "color-print" @@ -2861,7 +2861,7 @@ dependencies = [ [[package]] name = "typst-assets" version = "0.13.1" -source = "git+https://github.com/typst/typst-assets?rev=c1089b4#c1089b46c461bdde579c55caa941a3cc7dec3e8a" +source = "git+https://github.com/typst/typst-assets?rev=edf0d64#edf0d648376e29738a05a933af9ea99bb81557b1" [[package]] name = "typst-cli" @@ -2971,8 +2971,12 @@ dependencies = [ name = "typst-html" version = "0.13.1" dependencies = [ + "bumpalo", "comemo", "ecow", + "palette", + "time", + "typst-assets", "typst-library", "typst-macros", "typst-svg", @@ -3028,6 +3032,7 @@ version = "0.13.1" dependencies = [ "az", "bumpalo", + "codex", "comemo", "ecow", "hypher", diff --git a/Cargo.toml b/Cargo.toml index 00f1220fe..4e1e0a085 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ typst-svg = { path = "crates/typst-svg", version = "0.13.1" } typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" } typst-timing = { path = "crates/typst-timing", version = "0.13.1" } typst-utils = { path = "crates/typst-utils", version = "0.13.1" } -typst-assets = { git = "https://github.com/typst/typst-assets", rev = "c1089b4" } +typst-assets = { git = "https://github.com/typst/typst-assets", rev = "edf0d64" } typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "bfa947f" } arrayvec = "0.7.4" az = "1.2" @@ -47,7 +47,7 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] } clap_complete = "4.2.1" clap_mangen = "0.2.10" codespan-reporting = "0.11" -codex = { git = "https://github.com/typst/codex", rev = "2f7efc3" } +codex = { git = "https://github.com/typst/codex", rev = "775d828" } color-print = "0.3.6" comemo = "0.4" csv = "1" diff --git a/README.md b/README.md index 9526f3df4..9dee102b6 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,26 @@ instant preview. To achieve these goals, we follow three core design principles: Luckily we have [`comemo`], a system for incremental compilation which does most of the hard work in the background. +## Acknowledgements + +We'd like to thank everyone who is supporting Typst's development, be it via +[GitHub sponsors] or elsewhere. In particular, special thanks[^1] go to: + +- [Posit](https://posit.co/blog/posit-and-typst/) for financing a full-time + compiler engineer +- [NLnet](https://nlnet.nl/) for supporting work on Typst via multiple grants + through the [NGI Zero Core](https://nlnet.nl/core) fund: + - Work on [HTML export](https://nlnet.nl/project/Typst-HTML/) + - Work on [PDF accessibility](https://nlnet.nl/project/Typst-Accessibility/) +- [Science & Startups](https://www.science-startups.berlin/) for having financed + Typst development from January through June 2023 via the Berlin Startup + Scholarship +- [Zerodha](https://zerodha.tech/blog/1-5-million-pdfs-in-25-minutes/) for their + generous one-time sponsorship + +[^1]: This list only includes contributions for our open-source work that exceed + or are expected to exceed €10K. + [docs]: https://typst.app/docs/ [app]: https://typst.app/ [discord]: https://discord.gg/2uDybryKPe @@ -259,3 +279,4 @@ instant preview. To achieve these goals, we follow three core design principles: [packages]: https://github.com/typst/packages/ [`comemo`]: https://github.com/typst/comemo/ [snap]: https://snapcraft.io/typst +[GitHub sponsors]: https://github.com/sponsors/typst/ diff --git a/crates/typst-cli/Cargo.toml b/crates/typst-cli/Cargo.toml index 7e9b93f93..792cabae1 100644 --- a/crates/typst-cli/Cargo.toml +++ b/crates/typst-cli/Cargo.toml @@ -29,6 +29,7 @@ typst-svg = { workspace = true } typst-timing = { workspace = true } chrono = { workspace = true } clap = { workspace = true } +clap_complete = { workspace = true } codespan-reporting = { workspace = true } color-print = { workspace = true } comemo = { workspace = true } diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs index fd0eb5f05..7459be0f2 100644 --- a/crates/typst-cli/src/args.rs +++ b/crates/typst-cli/src/args.rs @@ -7,6 +7,7 @@ use std::str::FromStr; use chrono::{DateTime, Utc}; use clap::builder::{TypedValueParser, ValueParser}; use clap::{ArgAction, Args, ColorChoice, Parser, Subcommand, ValueEnum, ValueHint}; +use clap_complete::Shell; use semver::Version; /// The character typically used to separate path components @@ -81,6 +82,9 @@ pub enum Command { /// Self update the Typst CLI. #[cfg_attr(not(feature = "self-update"), clap(hide = true))] Update(UpdateCommand), + + /// Generates shell completion scripts. + Completions(CompletionsCommand), } /// Compiles an input file into a supported output format. @@ -198,6 +202,14 @@ pub struct UpdateCommand { pub backup_path: Option, } +/// Generates shell completion scripts. +#[derive(Debug, Clone, Parser)] +pub struct CompletionsCommand { + /// The shell to generate completions for. + #[arg(value_enum)] + pub shell: Shell, +} + /// Arguments for compilation and watching. #[derive(Debug, Clone, Args)] pub struct CompileArgs { @@ -491,7 +503,7 @@ pub enum PdfStandard { /// PDF/A-2u. #[value(name = "a-2u")] A_2u, - /// PDF/A-3u. + /// PDF/A-3b. #[value(name = "a-3b")] A_3b, /// PDF/A-3u. diff --git a/crates/typst-cli/src/completions.rs b/crates/typst-cli/src/completions.rs new file mode 100644 index 000000000..51e7db103 --- /dev/null +++ b/crates/typst-cli/src/completions.rs @@ -0,0 +1,13 @@ +use std::io::stdout; + +use clap::CommandFactory; +use clap_complete::generate; + +use crate::args::{CliArguments, CompletionsCommand}; + +/// Execute the completions command. +pub fn completions(command: &CompletionsCommand) { + let mut cmd = CliArguments::command(); + let bin_name = cmd.get_name().to_string(); + generate(command.shell, &mut cmd, bin_name, &mut stdout()); +} diff --git a/crates/typst-cli/src/main.rs b/crates/typst-cli/src/main.rs index 14f8a665d..6a3b337d8 100644 --- a/crates/typst-cli/src/main.rs +++ b/crates/typst-cli/src/main.rs @@ -1,5 +1,6 @@ mod args; mod compile; +mod completions; mod download; mod fonts; mod greet; @@ -71,6 +72,7 @@ fn dispatch() -> HintedStrResult<()> { Command::Query(command) => crate::query::query(command)?, Command::Fonts(command) => crate::fonts::fonts(command), Command::Update(command) => crate::update::update(command)?, + Command::Completions(command) => crate::completions::completions(command), } Ok(()) diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs index 95bee235c..8ad766b14 100644 --- a/crates/typst-cli/src/world.rs +++ b/crates/typst-cli/src/world.rs @@ -12,7 +12,7 @@ use typst::foundations::{Bytes, Datetime, Dict, IntoValue}; use typst::syntax::{FileId, Lines, Source, VirtualPath}; use typst::text::{Font, FontBook}; use typst::utils::LazyHash; -use typst::{Library, World}; +use typst::{Library, LibraryExt, World}; use typst_kit::fonts::{FontSlot, Fonts}; use typst_kit::package::PackageStorage; use typst_timing::timed; diff --git a/crates/typst-eval/src/markup.rs b/crates/typst-eval/src/markup.rs index 6b5daf5b6..03da144d0 100644 --- a/crates/typst-eval/src/markup.rs +++ b/crates/typst-eval/src/markup.rs @@ -186,7 +186,7 @@ impl Eval for ast::Raw<'_> { let lines = self.lines().map(|line| (line.get().clone(), line.span())).collect(); let mut elem = RawElem::new(RawContent::Lines(lines)).with_block(self.block()); if let Some(lang) = self.lang() { - elem.push_lang(Some(lang.get().clone())); + elem.lang.set(Some(lang.get().clone())); } Ok(elem.pack()) } @@ -219,9 +219,8 @@ impl Eval for ast::Ref<'_> { .expect("unexpected empty reference"); let mut elem = RefElem::new(target); if let Some(supplement) = self.supplement() { - elem.push_supplement(Smart::Custom(Some(Supplement::Content( - supplement.eval(vm)?, - )))); + elem.supplement + .set(Smart::Custom(Some(Supplement::Content(supplement.eval(vm)?)))); } Ok(elem.pack()) } @@ -252,7 +251,7 @@ impl Eval for ast::EnumItem<'_> { let body = self.body().eval(vm)?; let mut elem = EnumItem::new(body); if let Some(number) = self.number() { - elem.push_number(Some(number)); + elem.number.set(Some(number)); } Ok(elem.pack()) } diff --git a/crates/typst-eval/src/math.rs b/crates/typst-eval/src/math.rs index 50e396212..24567c37d 100644 --- a/crates/typst-eval/src/math.rs +++ b/crates/typst-eval/src/math.rs @@ -80,17 +80,17 @@ impl Eval for ast::MathAttach<'_> { let mut elem = AttachElem::new(base); if let Some(expr) = self.top() { - elem.push_t(Some(expr.eval_display(vm)?)); + elem.t.set(Some(expr.eval_display(vm)?)); } // Always attach primes in scripts style (not limits style), // i.e. at the top-right corner. if let Some(primes) = self.primes() { - elem.push_tr(Some(primes.eval(vm)?)); + elem.tr.set(Some(primes.eval(vm)?)); } if let Some(expr) = self.bottom() { - elem.push_b(Some(expr.eval_display(vm)?)); + elem.b.set(Some(expr.eval_display(vm)?)); } Ok(elem.pack()) diff --git a/crates/typst-eval/src/rules.rs b/crates/typst-eval/src/rules.rs index f4c1563f3..eb6a1e6da 100644 --- a/crates/typst-eval/src/rules.rs +++ b/crates/typst-eval/src/rules.rs @@ -1,6 +1,6 @@ use typst_library::diag::{warning, At, SourceResult}; use typst_library::foundations::{ - Element, Fields, Func, Recipe, Selector, ShowableSelector, Styles, Transformation, + Element, Func, Recipe, Selector, ShowableSelector, Styles, Transformation, }; use typst_library::layout::BlockElem; use typst_library::model::ParElem; @@ -62,8 +62,7 @@ fn check_show_par_set_block(vm: &mut Vm, recipe: &Recipe) { if let Some(Selector::Elem(elem, _)) = recipe.selector(); if *elem == Element::of::(); if let Transformation::Style(styles) = recipe.transform(); - if styles.has::(::Enum::Above as _) || - styles.has::(::Enum::Below as _); + if styles.has(BlockElem::above) || styles.has(BlockElem::below); then { vm.engine.sink.warn(warning!( recipe.span(), diff --git a/crates/typst-html/Cargo.toml b/crates/typst-html/Cargo.toml index 534848f96..54cad0124 100644 --- a/crates/typst-html/Cargo.toml +++ b/crates/typst-html/Cargo.toml @@ -13,14 +13,18 @@ keywords = { workspace = true } readme = { workspace = true } [dependencies] +typst-assets = { workspace = true } typst-library = { workspace = true } typst-macros = { workspace = true } typst-syntax = { workspace = true } typst-timing = { workspace = true } typst-utils = { workspace = true } typst-svg = { workspace = true } +bumpalo = { workspace = true } comemo = { workspace = true } ecow = { workspace = true } +palette = { workspace = true } +time = { workspace = true } [lints] workspace = true diff --git a/crates/typst-html/src/css.rs b/crates/typst-html/src/css.rs new file mode 100644 index 000000000..6c84cba0f --- /dev/null +++ b/crates/typst-html/src/css.rs @@ -0,0 +1,196 @@ +//! Conversion from Typst data types into CSS data types. + +use std::fmt::{self, Display, Write}; + +use ecow::EcoString; +use typst_library::html::{attr, HtmlElem}; +use typst_library::layout::{Length, Rel}; +use typst_library::visualize::{Color, Hsl, LinearRgb, Oklab, Oklch, Rgb}; +use typst_utils::Numeric; + +/// Additional methods for [`HtmlElem`]. +pub trait HtmlElemExt { + /// Adds the styles to an element if the property list is non-empty. + fn with_styles(self, properties: Properties) -> Self; +} + +impl HtmlElemExt for HtmlElem { + /// Adds CSS styles to an element. + fn with_styles(self, properties: Properties) -> Self { + if let Some(value) = properties.into_inline_styles() { + self.with_attr(attr::style, value) + } else { + self + } + } +} + +/// A list of CSS properties with values. +#[derive(Debug, Default)] +pub struct Properties(EcoString); + +impl Properties { + /// Creates an empty list. + pub fn new() -> Self { + Self::default() + } + + /// Adds a new property to the list. + pub fn push(&mut self, property: &str, value: impl Display) { + if !self.0.is_empty() { + self.0.push_str("; "); + } + write!(&mut self.0, "{property}: {value}").unwrap(); + } + + /// Adds a new property in builder-style. + #[expect(unused)] + pub fn with(mut self, property: &str, value: impl Display) -> Self { + self.push(property, value); + self + } + + /// Turns this into a string suitable for use as an inline `style` + /// attribute. + pub fn into_inline_styles(self) -> Option { + (!self.0.is_empty()).then_some(self.0) + } +} + +pub fn rel(rel: Rel) -> impl Display { + typst_utils::display(move |f| match (rel.abs.is_zero(), rel.rel.is_zero()) { + (false, false) => { + write!(f, "calc({}% + {})", rel.rel.get(), length(rel.abs)) + } + (true, false) => write!(f, "{}%", rel.rel.get()), + (_, true) => write!(f, "{}", length(rel.abs)), + }) +} + +pub fn length(length: Length) -> impl Display { + typst_utils::display(move |f| match (length.abs.is_zero(), length.em.is_zero()) { + (false, false) => { + write!(f, "calc({}pt + {}em)", length.abs.to_pt(), length.em.get()) + } + (true, false) => write!(f, "{}em", length.em.get()), + (_, true) => write!(f, "{}pt", length.abs.to_pt()), + }) +} + +pub fn color(color: Color) -> impl Display { + typst_utils::display(move |f| match color { + Color::Rgb(_) | Color::Cmyk(_) | Color::Luma(_) => rgb(f, color.to_rgb()), + Color::Oklab(v) => oklab(f, v), + Color::Oklch(v) => oklch(f, v), + Color::LinearRgb(v) => linear_rgb(f, v), + Color::Hsl(_) | Color::Hsv(_) => hsl(f, color.to_hsl()), + }) +} + +fn oklab(f: &mut fmt::Formatter<'_>, v: Oklab) -> fmt::Result { + write!(f, "oklab({} {} {}{})", percent(v.l), number(v.a), number(v.b), alpha(v.alpha)) +} + +fn oklch(f: &mut fmt::Formatter<'_>, v: Oklch) -> fmt::Result { + write!( + f, + "oklch({} {} {}deg{})", + percent(v.l), + number(v.chroma), + number(v.hue.into_degrees()), + alpha(v.alpha) + ) +} + +fn rgb(f: &mut fmt::Formatter<'_>, v: Rgb) -> fmt::Result { + if let Some(v) = rgb_to_8_bit_lossless(v) { + let (r, g, b, a) = v.into_components(); + write!(f, "#{r:02x}{g:02x}{b:02x}")?; + if a != u8::MAX { + write!(f, "{a:02x}")?; + } + Ok(()) + } else { + write!( + f, + "rgb({} {} {}{})", + percent(v.red), + percent(v.green), + percent(v.blue), + alpha(v.alpha) + ) + } +} + +/// Converts an f32 RGBA color to its 8-bit representation if the result is +/// [very close](is_very_close) to the original. +fn rgb_to_8_bit_lossless( + v: Rgb, +) -> Option> { + let l = v.into_format::(); + let h = l.into_format::(); + (is_very_close(v.red, h.red) + && is_very_close(v.blue, h.blue) + && is_very_close(v.green, h.green) + && is_very_close(v.alpha, h.alpha)) + .then_some(l) +} + +fn linear_rgb(f: &mut fmt::Formatter<'_>, v: LinearRgb) -> fmt::Result { + write!( + f, + "color(srgb-linear {} {} {}{})", + percent(v.red), + percent(v.green), + percent(v.blue), + alpha(v.alpha), + ) +} + +fn hsl(f: &mut fmt::Formatter<'_>, v: Hsl) -> fmt::Result { + write!( + f, + "hsl({}deg {} {}{})", + number(v.hue.into_degrees()), + percent(v.saturation), + percent(v.lightness), + alpha(v.alpha), + ) +} + +/// Displays an alpha component if it not 1. +fn alpha(value: f32) -> impl Display { + typst_utils::display(move |f| { + if !is_very_close(value, 1.0) { + write!(f, " / {}", percent(value))?; + } + Ok(()) + }) +} + +/// Displays a rounded percentage. +/// +/// For a percentage, two significant digits after the comma gives us a +/// precision of 1/10_000, which is more than 12 bits (see `is_very_close`). +fn percent(ratio: f32) -> impl Display { + typst_utils::display(move |f| { + write!(f, "{}%", typst_utils::round_with_precision(ratio as f64 * 100.0, 2)) + }) +} + +/// Rounds a number for display. +/// +/// For a number between 0 and 1, four significant digits give us a +/// precision of 1/10_000, which is more than 12 bits (see `is_very_close`). +fn number(value: f32) -> impl Display { + typst_utils::round_with_precision(value as f64, 4) +} + +/// Whether two component values are close enough that there is no +/// difference when encoding them with 12-bit. 12 bit is the highest +/// reasonable color bit depth found in the industry. +fn is_very_close(a: f32, b: f32) -> bool { + const MAX_BIT_DEPTH: u32 = 12; + const EPS: f32 = 0.5 / 2_i32.pow(MAX_BIT_DEPTH) as f32; + (a - b).abs() < EPS +} diff --git a/crates/typst-html/src/lib.rs b/crates/typst-html/src/lib.rs index 495187166..7063931b7 100644 --- a/crates/typst-html/src/lib.rs +++ b/crates/typst-html/src/lib.rs @@ -1,13 +1,19 @@ //! Typst's HTML exporter. +mod css; mod encode; +mod rules; +mod typed; pub use self::encode::html; +pub use self::rules::register; 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::foundations::{ + Content, Module, Scope, StyleChain, Target, TargetElem, +}; use typst_library::html::{ attr, tag, FrameElem, HtmlDocument, HtmlElem, HtmlElement, HtmlFrame, HtmlNode, }; @@ -18,9 +24,19 @@ use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Si use typst_library::model::{DocumentInfo, ParElem}; use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines}; use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; -use typst_library::World; +use typst_library::{Category, World}; use typst_syntax::Span; +/// Create a module with all HTML definitions. +pub fn module() -> Module { + let mut html = Scope::deduplicating(); + html.start_category(Category::Html); + html.define_elem::(); + html.define_elem::(); + crate::typed::define(&mut html); + Module::new("html", html) +} + /// Produce an HTML document from content. /// /// This first performs root-level realization and then turns the resulting @@ -177,12 +193,12 @@ fn handle( output.push(HtmlNode::Tag(elem.tag.clone())); } else if let Some(elem) = child.to_packed::() { let mut children = vec![]; - if let Some(body) = elem.body(styles) { + if let Some(body) = elem.body.get_ref(styles) { children = html_fragment(engine, body, locator.next(&elem.span()), styles)?; } let element = HtmlElement { tag: elem.tag, - attrs: elem.attrs(styles).clone(), + attrs: elem.attrs.get_cloned(styles), children, span: elem.span(), }; @@ -198,7 +214,7 @@ fn handle( ); } else if let Some(elem) = child.to_packed::() { // TODO: This is rather incomplete. - if let Some(body) = elem.body(styles) { + if let Some(body) = elem.body.get_ref(styles) { let children = html_fragment(engine, body, locator.next(&elem.span()), styles)?; output.push( @@ -212,7 +228,7 @@ fn handle( } else if let Some((elem, body)) = child .to_packed::() - .and_then(|elem| match elem.body(styles) { + .and_then(|elem| match elem.body.get_ref(styles) { Some(BlockBody::Content(body)) => Some((elem, body)), _ => None, }) @@ -233,12 +249,12 @@ fn handle( output.push(HtmlElement::new(tag::br).spanned(elem.span()).into()); } else if let Some(elem) = child.to_packed::() { output.push(HtmlNode::text( - if elem.double(styles) { '"' } else { '\'' }, + if elem.double.get(styles) { '"' } else { '\'' }, child.span(), )); } else if let Some(elem) = child.to_packed::() { let locator = locator.next(&elem.span()); - let style = TargetElem::set_target(Target::Paged).wrap(); + let style = TargetElem::target.set(Target::Paged).wrap(); let frame = (engine.routines.layout_frame)( engine, &elem.body, @@ -248,7 +264,7 @@ fn handle( )?; output.push(HtmlNode::Frame(HtmlFrame { inner: frame, - text_size: TextElem::size_in(styles), + text_size: styles.resolve(TextElem::size), })); } else { engine.sink.warn(warning!( diff --git a/crates/typst-html/src/rules.rs b/crates/typst-html/src/rules.rs new file mode 100644 index 000000000..5bf25e79b --- /dev/null +++ b/crates/typst-html/src/rules.rs @@ -0,0 +1,450 @@ +use std::num::NonZeroUsize; + +use ecow::{eco_format, EcoVec}; +use typst_library::diag::warning; +use typst_library::foundations::{ + Content, NativeElement, NativeRuleMap, ShowFn, Smart, StyleChain, Target, +}; +use typst_library::html::{attr, tag, HtmlAttrs, HtmlElem, HtmlTag}; +use typst_library::introspection::{Counter, Locator}; +use typst_library::layout::resolve::{table_to_cellgrid, Cell, CellGrid, Entry}; +use typst_library::layout::{OuterVAlignment, Sizing}; +use typst_library::model::{ + Attribution, CiteElem, CiteGroup, Destination, EmphElem, EnumElem, FigureCaption, + FigureElem, HeadingElem, LinkElem, LinkTarget, ListElem, ParbreakElem, QuoteElem, + RefElem, StrongElem, TableCell, TableElem, TermsElem, +}; +use typst_library::text::{ + HighlightElem, LinebreakElem, OverlineElem, RawElem, RawLine, SpaceElem, StrikeElem, + SubElem, SuperElem, UnderlineElem, +}; +use typst_library::visualize::ImageElem; + +use crate::css::{self, HtmlElemExt}; + +/// Register show rules for the [HTML target](Target::Html). +pub fn register(rules: &mut NativeRuleMap) { + use Target::Html; + + // Model. + rules.register(Html, STRONG_RULE); + rules.register(Html, EMPH_RULE); + rules.register(Html, LIST_RULE); + rules.register(Html, ENUM_RULE); + rules.register(Html, TERMS_RULE); + rules.register(Html, LINK_RULE); + rules.register(Html, HEADING_RULE); + rules.register(Html, FIGURE_RULE); + rules.register(Html, FIGURE_CAPTION_RULE); + rules.register(Html, QUOTE_RULE); + rules.register(Html, REF_RULE); + rules.register(Html, CITE_GROUP_RULE); + rules.register(Html, TABLE_RULE); + + // Text. + rules.register(Html, SUB_RULE); + rules.register(Html, SUPER_RULE); + rules.register(Html, UNDERLINE_RULE); + rules.register(Html, OVERLINE_RULE); + rules.register(Html, STRIKE_RULE); + rules.register(Html, HIGHLIGHT_RULE); + rules.register(Html, RAW_RULE); + rules.register(Html, RAW_LINE_RULE); + + // Visualize. + rules.register(Html, IMAGE_RULE); +} + +const STRONG_RULE: ShowFn = |elem, _, _| { + Ok(HtmlElem::new(tag::strong) + .with_body(Some(elem.body.clone())) + .pack() + .spanned(elem.span())) +}; + +const EMPH_RULE: ShowFn = |elem, _, _| { + Ok(HtmlElem::new(tag::em) + .with_body(Some(elem.body.clone())) + .pack() + .spanned(elem.span())) +}; + +const LIST_RULE: ShowFn = |elem, _, styles| { + Ok(HtmlElem::new(tag::ul) + .with_body(Some(Content::sequence(elem.children.iter().map(|item| { + // Text in wide lists shall always turn into paragraphs. + let mut body = item.body.clone(); + if !elem.tight.get(styles) { + body += ParbreakElem::shared(); + } + HtmlElem::new(tag::li) + .with_body(Some(body)) + .pack() + .spanned(item.span()) + })))) + .pack() + .spanned(elem.span())) +}; + +const ENUM_RULE: ShowFn = |elem, _, styles| { + let mut ol = HtmlElem::new(tag::ol); + + if elem.reversed.get(styles) { + ol = ol.with_attr(attr::reversed, "reversed"); + } + + if let Some(n) = elem.start.get(styles).custom() { + ol = ol.with_attr(attr::start, eco_format!("{n}")); + } + + let body = Content::sequence(elem.children.iter().map(|item| { + let mut li = HtmlElem::new(tag::li); + if let Some(nr) = item.number.get(styles) { + li = li.with_attr(attr::value, eco_format!("{nr}")); + } + // Text in wide enums shall always turn into paragraphs. + let mut body = item.body.clone(); + if !elem.tight.get(styles) { + body += ParbreakElem::shared(); + } + li.with_body(Some(body)).pack().spanned(item.span()) + })); + + Ok(ol.with_body(Some(body)).pack().spanned(elem.span())) +}; + +const TERMS_RULE: ShowFn = |elem, _, styles| { + Ok(HtmlElem::new(tag::dl) + .with_body(Some(Content::sequence(elem.children.iter().flat_map(|item| { + // Text in wide term lists shall always turn into paragraphs. + let mut description = item.description.clone(); + if !elem.tight.get(styles) { + description += ParbreakElem::shared(); + } + + [ + HtmlElem::new(tag::dt) + .with_body(Some(item.term.clone())) + .pack() + .spanned(item.term.span()), + HtmlElem::new(tag::dd) + .with_body(Some(description)) + .pack() + .spanned(item.description.span()), + ] + })))) + .pack()) +}; + +const LINK_RULE: ShowFn = |elem, engine, _| { + let body = elem.body.clone(); + Ok(if let LinkTarget::Dest(Destination::Url(url)) = &elem.dest { + HtmlElem::new(tag::a) + .with_attr(attr::href, url.clone().into_inner()) + .with_body(Some(body)) + .pack() + .spanned(elem.span()) + } else { + engine.sink.warn(warning!( + elem.span(), + "non-URL links are not yet supported by HTML export" + )); + body + }) +}; + +const HEADING_RULE: ShowFn = |elem, engine, styles| { + let span = elem.span(); + + let mut realized = elem.body.clone(); + if let Some(numbering) = elem.numbering.get_ref(styles).as_ref() { + let location = elem.location().unwrap(); + let numbering = Counter::of(HeadingElem::ELEM) + .display_at_loc(engine, location, styles, numbering)? + .spanned(span); + realized = numbering + SpaceElem::shared().clone() + realized; + } + + // HTML's h1 is closer to a title element. There should only be one. + // Meanwhile, a level 1 Typst heading is a section heading. For this + // reason, levels are offset by one: A Typst level 1 heading becomes + // a `

`. + let level = elem.resolve_level(styles).get(); + Ok(if level >= 6 { + engine.sink.warn(warning!( + span, + "heading of level {} was transformed to \ +
, which is not \ + supported by all assistive technology", + level, level + 1; + hint: "HTML only supports

to

, not ", level + 1; + hint: "you may want to restructure your document so that \ + it doesn't contain deep headings" + )); + HtmlElem::new(tag::div) + .with_body(Some(realized)) + .with_attr(attr::role, "heading") + .with_attr(attr::aria_level, eco_format!("{}", level + 1)) + .pack() + .spanned(span) + } else { + let t = [tag::h2, tag::h3, tag::h4, tag::h5, tag::h6][level - 1]; + HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span) + }) +}; + +const FIGURE_RULE: ShowFn = |elem, _, styles| { + let span = elem.span(); + let mut realized = elem.body.clone(); + + // Build the caption, if any. + if let Some(caption) = elem.caption.get_cloned(styles) { + realized = match caption.position.get(styles) { + OuterVAlignment::Top => caption.pack() + realized, + OuterVAlignment::Bottom => realized + caption.pack(), + }; + } + + // Ensure that the body is considered a paragraph. + realized += ParbreakElem::shared().clone().spanned(span); + + Ok(HtmlElem::new(tag::figure) + .with_body(Some(realized)) + .pack() + .spanned(span)) +}; + +const FIGURE_CAPTION_RULE: ShowFn = |elem, engine, styles| { + Ok(HtmlElem::new(tag::figcaption) + .with_body(Some(elem.realize(engine, styles)?)) + .pack() + .spanned(elem.span())) +}; + +const QUOTE_RULE: ShowFn = |elem, _, styles| { + let span = elem.span(); + let block = elem.block.get(styles); + + let mut realized = elem.body.clone(); + + if elem.quotes.get(styles).unwrap_or(!block) { + realized = QuoteElem::quoted(realized, styles); + } + + let attribution = elem.attribution.get_ref(styles); + + if block { + let mut blockquote = HtmlElem::new(tag::blockquote).with_body(Some(realized)); + if let Some(Attribution::Content(attribution)) = attribution { + if let Some(link) = attribution.to_packed::() { + if let LinkTarget::Dest(Destination::Url(url)) = &link.dest { + blockquote = + blockquote.with_attr(attr::cite, url.clone().into_inner()); + } + } + } + + realized = blockquote.pack().spanned(span); + + if let Some(attribution) = attribution.as_ref() { + realized += attribution.realize(span); + } + } else if let Some(Attribution::Label(label)) = attribution { + realized += SpaceElem::shared().clone(); + realized += CiteElem::new(*label).pack().spanned(span); + } + + Ok(realized) +}; + +const REF_RULE: ShowFn = |elem, engine, styles| elem.realize(engine, styles); + +const CITE_GROUP_RULE: ShowFn = |elem, engine, _| elem.realize(engine); + +const TABLE_RULE: ShowFn = |elem, engine, styles| { + // The locator is not used by HTML export, so we can just fabricate one. + let locator = Locator::root(); + Ok(show_cellgrid(table_to_cellgrid(elem, engine, locator, styles)?, styles)) +}; + +fn show_cellgrid(grid: CellGrid, styles: StyleChain) -> Content { + let elem = |tag, body| HtmlElem::new(tag).with_body(Some(body)).pack(); + let mut rows: Vec<_> = grid.entries.chunks(grid.non_gutter_column_count()).collect(); + + let tr = |tag, row: &[Entry]| { + let row = row + .iter() + .flat_map(|entry| entry.as_cell()) + .map(|cell| show_cell(tag, cell, styles)); + elem(tag::tr, Content::sequence(row)) + }; + + // TODO(subfooters): similarly to headers, take consecutive footers from + // the end for 'tfoot'. + let footer = grid.footer.map(|ft| { + let rows = rows.drain(ft.start..); + elem(tag::tfoot, Content::sequence(rows.map(|row| tr(tag::td, row)))) + }); + + // Store all consecutive headers at the start in 'thead'. All remaining + // headers are just 'th' rows across the table body. + let mut consecutive_header_end = 0; + let first_mid_table_header = grid + .headers + .iter() + .take_while(|hd| { + let is_consecutive = hd.range.start == consecutive_header_end; + consecutive_header_end = hd.range.end; + is_consecutive + }) + .count(); + + let (y_offset, header) = if first_mid_table_header > 0 { + let removed_header_rows = + grid.headers.get(first_mid_table_header - 1).unwrap().range.end; + let rows = rows.drain(..removed_header_rows); + + ( + removed_header_rows, + Some(elem(tag::thead, Content::sequence(rows.map(|row| tr(tag::th, row))))), + ) + } else { + (0, None) + }; + + // TODO: Consider improving accessibility properties of multi-level headers + // inside tables in the future, e.g. indicating which columns they are + // relative to and so on. See also: + // https://www.w3.org/WAI/tutorials/tables/multi-level/ + let mut next_header = first_mid_table_header; + let mut body = + Content::sequence(rows.into_iter().enumerate().map(|(relative_y, row)| { + let y = relative_y + y_offset; + if let Some(current_header) = + grid.headers.get(next_header).filter(|h| h.range.contains(&y)) + { + if y + 1 == current_header.range.end { + next_header += 1; + } + + tr(tag::th, row) + } else { + tr(tag::td, row) + } + })); + + if header.is_some() || footer.is_some() { + body = elem(tag::tbody, body); + } + + let content = header.into_iter().chain(core::iter::once(body)).chain(footer); + elem(tag::table, Content::sequence(content)) +} + +fn show_cell(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content { + let cell = cell.body.clone(); + let Some(cell) = cell.to_packed::() else { return cell }; + let mut attrs = HtmlAttrs::new(); + let span = |n: NonZeroUsize| (n != NonZeroUsize::MIN).then(|| n.to_string()); + if let Some(colspan) = span(cell.colspan.get(styles)) { + attrs.push(attr::colspan, colspan); + } + if let Some(rowspan) = span(cell.rowspan.get(styles)) { + attrs.push(attr::rowspan, rowspan); + } + HtmlElem::new(tag) + .with_body(Some(cell.body.clone())) + .with_attrs(attrs) + .pack() + .spanned(cell.span()) +} + +const SUB_RULE: ShowFn = |elem, _, _| { + Ok(HtmlElem::new(tag::sub) + .with_body(Some(elem.body.clone())) + .pack() + .spanned(elem.span())) +}; + +const SUPER_RULE: ShowFn = |elem, _, _| { + Ok(HtmlElem::new(tag::sup) + .with_body(Some(elem.body.clone())) + .pack() + .spanned(elem.span())) +}; + +const UNDERLINE_RULE: ShowFn = |elem, _, _| { + // Note: In modern HTML, `` is not the underline element, but + // rather an "Unarticulated Annotation" element (see HTML spec + // 4.5.22). Using `text-decoration` instead is recommended by MDN. + Ok(HtmlElem::new(tag::span) + .with_attr(attr::style, "text-decoration: underline") + .with_body(Some(elem.body.clone())) + .pack()) +}; + +const OVERLINE_RULE: ShowFn = |elem, _, _| { + Ok(HtmlElem::new(tag::span) + .with_attr(attr::style, "text-decoration: overline") + .with_body(Some(elem.body.clone())) + .pack()) +}; + +const STRIKE_RULE: ShowFn = + |elem, _, _| Ok(HtmlElem::new(tag::s).with_body(Some(elem.body.clone())).pack()); + +const HIGHLIGHT_RULE: ShowFn = + |elem, _, _| Ok(HtmlElem::new(tag::mark).with_body(Some(elem.body.clone())).pack()); + +const RAW_RULE: ShowFn = |elem, _, styles| { + let lines = elem.lines.as_deref().unwrap_or_default(); + + let mut seq = EcoVec::with_capacity((2 * lines.len()).saturating_sub(1)); + for (i, line) in lines.iter().enumerate() { + if i != 0 { + seq.push(LinebreakElem::shared().clone()); + } + + seq.push(line.clone().pack()); + } + + Ok(HtmlElem::new(if elem.block.get(styles) { tag::pre } else { tag::code }) + .with_body(Some(Content::sequence(seq))) + .pack() + .spanned(elem.span())) +}; + +const RAW_LINE_RULE: ShowFn = |elem, _, _| Ok(elem.body.clone()); + +const IMAGE_RULE: ShowFn = |elem, engine, styles| { + let image = elem.decode(engine, styles)?; + + let mut attrs = HtmlAttrs::new(); + attrs.push(attr::src, typst_svg::convert_image_to_base64_url(&image)); + + if let Some(alt) = elem.alt.get_cloned(styles) { + attrs.push(attr::alt, alt); + } + + let mut inline = css::Properties::new(); + + // TODO: Exclude in semantic profile. + if let Some(value) = typst_svg::convert_image_scaling(image.scaling()) { + inline.push("image-rendering", value); + } + + // TODO: Exclude in semantic profile? + match elem.width.get(styles) { + Smart::Auto => {} + Smart::Custom(rel) => inline.push("width", css::rel(rel)), + } + + // TODO: Exclude in semantic profile? + match elem.height.get(styles) { + Sizing::Auto => {} + Sizing::Rel(rel) => inline.push("height", css::rel(rel)), + Sizing::Fr(_) => {} + } + + Ok(HtmlElem::new(tag::img).with_attrs(attrs).with_styles(inline).pack()) +}; diff --git a/crates/typst-library/src/html/typed.rs b/crates/typst-html/src/typed.rs similarity index 79% rename from crates/typst-library/src/html/typed.rs rename to crates/typst-html/src/typed.rs index 1e7c1ad6f..4b794bbba 100644 --- a/crates/typst-library/src/html/typed.rs +++ b/crates/typst-html/src/typed.rs @@ -11,19 +11,20 @@ use bumpalo::Bump; use comemo::Tracked; use ecow::{eco_format, eco_vec, EcoString}; use typst_assets::html as data; -use typst_macros::cast; - -use crate::diag::{bail, At, Hint, HintedStrResult, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ +use typst_library::diag::{bail, At, Hint, HintedStrResult, SourceResult}; +use typst_library::engine::Engine; +use typst_library::foundations::{ Args, Array, AutoValue, CastInfo, Content, Context, Datetime, Dict, Duration, FromValue, IntoValue, NativeFuncData, NativeFuncPtr, NoneValue, ParamInfo, PositiveF64, Reflect, Scope, Str, Type, Value, }; -use crate::html::tag; -use crate::html::{HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag}; -use crate::layout::{Axes, Axis, Dir, Length}; -use crate::visualize::Color; +use typst_library::html::tag; +use typst_library::html::{HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag}; +use typst_library::layout::{Axes, Axis, Dir, Length}; +use typst_library::visualize::Color; +use typst_macros::cast; + +use crate::css; /// Hook up all typed HTML definitions. pub(super) fn define(html: &mut Scope) { @@ -127,12 +128,12 @@ fn construct(element: &'static data::ElemInfo, args: &mut Args) -> SourceResult< let tag = HtmlTag::constant(element.name); let mut elem = HtmlElem::new(tag); if !attrs.0.is_empty() { - elem.push_attrs(attrs); + elem.attrs.set(attrs); } if !tag::is_void(tag) { let body = args.eat::()?; - elem.push_body(body); + elem.body.set(body); } Ok(elem.into_value()) @@ -705,153 +706,6 @@ impl IntoAttr for SourceSize { } } -/// Conversion from Typst data types into CSS data types. -/// -/// This can be moved elsewhere once we start supporting more CSS stuff. -mod css { - use std::fmt::{self, Display}; - - use typst_utils::Numeric; - - use crate::layout::Length; - use crate::visualize::{Color, Hsl, LinearRgb, Oklab, Oklch, Rgb}; - - pub fn length(length: Length) -> impl Display { - typst_utils::display(move |f| match (length.abs.is_zero(), length.em.is_zero()) { - (false, false) => { - write!(f, "calc({}pt + {}em)", length.abs.to_pt(), length.em.get()) - } - (true, false) => write!(f, "{}em", length.em.get()), - (_, true) => write!(f, "{}pt", length.abs.to_pt()), - }) - } - - pub fn color(color: Color) -> impl Display { - typst_utils::display(move |f| match color { - Color::Rgb(_) | Color::Cmyk(_) | Color::Luma(_) => rgb(f, color.to_rgb()), - Color::Oklab(v) => oklab(f, v), - Color::Oklch(v) => oklch(f, v), - Color::LinearRgb(v) => linear_rgb(f, v), - Color::Hsl(_) | Color::Hsv(_) => hsl(f, color.to_hsl()), - }) - } - - fn oklab(f: &mut fmt::Formatter<'_>, v: Oklab) -> fmt::Result { - write!( - f, - "oklab({} {} {}{})", - percent(v.l), - number(v.a), - number(v.b), - alpha(v.alpha) - ) - } - - fn oklch(f: &mut fmt::Formatter<'_>, v: Oklch) -> fmt::Result { - write!( - f, - "oklch({} {} {}deg{})", - percent(v.l), - number(v.chroma), - number(v.hue.into_degrees()), - alpha(v.alpha) - ) - } - - fn rgb(f: &mut fmt::Formatter<'_>, v: Rgb) -> fmt::Result { - if let Some(v) = rgb_to_8_bit_lossless(v) { - let (r, g, b, a) = v.into_components(); - write!(f, "#{r:02x}{g:02x}{b:02x}")?; - if a != u8::MAX { - write!(f, "{a:02x}")?; - } - Ok(()) - } else { - write!( - f, - "rgb({} {} {}{})", - percent(v.red), - percent(v.green), - percent(v.blue), - alpha(v.alpha) - ) - } - } - - /// Converts an f32 RGBA color to its 8-bit representation if the result is - /// [very close](is_very_close) to the original. - fn rgb_to_8_bit_lossless( - v: Rgb, - ) -> Option> { - let l = v.into_format::(); - let h = l.into_format::(); - (is_very_close(v.red, h.red) - && is_very_close(v.blue, h.blue) - && is_very_close(v.green, h.green) - && is_very_close(v.alpha, h.alpha)) - .then_some(l) - } - - fn linear_rgb(f: &mut fmt::Formatter<'_>, v: LinearRgb) -> fmt::Result { - write!( - f, - "color(srgb-linear {} {} {}{})", - percent(v.red), - percent(v.green), - percent(v.blue), - alpha(v.alpha), - ) - } - - fn hsl(f: &mut fmt::Formatter<'_>, v: Hsl) -> fmt::Result { - write!( - f, - "hsl({}deg {} {}{})", - number(v.hue.into_degrees()), - percent(v.saturation), - percent(v.lightness), - alpha(v.alpha), - ) - } - - /// Displays an alpha component if it not 1. - fn alpha(value: f32) -> impl Display { - typst_utils::display(move |f| { - if !is_very_close(value, 1.0) { - write!(f, " / {}", percent(value))?; - } - Ok(()) - }) - } - - /// Displays a rounded percentage. - /// - /// For a percentage, two significant digits after the comma gives us a - /// precision of 1/10_000, which is more than 12 bits (see `is_very_close`). - fn percent(ratio: f32) -> impl Display { - typst_utils::display(move |f| { - write!(f, "{}%", typst_utils::round_with_precision(ratio as f64 * 100.0, 2)) - }) - } - - /// Rounds a number for display. - /// - /// For a number between 0 and 1, four significant digits give us a - /// precision of 1/10_000, which is more than 12 bits (see `is_very_close`). - fn number(value: f32) -> impl Display { - typst_utils::round_with_precision(value as f64, 4) - } - - /// Whether two component values are close enough that there is no - /// difference when encoding them with 12-bit. 12 bit is the highest - /// reasonable color bit depth found in the industry. - fn is_very_close(a: f32, b: f32) -> bool { - const MAX_BIT_DEPTH: u32 = 12; - const EPS: f32 = 0.5 / 2_i32.pow(MAX_BIT_DEPTH) as f32; - (a - b).abs() < EPS - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/typst-ide/src/analyze.rs b/crates/typst-ide/src/analyze.rs index c493da81a..76739fec0 100644 --- a/crates/typst-ide/src/analyze.rs +++ b/crates/typst-ide/src/analyze.rs @@ -2,7 +2,7 @@ use comemo::Track; use ecow::{eco_vec, EcoString, EcoVec}; use typst::foundations::{Label, Styles, Value}; use typst::layout::PagedDocument; -use typst::model::BibliographyElem; +use typst::model::{BibliographyElem, FigureElem}; use typst::syntax::{ast, LinkedNode, SyntaxKind}; use crate::IdeWorld; @@ -75,8 +75,13 @@ pub fn analyze_labels( for elem in document.introspector.all() { let Some(label) = elem.label() else { continue }; let details = elem - .get_by_name("caption") - .or_else(|_| elem.get_by_name("body")) + .to_packed::() + .and_then(|figure| match figure.caption.as_option() { + Some(Some(caption)) => Some(caption.pack_ref()), + _ => None, + }) + .unwrap_or(elem) + .get_by_name("body") .ok() .and_then(|field| match field { Value::Content(content) => Some(content), diff --git a/crates/typst-ide/src/definition.rs b/crates/typst-ide/src/definition.rs index ae1ba287b..4c2b80cd4 100644 --- a/crates/typst-ide/src/definition.rs +++ b/crates/typst-ide/src/definition.rs @@ -187,6 +187,6 @@ mod tests { #[test] fn test_definition_std() { - test("#table", 1, Side::After).must_be_value(typst::model::TableElem::elem()); + test("#table", 1, Side::After).must_be_value(typst::model::TableElem::ELEM); } } diff --git a/crates/typst-ide/src/tests.rs b/crates/typst-ide/src/tests.rs index dd5c230ad..168dfc9f2 100644 --- a/crates/typst-ide/src/tests.rs +++ b/crates/typst-ide/src/tests.rs @@ -10,7 +10,7 @@ use typst::syntax::package::{PackageSpec, PackageVersion}; use typst::syntax::{FileId, Source, VirtualPath}; use typst::text::{Font, FontBook, TextElem, TextSize}; use typst::utils::{singleton, LazyHash}; -use typst::{Feature, Library, World}; +use typst::{Feature, Library, LibraryExt, World}; use crate::IdeWorld; @@ -171,13 +171,11 @@ fn library() -> Library { let mut lib = typst::Library::builder() .with_features([Feature::Html].into_iter().collect()) .build(); + lib.styles.set(PageElem::width, Smart::Custom(Abs::pt(120.0).into())); + lib.styles.set(PageElem::height, Smart::Auto); lib.styles - .set(PageElem::set_width(Smart::Custom(Abs::pt(120.0).into()))); - lib.styles.set(PageElem::set_height(Smart::Auto)); - lib.styles.set(PageElem::set_margin(Margin::splat(Some(Smart::Custom( - Abs::pt(10.0).into(), - ))))); - lib.styles.set(TextElem::set_size(TextSize(Abs::pt(10.0).into()))); + .set(PageElem::margin, Margin::splat(Some(Smart::Custom(Abs::pt(10.0).into())))); + lib.styles.set(TextElem::size, TextSize(Abs::pt(10.0).into())); lib } diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs index 528f679cf..e0d66a89b 100644 --- a/crates/typst-ide/src/tooltip.rs +++ b/crates/typst-ide/src/tooltip.rs @@ -378,4 +378,9 @@ mod tests { .with_source("other.typ", "#let f = (x) => 1"); test(&world, -4, Side::After).must_be_code("(..) => .."); } + + #[test] + fn test_tooltip_reference() { + test("#figure(caption: [Hi])[] @f", -1, Side::Before).must_be_text("Hi"); + } } diff --git a/crates/typst-kit/src/package.rs b/crates/typst-kit/src/package.rs index 584ec83c0..e62e843cd 100644 --- a/crates/typst-kit/src/package.rs +++ b/crates/typst-kit/src/package.rs @@ -199,7 +199,7 @@ impl PackageStorage { // The place at which the specific package version will live in the end. let package_dir = base_dir.join(format!("{}", spec.version)); - // To prevent multiple Typst instances from interferring, we download + // To prevent multiple Typst instances from interfering, we download // into a temporary directory first and then move this directory to // its final destination. // diff --git a/crates/typst-layout/Cargo.toml b/crates/typst-layout/Cargo.toml index cc355a3db..2c314e5c5 100644 --- a/crates/typst-layout/Cargo.toml +++ b/crates/typst-layout/Cargo.toml @@ -21,6 +21,7 @@ typst-timing = { workspace = true } typst-utils = { workspace = true } az = { workspace = true } bumpalo = { workspace = true } +codex = { workspace = true } comemo = { workspace = true } ecow = { workspace = true } hypher = { workspace = true } diff --git a/crates/typst-layout/src/flow/block.rs b/crates/typst-layout/src/flow/block.rs index 6c2c3923d..d6cfe3a9e 100644 --- a/crates/typst-layout/src/flow/block.rs +++ b/crates/typst-layout/src/flow/block.rs @@ -24,15 +24,15 @@ pub fn layout_single_block( region: Region, ) -> SourceResult { // Fetch sizing properties. - let width = elem.width(styles); - let height = elem.height(styles); - let inset = elem.inset(styles).unwrap_or_default(); + let width = elem.width.get(styles); + let height = elem.height.get(styles); + let inset = elem.inset.resolve(styles).unwrap_or_default(); // Build the pod regions. let pod = unbreakable_pod(&width.into(), &height, &inset, styles, region.size); // Layout the body. - let body = elem.body(styles); + let body = elem.body.get_ref(styles); let mut frame = match body { // If we have no body, just create one frame. Its size will be // adjusted below. @@ -73,18 +73,19 @@ pub fn layout_single_block( } // Prepare fill and stroke. - let fill = elem.fill(styles); + let fill = elem.fill.get_cloned(styles); let stroke = elem - .stroke(styles) + .stroke + .resolve(styles) .unwrap_or_default() .map(|s| s.map(Stroke::unwrap_or_default)); // Only fetch these if necessary (for clipping or filling/stroking). - let outset = LazyCell::new(|| elem.outset(styles).unwrap_or_default()); - let radius = LazyCell::new(|| elem.radius(styles).unwrap_or_default()); + let outset = LazyCell::new(|| elem.outset.resolve(styles).unwrap_or_default()); + let radius = LazyCell::new(|| elem.radius.resolve(styles).unwrap_or_default()); // Clip the contents, if requested. - if elem.clip(styles) { + if elem.clip.get(styles) { frame.clip(clip_rect(frame.size(), &radius, &stroke, &outset)); } @@ -111,9 +112,9 @@ pub fn layout_multi_block( regions: Regions, ) -> SourceResult { // Fetch sizing properties. - let width = elem.width(styles); - let height = elem.height(styles); - let inset = elem.inset(styles).unwrap_or_default(); + let width = elem.width.get(styles); + let height = elem.height.get(styles); + let inset = elem.inset.resolve(styles).unwrap_or_default(); // Allocate a small vector for backlogs. let mut buf = SmallVec::<[Abs; 2]>::new(); @@ -122,7 +123,7 @@ pub fn layout_multi_block( let pod = breakable_pod(&width.into(), &height, &inset, styles, regions, &mut buf); // Layout the body. - let body = elem.body(styles); + let body = elem.body.get_ref(styles); let mut fragment = match body { // If we have no body, just create one frame plus one per backlog // region. We create them zero-sized; if necessary, their size will @@ -188,18 +189,19 @@ pub fn layout_multi_block( }; // Prepare fill and stroke. - let fill = elem.fill(styles); + let fill = elem.fill.get_ref(styles); let stroke = elem - .stroke(styles) + .stroke + .resolve(styles) .unwrap_or_default() .map(|s| s.map(Stroke::unwrap_or_default)); // Only fetch these if necessary (for clipping or filling/stroking). - let outset = LazyCell::new(|| elem.outset(styles).unwrap_or_default()); - let radius = LazyCell::new(|| elem.radius(styles).unwrap_or_default()); + let outset = LazyCell::new(|| elem.outset.resolve(styles).unwrap_or_default()); + let radius = LazyCell::new(|| elem.radius.resolve(styles).unwrap_or_default()); // Fetch/compute these outside of the loop. - let clip = elem.clip(styles); + let clip = elem.clip.get(styles); let has_fill_or_stroke = fill.is_some() || stroke.iter().any(Option::is_some); let has_inset = !inset.is_zero(); let is_explicit = matches!(body, None | Some(BlockBody::Content(_))); diff --git a/crates/typst-layout/src/flow/collect.rs b/crates/typst-layout/src/flow/collect.rs index 2c14f7a37..76268b590 100644 --- a/crates/typst-layout/src/flow/collect.rs +++ b/crates/typst-layout/src/flow/collect.rs @@ -89,7 +89,7 @@ impl<'a> Collector<'a, '_, '_> { } else if child.is::() { self.output.push(Child::Flush); } else if let Some(elem) = child.to_packed::() { - self.output.push(Child::Break(elem.weak(styles))); + self.output.push(Child::Break(elem.weak.get(styles))); } else if child.is::() { bail!( child.span(), "pagebreaks are not allowed inside of containers"; @@ -132,7 +132,7 @@ impl<'a> Collector<'a, '_, '_> { self.output.push(Child::Tag(&elem.tag)); } - let leading = ParElem::leading_in(styles); + let leading = styles.resolve(ParElem::leading); self.lines(lines, leading, styles); for (c, _) in &self.children[end..] { @@ -146,7 +146,9 @@ impl<'a> Collector<'a, '_, '_> { /// Collect vertical spacing into a relative or fractional child. fn v(&mut self, elem: &'a Packed, styles: StyleChain<'a>) { self.output.push(match elem.amount { - Spacing::Rel(rel) => Child::Rel(rel.resolve(styles), elem.weak(styles) as u8), + Spacing::Rel(rel) => { + Child::Rel(rel.resolve(styles), elem.weak.get(styles) as u8) + } Spacing::Fr(fr) => Child::Fr(fr), }); } @@ -169,8 +171,8 @@ impl<'a> Collector<'a, '_, '_> { )? .into_frames(); - let spacing = elem.spacing(styles); - let leading = elem.leading(styles); + let spacing = elem.spacing.resolve(styles); + let leading = elem.leading.resolve(styles); self.output.push(Child::Rel(spacing.into(), 4)); @@ -184,8 +186,8 @@ impl<'a> Collector<'a, '_, '_> { /// Collect laid-out lines. fn lines(&mut self, lines: Vec, leading: Abs, styles: StyleChain<'a>) { - let align = AlignElem::alignment_in(styles).resolve(styles); - let costs = TextElem::costs_in(styles); + let align = styles.resolve(AlignElem::alignment); + let costs = styles.get(TextElem::costs); // Determine whether to prevent widow and orphans. let len = lines.len(); @@ -231,23 +233,23 @@ impl<'a> Collector<'a, '_, '_> { /// whether it is breakable. fn block(&mut self, elem: &'a Packed, styles: StyleChain<'a>) { let locator = self.locator.next(&elem.span()); - let align = AlignElem::alignment_in(styles).resolve(styles); + let align = styles.resolve(AlignElem::alignment); let alone = self.children.len() == 1; - let sticky = elem.sticky(styles); - let breakable = elem.breakable(styles); - let fr = match elem.height(styles) { + let sticky = elem.sticky.get(styles); + let breakable = elem.breakable.get(styles); + let fr = match elem.height.get(styles) { Sizing::Fr(fr) => Some(fr), _ => None, }; - let fallback = LazyCell::new(|| ParElem::spacing_in(styles)); + let fallback = LazyCell::new(|| styles.resolve(ParElem::spacing)); let spacing = |amount| match amount { Smart::Auto => Child::Rel((*fallback).into(), 4), Smart::Custom(Spacing::Rel(rel)) => Child::Rel(rel.resolve(styles), 3), Smart::Custom(Spacing::Fr(fr)) => Child::Fr(fr), }; - self.output.push(spacing(elem.above(styles))); + self.output.push(spacing(elem.above.get(styles))); if !breakable || fr.is_some() { self.output.push(Child::Single(self.boxed(SingleChild { @@ -272,7 +274,7 @@ impl<'a> Collector<'a, '_, '_> { }))); }; - self.output.push(spacing(elem.below(styles))); + self.output.push(spacing(elem.below.get(styles))); self.par_situation = ParSituation::Other; } @@ -282,13 +284,13 @@ impl<'a> Collector<'a, '_, '_> { elem: &'a Packed, styles: StyleChain<'a>, ) -> SourceResult<()> { - let alignment = elem.alignment(styles); + let alignment = elem.alignment.get(styles); let align_x = alignment.map_or(FixedAlignment::Center, |align| { align.x().unwrap_or_default().resolve(styles) }); let align_y = alignment.map(|align| align.y().map(|y| y.resolve(styles))); - let scope = elem.scope(styles); - let float = elem.float(styles); + let scope = elem.scope.get(styles); + let float = elem.float.get(styles); match (float, align_y) { (true, Smart::Custom(None | Some(FixedAlignment::Center))) => bail!( @@ -312,8 +314,8 @@ impl<'a> Collector<'a, '_, '_> { } let locator = self.locator.next(&elem.span()); - let clearance = elem.clearance(styles); - let delta = Axes::new(elem.dx(styles), elem.dy(styles)).resolve(styles); + let clearance = elem.clearance.resolve(styles); + let delta = Axes::new(elem.dx.get(styles), elem.dy.get(styles)).resolve(styles); self.output.push(Child::Placed(self.boxed(PlacedChild { align_x, align_y, @@ -631,7 +633,7 @@ impl PlacedChild<'_> { pub fn layout(&self, engine: &mut Engine, base: Size) -> SourceResult { self.cell.get_or_init(base, |base| { let align = self.alignment.unwrap_or_else(|| Alignment::CENTER); - let aligned = AlignElem::set_alignment(align).wrap(); + let aligned = AlignElem::alignment.set(align).wrap(); let styles = self.styles.chain(&aligned); let mut frame = layout_and_modify(styles, |styles| { diff --git a/crates/typst-layout/src/flow/compose.rs b/crates/typst-layout/src/flow/compose.rs index 54dc487a3..ed514a248 100644 --- a/crates/typst-layout/src/flow/compose.rs +++ b/crates/typst-layout/src/flow/compose.rs @@ -851,7 +851,7 @@ fn layout_line_number_reset( config: &Config, locator: &mut SplitLocator, ) -> SourceResult { - let counter = Counter::of(ParLineMarker::elem()); + let counter = Counter::of(ParLineMarker::ELEM); let update = CounterUpdate::Set(CounterState::init(false)); let content = counter.update(Span::detached(), update); crate::layout_frame( @@ -879,7 +879,7 @@ fn layout_line_number( locator: &mut SplitLocator, numbering: &Numbering, ) -> SourceResult { - let counter = Counter::of(ParLineMarker::elem()); + let counter = Counter::of(ParLineMarker::ELEM); let update = CounterUpdate::Step(NonZeroUsize::ONE); let numbering = Smart::Custom(numbering.clone()); diff --git a/crates/typst-layout/src/flow/mod.rs b/crates/typst-layout/src/flow/mod.rs index cba228bcd..f4f1c0915 100644 --- a/crates/typst-layout/src/flow/mod.rs +++ b/crates/typst-layout/src/flow/mod.rs @@ -98,8 +98,8 @@ pub fn layout_columns( locator.track(), styles, regions, - elem.count(styles), - elem.gutter(styles), + elem.count.get(styles), + elem.gutter.resolve(styles), ) } @@ -251,22 +251,22 @@ fn configuration<'x>( let gutter = column_gutter.relative_to(regions.base().x); let width = (regions.size.x - gutter * (count - 1) as f64) / count as f64; - let dir = TextElem::dir_in(shared); + let dir = shared.resolve(TextElem::dir); ColumnConfig { count, width, gutter, dir } }, footnote: FootnoteConfig { - separator: FootnoteEntry::separator_in(shared), - clearance: FootnoteEntry::clearance_in(shared), - gap: FootnoteEntry::gap_in(shared), + separator: shared.get_cloned(FootnoteEntry::separator), + clearance: shared.resolve(FootnoteEntry::clearance), + gap: shared.resolve(FootnoteEntry::gap), expand: regions.expand.x, }, line_numbers: (mode == FlowMode::Root).then(|| LineNumberConfig { - scope: ParLine::numbering_scope_in(shared), + scope: shared.get(ParLine::numbering_scope), default_clearance: { - let width = if PageElem::flipped_in(shared) { - PageElem::height_in(shared) + let width = if shared.get(PageElem::flipped) { + shared.resolve(PageElem::height) } else { - PageElem::width_in(shared) + shared.resolve(PageElem::width) }; // Clamp below is safe (min <= max): if the font size is diff --git a/crates/typst-layout/src/grid/layouter.rs b/crates/typst-layout/src/grid/layouter.rs index 42fe38dbe..d4f11f470 100644 --- a/crates/typst-layout/src/grid/layouter.rs +++ b/crates/typst-layout/src/grid/layouter.rs @@ -249,7 +249,7 @@ impl<'a> GridLayouter<'a> { rowspans: vec![], finished: vec![], finished_header_rows: vec![], - is_rtl: TextElem::dir_in(styles) == Dir::RTL, + is_rtl: styles.resolve(TextElem::dir) == Dir::RTL, repeating_headers: vec![], upcoming_headers: &grid.headers, pending_headers: Default::default(), diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index a8f4a0c81..d4fd121ec 100644 --- a/crates/typst-layout/src/image.rs +++ b/crates/typst-layout/src/image.rs @@ -1,18 +1,11 @@ -use std::ffi::OsStr; - -use typst_library::diag::{warning, At, LoadedWithin, SourceResult, StrResult}; +use typst_library::diag::SourceResult; use typst_library::engine::Engine; -use typst_library::foundations::{Bytes, Derived, Packed, Smart, StyleChain}; +use typst_library::foundations::{Packed, StyleChain}; use typst_library::introspection::Locator; use typst_library::layout::{ Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size, }; -use typst_library::loading::DataSource; -use typst_library::text::families; -use typst_library::visualize::{ - Curve, ExchangeFormat, Image, ImageElem, ImageFit, ImageFormat, ImageKind, - RasterImage, SvgImage, VectorFormat, -}; +use typst_library::visualize::{Curve, Image, ImageElem, ImageFit}; /// Layout the image. #[typst_macros::time(span = elem.span())] @@ -23,53 +16,7 @@ pub fn layout_image( styles: StyleChain, region: Region, ) -> SourceResult { - let span = elem.span(); - - // Take the format that was explicitly defined, or parse the extension, - // or try to detect the format. - let Derived { source, derived: loaded } = &elem.source; - let format = match elem.format(styles) { - Smart::Custom(v) => v, - Smart::Auto => determine_format(source, &loaded.data).at(span)?, - }; - - // Warn the user if the image contains a foreign object. Not perfect - // because the svg could also be encoded, but that's an edge case. - if format == ImageFormat::Vector(VectorFormat::Svg) { - let has_foreign_object = - memchr::memmem::find(&loaded.data, b" ImageKind::Raster( - RasterImage::new( - loaded.data.clone(), - format, - elem.icc(styles).as_ref().map(|icc| icc.derived.clone()), - ) - .at(span)?, - ), - ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg( - SvgImage::with_fonts( - loaded.data.clone(), - engine.world, - &families(styles).map(|f| f.as_str()).collect::>(), - ) - .within(loaded)?, - ), - }; - - let image = Image::new(kind, elem.alt(styles), elem.scaling(styles)); + let image = elem.decode(engine, styles)?; // Determine the image's pixel aspect ratio. let pxw = image.width(); @@ -106,7 +53,7 @@ pub fn layout_image( }; // Compute the actual size of the fitted image. - let fit = elem.fit(styles); + let fit = elem.fit.get(styles); let fitted = match fit { ImageFit::Cover | ImageFit::Contain => { if wide == (fit == ImageFit::Contain) { @@ -122,7 +69,7 @@ pub fn layout_image( // the frame to the target size, center aligning the image in the // process. let mut frame = Frame::soft(fitted); - frame.push(Point::zero(), FrameItem::Image(image, fitted, span)); + frame.push(Point::zero(), FrameItem::Image(image, fitted, elem.span())); frame.resize(target, Axes::splat(FixedAlignment::Center)); // Create a clipping group if only part of the image should be visible. @@ -132,25 +79,3 @@ pub fn layout_image( Ok(frame) } - -/// Try to determine the image format based on the data. -fn determine_format(source: &DataSource, data: &Bytes) -> StrResult { - if let DataSource::Path(path) = source { - let ext = std::path::Path::new(path.as_str()) - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default() - .to_lowercase(); - - match ext.as_str() { - "png" => return Ok(ExchangeFormat::Png.into()), - "jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()), - "gif" => return Ok(ExchangeFormat::Gif.into()), - "svg" | "svgz" => return Ok(VectorFormat::Svg.into()), - "webp" => return Ok(ExchangeFormat::Webp.into()), - _ => {} - } - } - - Ok(ImageFormat::detect(data).ok_or("unknown image format")?) -} diff --git a/crates/typst-layout/src/inline/box.rs b/crates/typst-layout/src/inline/box.rs index e21928d3c..65b025334 100644 --- a/crates/typst-layout/src/inline/box.rs +++ b/crates/typst-layout/src/inline/box.rs @@ -21,15 +21,15 @@ pub fn layout_box( region: Size, ) -> SourceResult { // Fetch sizing properties. - let width = elem.width(styles); - let height = elem.height(styles); - let inset = elem.inset(styles).unwrap_or_default(); + let width = elem.width.get(styles); + let height = elem.height.get(styles); + let inset = elem.inset.resolve(styles).unwrap_or_default(); // Build the pod region. let pod = unbreakable_pod(&width, &height.into(), &inset, styles, region); // Layout the body. - let mut frame = match elem.body(styles) { + let mut frame = match elem.body.get_ref(styles) { // If we have no body, just create an empty frame. If necessary, // its size will be adjusted below. None => Frame::hard(Size::zero()), @@ -50,18 +50,19 @@ pub fn layout_box( } // Prepare fill and stroke. - let fill = elem.fill(styles); + let fill = elem.fill.get_cloned(styles); let stroke = elem - .stroke(styles) + .stroke + .resolve(styles) .unwrap_or_default() .map(|s| s.map(Stroke::unwrap_or_default)); // Only fetch these if necessary (for clipping or filling/stroking). - let outset = LazyCell::new(|| elem.outset(styles).unwrap_or_default()); - let radius = LazyCell::new(|| elem.radius(styles).unwrap_or_default()); + let outset = LazyCell::new(|| elem.outset.resolve(styles).unwrap_or_default()); + let radius = LazyCell::new(|| elem.radius.resolve(styles).unwrap_or_default()); // Clip the contents, if requested. - if elem.clip(styles) { + if elem.clip.get(styles) { frame.clip(clip_rect(frame.size(), &radius, &stroke, &outset)); } @@ -78,7 +79,7 @@ pub fn layout_box( // Apply baseline shift. Do this after setting the size and applying the // inset, so that a relative shift is resolved relative to the final // height. - let shift = elem.baseline(styles).relative_to(frame.height()); + let shift = elem.baseline.resolve(styles).relative_to(frame.height()); if !shift.is_zero() { frame.set_baseline(frame.baseline() - shift); } diff --git a/crates/typst-layout/src/inline/collect.rs b/crates/typst-layout/src/inline/collect.rs index 5a1b7b4fc..2744b31e0 100644 --- a/crates/typst-layout/src/inline/collect.rs +++ b/crates/typst-layout/src/inline/collect.rs @@ -144,7 +144,7 @@ pub fn collect<'a>( collector.push_text(" ", styles); } else if let Some(elem) = child.to_packed::() { collector.build_text(styles, |full| { - let dir = TextElem::dir_in(styles); + let dir = styles.resolve(TextElem::dir); if dir != config.dir { // Insert "Explicit Directional Embedding". match dir { @@ -154,7 +154,7 @@ pub fn collect<'a>( } } - if let Some(case) = TextElem::case_in(styles) { + if let Some(case) = styles.get(TextElem::case) { full.push_str(&case.apply(&elem.text)); } else { full.push_str(&elem.text); @@ -174,20 +174,22 @@ pub fn collect<'a>( Spacing::Fr(fr) => Item::Fractional(fr, None), Spacing::Rel(rel) => Item::Absolute( rel.resolve(styles).relative_to(region.x), - elem.weak(styles), + elem.weak.get(styles), ), }); } else if let Some(elem) = child.to_packed::() { - collector - .push_text(if elem.justify(styles) { "\u{2028}" } else { "\n" }, styles); + collector.push_text( + if elem.justify.get(styles) { "\u{2028}" } else { "\n" }, + styles, + ); } else if let Some(elem) = child.to_packed::() { - let double = elem.double(styles); - if elem.enabled(styles) { + let double = elem.double.get(styles); + if elem.enabled.get(styles) { let quotes = SmartQuotes::get( - elem.quotes(styles), - TextElem::lang_in(styles), - TextElem::region_in(styles), - elem.alternative(styles), + elem.quotes.get_ref(styles), + styles.get(TextElem::lang), + styles.get(TextElem::region), + elem.alternative.get(styles), ); let before = collector.full.chars().rev().find(|&c| !is_default_ignorable(c)); @@ -206,7 +208,7 @@ pub fn collect<'a>( } InlineItem::Frame(mut frame) => { frame.modify(&FrameModifiers::get_in(styles)); - apply_baseline_shift(&mut frame, styles); + apply_shift(&engine.world, &mut frame, styles); collector.push_item(Item::Frame(frame)); } } @@ -215,13 +217,13 @@ pub fn collect<'a>( collector.push_item(Item::Skip(POP_ISOLATE)); } else if let Some(elem) = child.to_packed::() { let loc = locator.next(&elem.span()); - if let Sizing::Fr(v) = elem.width(styles) { + if let Sizing::Fr(v) = elem.width.get(styles) { collector.push_item(Item::Fractional(v, Some((elem, loc, styles)))); } else { let mut frame = layout_and_modify(styles, |styles| { layout_box(elem, engine, loc, styles, region) })?; - apply_baseline_shift(&mut frame, styles); + apply_shift(&engine.world, &mut frame, styles); collector.push_item(Item::Frame(frame)); } } else if let Some(elem) = child.to_packed::() { diff --git a/crates/typst-layout/src/inline/line.rs b/crates/typst-layout/src/inline/line.rs index 7bf4d4c73..58162d12b 100644 --- a/crates/typst-layout/src/inline/line.rs +++ b/crates/typst-layout/src/inline/line.rs @@ -2,10 +2,11 @@ use std::fmt::{self, Debug, Formatter}; use std::ops::{Deref, DerefMut}; use typst_library::engine::Engine; +use typst_library::foundations::Resolve; use typst_library::introspection::{SplitLocator, Tag}; use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point}; use typst_library::model::ParLineMarker; -use typst_library::text::{Lang, TextElem}; +use typst_library::text::{variant, Lang, TextElem}; use typst_utils::Numeric; use super::*; @@ -330,7 +331,7 @@ fn adjust_cj_at_line_start(p: &Preparation, items: &mut Items) { let glyph = shaped.glyphs.to_mut().first_mut().unwrap(); let shrink = glyph.shrinkability().0; glyph.shrink_left(shrink); - shaped.width -= shrink.at(shaped.size); + shaped.width -= shrink.at(glyph.size); } else if p.config.cjk_latin_spacing && glyph.is_cj_script() && glyph.x_offset > Em::zero() @@ -342,7 +343,7 @@ fn adjust_cj_at_line_start(p: &Preparation, items: &mut Items) { glyph.x_advance -= shrink; glyph.x_offset = Em::zero(); glyph.adjustability.shrinkability.0 = Em::zero(); - shaped.width -= shrink.at(shaped.size); + shaped.width -= shrink.at(glyph.size); } } @@ -360,7 +361,7 @@ fn adjust_cj_at_line_end(p: &Preparation, items: &mut Items) { let shrink = glyph.shrinkability().1; let punct = shaped.glyphs.to_mut().last_mut().unwrap(); punct.shrink_right(shrink); - shaped.width -= shrink.at(shaped.size); + shaped.width -= shrink.at(punct.size); } else if p.config.cjk_latin_spacing && glyph.is_cj_script() && (glyph.x_advance - glyph.x_offset) > Em::one() @@ -371,7 +372,7 @@ fn adjust_cj_at_line_end(p: &Preparation, items: &mut Items) { let glyph = shaped.glyphs.to_mut().last_mut().unwrap(); glyph.x_advance -= shrink; glyph.adjustability.shrinkability.1 = Em::zero(); - shaped.width -= shrink.at(shaped.size); + shaped.width -= shrink.at(glyph.size); } } @@ -412,9 +413,31 @@ fn should_repeat_hyphen(pred_line: &Line, text: &str) -> bool { } } -/// Apply the current baseline shift to a frame. -pub fn apply_baseline_shift(frame: &mut Frame, styles: StyleChain) { - frame.translate(Point::with_y(TextElem::baseline_in(styles))); +/// Apply the current baseline shift and italic compensation to a frame. +pub fn apply_shift<'a>( + world: &Tracked<'a, dyn World + 'a>, + frame: &mut Frame, + styles: StyleChain, +) { + let mut baseline = styles.resolve(TextElem::baseline); + let mut compensation = Abs::zero(); + if let Some(scripts) = styles.get_ref(TextElem::shift_settings) { + let font_metrics = styles + .get_ref(TextElem::font) + .into_iter() + .find_map(|family| { + world + .book() + .select(family.as_str(), variant(styles)) + .and_then(|id| world.font(id)) + }) + .map_or(*scripts.kind.default_metrics(), |f| { + *scripts.kind.read_metrics(f.metrics()) + }); + baseline -= scripts.shift.unwrap_or(font_metrics.vertical_offset).resolve(styles); + compensation += font_metrics.horizontal_offset.resolve(styles); + } + frame.translate(Point::new(compensation, baseline)); } /// Commit to a line and build its frame. @@ -441,10 +464,10 @@ pub fn commit( if let Some(Item::Text(text)) = line.items.first() { if let Some(glyph) = text.glyphs.first() { if !text.dir.is_positive() - && TextElem::overhang_in(text.styles) + && text.styles.get(TextElem::overhang) && (line.items.len() > 1 || text.glyphs.len() > 1) { - let amount = overhang(glyph.c) * glyph.x_advance.at(text.size); + let amount = overhang(glyph.c) * glyph.x_advance.at(glyph.size); offset -= amount; remaining += amount; } @@ -455,10 +478,10 @@ pub fn commit( if let Some(Item::Text(text)) = line.items.last() { if let Some(glyph) = text.glyphs.last() { if text.dir.is_positive() - && TextElem::overhang_in(text.styles) + && text.styles.get(TextElem::overhang) && (line.items.len() > 1 || text.glyphs.len() > 1) { - let amount = overhang(glyph.c) * glyph.x_advance.at(text.size); + let amount = overhang(glyph.c) * glyph.x_advance.at(glyph.size); remaining += amount; } } @@ -519,7 +542,7 @@ pub fn commit( let mut frame = layout_and_modify(*styles, |styles| { layout_box(elem, engine, loc.relayout(), styles, region) })?; - apply_baseline_shift(&mut frame, *styles); + apply_shift(&engine.world, &mut frame, *styles); push(&mut offset, frame, idx); } else { offset += amount; diff --git a/crates/typst-layout/src/inline/linebreak.rs b/crates/typst-layout/src/inline/linebreak.rs index ada048c7d..955360df1 100644 --- a/crates/typst-layout/src/inline/linebreak.rs +++ b/crates/typst-layout/src/inline/linebreak.rs @@ -846,7 +846,9 @@ fn hyphenate_at(p: &Preparation, offset: usize) -> bool { p.config.hyphenate.unwrap_or_else(|| { let (_, item) = p.get(offset); match item.text() { - Some(text) => TextElem::hyphenate_in(text.styles).unwrap_or(p.config.justify), + Some(text) => { + text.styles.get(TextElem::hyphenate).unwrap_or(p.config.justify) + } None => false, } }) @@ -857,7 +859,7 @@ fn lang_at(p: &Preparation, offset: usize) -> Option { let lang = p.config.lang.or_else(|| { let (_, item) = p.get(offset); let styles = item.text()?.styles; - Some(TextElem::lang_in(styles)) + Some(styles.get(TextElem::lang)) })?; let bytes = lang.as_str().as_bytes().try_into().ok()?; @@ -927,9 +929,9 @@ impl Estimates { let byte_len = g.range.len(); let stretch = g.stretchability().0 + g.stretchability().1; let shrink = g.shrinkability().0 + g.shrinkability().1; - widths.push(byte_len, g.x_advance.at(shaped.size)); - stretchability.push(byte_len, stretch.at(shaped.size)); - shrinkability.push(byte_len, shrink.at(shaped.size)); + widths.push(byte_len, g.x_advance.at(g.size)); + stretchability.push(byte_len, stretch.at(g.size)); + shrinkability.push(byte_len, shrink.at(g.size)); justifiables.push(byte_len, g.is_justifiable() as usize); } } else { diff --git a/crates/typst-layout/src/inline/mod.rs b/crates/typst-layout/src/inline/mod.rs index 6cafb9b00..06223cebf 100644 --- a/crates/typst-layout/src/inline/mod.rs +++ b/crates/typst-layout/src/inline/mod.rs @@ -14,7 +14,7 @@ pub use self::shaping::create_shape_plan; use comemo::{Track, Tracked, TrackedMut}; use typst_library::diag::SourceResult; use typst_library::engine::{Engine, Route, Sink, Traced}; -use typst_library::foundations::{Packed, Resolve, Smart, StyleChain}; +use typst_library::foundations::{Packed, Smart, StyleChain}; use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator}; use typst_library::layout::{Abs, AlignElem, Dir, FixedAlignment, Fragment, Size}; use typst_library::model::{ @@ -29,7 +29,7 @@ use typst_utils::{Numeric, SliceExt}; use self::collect::{collect, Item, Segment, SpanMapper}; use self::deco::decorate; use self::finalize::finalize; -use self::line::{apply_baseline_shift, commit, line, Line}; +use self::line::{apply_shift, commit, line, Line}; use self::linebreak::{linebreak, Breakpoint}; use self::prepare::{prepare, Preparation}; use self::shaping::{ @@ -113,10 +113,10 @@ fn layout_par_impl( expand, Some(situation), &ConfigBase { - justify: elem.justify(styles), - linebreaks: elem.linebreaks(styles), - first_line_indent: elem.first_line_indent(styles), - hanging_indent: elem.hanging_indent(styles), + justify: elem.justify.get(styles), + linebreaks: elem.linebreaks.get(styles), + first_line_indent: elem.first_line_indent.get(styles), + hanging_indent: elem.hanging_indent.resolve(styles), }, ) } @@ -139,10 +139,10 @@ pub fn layout_inline<'a>( expand, None, &ConfigBase { - justify: ParElem::justify_in(shared), - linebreaks: ParElem::linebreaks_in(shared), - first_line_indent: ParElem::first_line_indent_in(shared), - hanging_indent: ParElem::hanging_indent_in(shared), + justify: shared.get(ParElem::justify), + linebreaks: shared.get(ParElem::linebreaks), + first_line_indent: shared.get(ParElem::first_line_indent), + hanging_indent: shared.resolve(ParElem::hanging_indent), }, ) } @@ -184,8 +184,8 @@ fn configuration( situation: Option, ) -> Config { let justify = base.justify; - let font_size = TextElem::size_in(shared); - let dir = TextElem::dir_in(shared); + let font_size = shared.resolve(TextElem::size); + let dir = shared.resolve(TextElem::dir); Config { justify, @@ -207,7 +207,7 @@ fn configuration( Some(ParSituation::Other) => all, None => false, } - && AlignElem::alignment_in(shared).resolve(shared).x == dir.start().into() + && shared.resolve(AlignElem::alignment).x == dir.start().into() { amount.at(font_size) } else { @@ -219,26 +219,26 @@ fn configuration( } else { Abs::zero() }, - numbering_marker: ParLine::numbering_in(shared).map(|numbering| { + numbering_marker: shared.get_cloned(ParLine::numbering).map(|numbering| { Packed::new(ParLineMarker::new( numbering, - ParLine::number_align_in(shared), - ParLine::number_margin_in(shared), + shared.get(ParLine::number_align), + shared.get(ParLine::number_margin), // Delay resolving the number clearance until line numbers are // laid out to avoid inconsistent spacing depending on varying // font size. - ParLine::number_clearance_in(shared), + shared.get(ParLine::number_clearance), )) }), - align: AlignElem::alignment_in(shared).fix(dir).x, + align: shared.get(AlignElem::alignment).fix(dir).x, font_size, dir, - hyphenate: shared_get(children, shared, TextElem::hyphenate_in) + hyphenate: shared_get(children, shared, |s| s.get(TextElem::hyphenate)) .map(|uniform| uniform.unwrap_or(justify)), - lang: shared_get(children, shared, TextElem::lang_in), - fallback: TextElem::fallback_in(shared), - cjk_latin_spacing: TextElem::cjk_latin_spacing_in(shared).is_auto(), - costs: TextElem::costs_in(shared), + lang: shared_get(children, shared, |s| s.get(TextElem::lang)), + fallback: shared.get(TextElem::fallback), + cjk_latin_spacing: shared.get(TextElem::cjk_latin_spacing).is_auto(), + costs: shared.get(TextElem::costs), } } @@ -314,7 +314,7 @@ fn shared_get( /// When we support some kind of more general ancestry mechanism, this can /// become more elegant. fn in_list(styles: StyleChain) -> bool { - ListElem::depth_in(styles).0 > 0 - || !EnumElem::parents_in(styles).is_empty() - || TermsElem::within_in(styles) + styles.get(ListElem::depth).0 > 0 + || !styles.get_cloned(EnumElem::parents).is_empty() + || styles.get(TermsElem::within) } diff --git a/crates/typst-layout/src/inline/prepare.rs b/crates/typst-layout/src/inline/prepare.rs index 5d7fcd7cb..ab39bdb14 100644 --- a/crates/typst-layout/src/inline/prepare.rs +++ b/crates/typst-layout/src/inline/prepare.rs @@ -144,7 +144,7 @@ fn add_cjk_latin_spacing(items: &mut [(Range, Item)]) { // The spacing is default to 1/4 em, and can be shrunk to 1/8 em. glyph.x_advance += Em::new(0.25); glyph.adjustability.shrinkability.1 += Em::new(0.125); - text.width += Em::new(0.25).at(text.size); + text.width += Em::new(0.25).at(glyph.size); } // Case 2: Latin followed by a CJ character @@ -152,7 +152,7 @@ fn add_cjk_latin_spacing(items: &mut [(Range, Item)]) { glyph.x_advance += Em::new(0.25); glyph.x_offset += Em::new(0.25); glyph.adjustability.shrinkability.0 += Em::new(0.125); - text.width += Em::new(0.25).at(text.size); + text.width += Em::new(0.25).at(glyph.size); } prev = Some(glyph); diff --git a/crates/typst-layout/src/inline/shaping.rs b/crates/typst-layout/src/inline/shaping.rs index 935a86b38..d1e748da8 100644 --- a/crates/typst-layout/src/inline/shaping.rs +++ b/crates/typst-layout/src/inline/shaping.rs @@ -3,14 +3,15 @@ use std::fmt::{self, Debug, Formatter}; use std::sync::Arc; use az::SaturatingAs; -use rustybuzz::{BufferFlags, ShapePlan, UnicodeBuffer}; +use rustybuzz::{BufferFlags, Feature, ShapePlan, UnicodeBuffer}; +use ttf_parser::gsub::SubstitutionSubtable; use ttf_parser::Tag; use typst_library::engine::Engine; use typst_library::foundations::{Smart, StyleChain}; use typst_library::layout::{Abs, Dir, Em, Frame, FrameItem, Point, Size}; use typst_library::text::{ families, features, is_default_ignorable, language, variant, Font, FontFamily, - FontVariant, Glyph, Lang, Region, TextEdgeBounds, TextElem, TextItem, + FontVariant, Glyph, Lang, Region, ShiftSettings, TextEdgeBounds, TextElem, TextItem, }; use typst_library::World; use typst_utils::SliceExt; @@ -41,8 +42,6 @@ pub struct ShapedText<'a> { pub styles: StyleChain<'a>, /// The font variant. pub variant: FontVariant, - /// The font size. - pub size: Abs, /// The width of the text's bounding box. pub width: Abs, /// The shaped glyphs. @@ -62,6 +61,8 @@ pub struct ShapedGlyph { pub x_offset: Em, /// The vertical offset of the glyph. pub y_offset: Em, + /// The font size for the glyph. + pub size: Abs, /// The adjustability of the glyph. pub adjustability: Adjustability, /// The byte range of this glyph's cluster in the full inline layout. A @@ -222,14 +223,17 @@ impl<'a> ShapedText<'a> { let mut frame = Frame::soft(size); frame.set_baseline(top); - let shift = TextElem::baseline_in(self.styles); - let decos = TextElem::deco_in(self.styles); - let fill = TextElem::fill_in(self.styles); - let stroke = TextElem::stroke_in(self.styles); - let span_offset = TextElem::span_offset_in(self.styles); + let size = self.styles.resolve(TextElem::size); + let shift = self.styles.resolve(TextElem::baseline); + let decos = self.styles.get_cloned(TextElem::deco); + let fill = self.styles.get_ref(TextElem::fill); + let stroke = self.styles.resolve(TextElem::stroke); + let span_offset = self.styles.get(TextElem::span_offset); - for ((font, y_offset), group) in - self.glyphs.as_ref().group_by_key(|g| (g.font.clone(), g.y_offset)) + for ((font, y_offset, glyph_size), group) in self + .glyphs + .as_ref() + .group_by_key(|g| (g.font.clone(), g.y_offset, g.size)) { let mut range = group[0].range.clone(); for glyph in group { @@ -237,7 +241,7 @@ impl<'a> ShapedText<'a> { range.end = range.end.max(glyph.range.end); } - let pos = Point::new(offset, top + shift - y_offset.at(self.size)); + let pos = Point::new(offset, top + shift - y_offset.at(size)); let glyphs: Vec = group .iter() .map(|shaped: &ShapedGlyph| { @@ -257,11 +261,11 @@ impl<'a> ShapedText<'a> { adjustability_right * justification_ratio; if shaped.is_justifiable() { justification_right += - Em::from_length(extra_justification, self.size) + Em::from_abs(extra_justification, glyph_size) } - frame.size_mut().x += justification_left.at(self.size) - + justification_right.at(self.size); + frame.size_mut().x += justification_left.at(glyph_size) + + justification_right.at(glyph_size); // We may not be able to reach the offset completely if // it exceeds u16, but better to have a roughly correct @@ -304,7 +308,7 @@ impl<'a> ShapedText<'a> { let item = TextItem { font, - size: self.size, + size: glyph_size, lang: self.lang, region: self.region, fill: fill.clone(), @@ -336,12 +340,13 @@ impl<'a> ShapedText<'a> { let mut top = Abs::zero(); let mut bottom = Abs::zero(); - let top_edge = TextElem::top_edge_in(self.styles); - let bottom_edge = TextElem::bottom_edge_in(self.styles); + let size = self.styles.resolve(TextElem::size); + let top_edge = self.styles.get(TextElem::top_edge); + let bottom_edge = self.styles.get(TextElem::bottom_edge); // Expand top and bottom by reading the font's vertical metrics. let mut expand = |font: &Font, bounds: TextEdgeBounds| { - let (t, b) = font.edges(top_edge, bottom_edge, self.size, bounds); + let (t, b) = font.edges(top_edge, bottom_edge, size, bounds); top.set_max(t); bottom.set_max(b); }; @@ -388,18 +393,16 @@ impl<'a> ShapedText<'a> { pub fn stretchability(&self) -> Abs { self.glyphs .iter() - .map(|g| g.stretchability().0 + g.stretchability().1) - .sum::() - .at(self.size) + .map(|g| (g.stretchability().0 + g.stretchability().1).at(g.size)) + .sum() } /// The shrinkability of the text pub fn shrinkability(&self) -> Abs { self.glyphs .iter() - .map(|g| g.shrinkability().0 + g.shrinkability().1) - .sum::() - .at(self.size) + .map(|g| (g.shrinkability().0 + g.shrinkability().1).at(g.size)) + .sum() } /// Reshape a range of the shaped text, reusing information from this @@ -418,9 +421,8 @@ impl<'a> ShapedText<'a> { lang: self.lang, region: self.region, styles: self.styles, - size: self.size, variant: self.variant, - width: glyphs.iter().map(|g| g.x_advance).sum::().at(self.size), + width: glyphs_width(glyphs), glyphs: Cow::Borrowed(glyphs), } } else { @@ -484,13 +486,15 @@ impl<'a> ShapedText<'a> { // that subtracting either of the endpoints by self.base doesn't // underflow. See . .unwrap_or_else(|| self.base..self.base); - self.width += x_advance.at(self.size); + let size = self.styles.resolve(TextElem::size); + self.width += x_advance.at(size); let glyph = ShapedGlyph { font, glyph_id: glyph_id.0, x_advance, x_offset: Em::zero(), y_offset: Em::zero(), + size, adjustability: Adjustability::default(), range, safe_to_break: true, @@ -599,9 +603,9 @@ pub fn shape_range<'a>( range: Range, styles: StyleChain<'a>, ) { - let script = TextElem::script_in(styles); - let lang = TextElem::lang_in(styles); - let region = TextElem::region_in(styles); + let script = styles.get(TextElem::script); + let lang = styles.get(TextElem::lang); + let region = styles.get(TextElem::region); let mut process = |range: Range, level: BidiLevel| { let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL }; let shaped = @@ -665,7 +669,8 @@ fn shape<'a>( lang: Lang, region: Option, ) -> ShapedText<'a> { - let size = TextElem::size_in(styles); + let size = styles.resolve(TextElem::size); + let shift_settings = styles.get(TextElem::shift_settings); let mut ctx = ShapingContext { engine, size, @@ -674,8 +679,9 @@ fn shape<'a>( styles, variant: variant(styles), features: features(styles), - fallback: TextElem::fallback_in(styles), + fallback: styles.get(TextElem::fallback), dir, + shift_settings, }; if !text.is_empty() { @@ -698,12 +704,17 @@ fn shape<'a>( region, styles, variant: ctx.variant, - size, - width: ctx.glyphs.iter().map(|g| g.x_advance).sum::().at(size), + width: glyphs_width(&ctx.glyphs), glyphs: Cow::Owned(ctx.glyphs), } } +/// Computes the width of a run of glyphs relative to the font size, accounting +/// for their individual scaling factors and other font metrics. +fn glyphs_width(glyphs: &[ShapedGlyph]) -> Abs { + glyphs.iter().map(|g| g.x_advance.at(g.size)).sum() +} + /// Holds shaping results and metadata common to all shaped segments. struct ShapingContext<'a, 'v> { engine: &'a Engine<'v>, @@ -715,6 +726,7 @@ struct ShapingContext<'a, 'v> { features: Vec, fallback: bool, dir: Dir, + shift_settings: Option, } /// Shape text with font fallback using the `families` iterator. @@ -771,7 +783,7 @@ fn shape_segment<'a>( let mut buffer = UnicodeBuffer::new(); buffer.push_str(text); buffer.set_language(language(ctx.styles)); - if let Some(script) = TextElem::script_in(ctx.styles).custom().and_then(|script| { + if let Some(script) = ctx.styles.get(TextElem::script).custom().and_then(|script| { rustybuzz::Script::from_iso15924_tag(Tag::from_bytes(script.as_bytes())) }) { buffer.set_script(script) @@ -789,6 +801,18 @@ fn shape_segment<'a>( // text extraction. buffer.set_flags(BufferFlags::REMOVE_DEFAULT_IGNORABLES); + let (script_shift, script_compensation, scale, shift_feature) = ctx + .shift_settings + .map_or((Em::zero(), Em::zero(), Em::one(), None), |settings| { + determine_shift(text, &font, settings) + }); + + let has_shift_feature = shift_feature.is_some(); + if let Some(feat) = shift_feature { + // Temporarily push the feature. + ctx.features.push(feat) + } + // Prepare the shape plan. This plan depends on direction, script, language, // and features, but is independent from the text and can thus be memoized. let plan = create_shape_plan( @@ -799,6 +823,10 @@ fn shape_segment<'a>( &ctx.features, ); + if has_shift_feature { + ctx.features.pop(); + } + // Shape! let buffer = rustybuzz::shape_with_plan(font.rusty(), &plan, buffer); let infos = buffer.glyph_infos(); @@ -869,8 +897,9 @@ fn shape_segment<'a>( glyph_id: info.glyph_id as u16, // TODO: Don't ignore y_advance. x_advance, - x_offset: font.to_em(pos[i].x_offset), - y_offset: font.to_em(pos[i].y_offset), + x_offset: font.to_em(pos[i].x_offset) + script_compensation, + y_offset: font.to_em(pos[i].y_offset) + script_shift, + size: scale.at(ctx.size), adjustability: Adjustability::default(), range: start..end, safe_to_break: !info.unsafe_to_break(), @@ -932,6 +961,64 @@ fn shape_segment<'a>( ctx.used.pop(); } +/// Returns a `(script_shift, script_compensation, scale, feature)` quadruplet +/// describing how to produce scripts. +/// +/// Those values determine how the rendered text should be transformed to +/// display sub-/super-scripts. If the OpenType feature can be used, the +/// rendered text should not be transformed in any way, and so those values are +/// neutral (`(0, 0, 1, None)`). If scripts should be synthesized, those values +/// determine how to transform the rendered text to display scripts as expected. +fn determine_shift( + text: &str, + font: &Font, + settings: ShiftSettings, +) -> (Em, Em, Em, Option) { + settings + .typographic + .then(|| { + // If typographic scripts are enabled (i.e., we want to use the + // OpenType feature instead of synthesizing if possible), we add + // "subs"/"sups" to the feature list if supported by the font. + // In case of a problem, we just early exit + let gsub = font.rusty().tables().gsub?; + let subtable_index = + gsub.features.find(settings.kind.feature())?.lookup_indices.get(0)?; + let coverage = gsub + .lookups + .get(subtable_index)? + .subtables + .get::(0)? + .coverage(); + text.chars() + .all(|c| { + font.rusty().glyph_index(c).is_some_and(|i| coverage.contains(i)) + }) + .then(|| { + // If we can use the OpenType feature, we can keep the text + // as is. + ( + Em::zero(), + Em::zero(), + Em::one(), + Some(Feature::new(settings.kind.feature(), 1, ..)), + ) + }) + }) + // Reunite the cases where `typographic` is `false` or where using the + // OpenType feature would not work. + .flatten() + .unwrap_or_else(|| { + let script_metrics = settings.kind.read_metrics(font.metrics()); + ( + settings.shift.unwrap_or(script_metrics.vertical_offset), + script_metrics.horizontal_offset, + settings.size.unwrap_or(script_metrics.height), + None, + ) + }) +} + /// Create a shape plan. #[comemo::memoize] pub fn create_shape_plan( @@ -963,6 +1050,7 @@ fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) { x_advance, x_offset: Em::zero(), y_offset: Em::zero(), + size: ctx.size, adjustability: Adjustability::default(), range: start..end, safe_to_break: true, @@ -985,9 +1073,11 @@ fn shape_tofus(ctx: &mut ShapingContext, base: usize, text: &str, font: Font) { /// Apply tracking and spacing to the shaped glyphs. fn track_and_space(ctx: &mut ShapingContext) { - let tracking = Em::from_length(TextElem::tracking_in(ctx.styles), ctx.size); - let spacing = - TextElem::spacing_in(ctx.styles).map(|abs| Em::from_length(abs, ctx.size)); + let tracking = Em::from_abs(ctx.styles.resolve(TextElem::tracking), ctx.size); + let spacing = ctx + .styles + .resolve(TextElem::spacing) + .map(|abs| Em::from_abs(abs, ctx.size)); let mut glyphs = ctx.glyphs.iter_mut().peekable(); while let Some(glyph) = glyphs.next() { diff --git a/crates/typst-layout/src/lib.rs b/crates/typst-layout/src/lib.rs index 443e90d61..361bab463 100644 --- a/crates/typst-layout/src/lib.rs +++ b/crates/typst-layout/src/lib.rs @@ -10,21 +10,11 @@ mod modifiers; mod pad; mod pages; mod repeat; +mod rules; mod shapes; mod stack; mod transforms; -pub use self::flow::{layout_columns, layout_fragment, layout_frame}; -pub use self::grid::{layout_grid, layout_table}; -pub use self::image::layout_image; -pub use self::lists::{layout_enum, layout_list}; -pub use self::math::{layout_equation_block, layout_equation_inline}; -pub use self::pad::layout_pad; +pub use self::flow::{layout_fragment, layout_frame}; pub use self::pages::layout_document; -pub use self::repeat::layout_repeat; -pub use self::shapes::{ - layout_circle, layout_curve, layout_ellipse, layout_line, layout_path, - layout_polygon, layout_rect, layout_square, -}; -pub use self::stack::layout_stack; -pub use self::transforms::{layout_move, layout_rotate, layout_scale, layout_skew}; +pub use self::rules::register; diff --git a/crates/typst-layout/src/lists.rs b/crates/typst-layout/src/lists.rs index 974788a70..adb793fb9 100644 --- a/crates/typst-layout/src/lists.rs +++ b/crates/typst-layout/src/lists.rs @@ -20,20 +20,21 @@ pub fn layout_list( styles: StyleChain, regions: Regions, ) -> SourceResult { - let indent = elem.indent(styles); - let body_indent = elem.body_indent(styles); - let tight = elem.tight(styles); - let gutter = elem.spacing(styles).unwrap_or_else(|| { + let indent = elem.indent.get(styles); + let body_indent = elem.body_indent.get(styles); + let tight = elem.tight.get(styles); + let gutter = elem.spacing.get(styles).unwrap_or_else(|| { if tight { - ParElem::leading_in(styles).into() + styles.get(ParElem::leading) } else { - ParElem::spacing_in(styles).into() + styles.get(ParElem::spacing) } }); - let Depth(depth) = ListElem::depth_in(styles); + let Depth(depth) = styles.get(ListElem::depth); let marker = elem - .marker(styles) + .marker + .get_ref(styles) .resolve(engine, styles, depth)? // avoid '#set align' interference with the list .aligned(HAlignment::Start + VAlignment::Top); @@ -52,7 +53,7 @@ pub fn layout_list( cells.push(Cell::new(marker.clone(), locator.next(&marker.span()))); cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new( - body.styled(ListElem::set_depth(Depth(1))), + body.set(ListElem::depth, Depth(1)), locator.next(&item.body.span()), )); } @@ -81,40 +82,40 @@ pub fn layout_enum( styles: StyleChain, regions: Regions, ) -> SourceResult { - let numbering = elem.numbering(styles); - let reversed = elem.reversed(styles); - let indent = elem.indent(styles); - let body_indent = elem.body_indent(styles); - let tight = elem.tight(styles); - let gutter = elem.spacing(styles).unwrap_or_else(|| { + let numbering = elem.numbering.get_ref(styles); + let reversed = elem.reversed.get(styles); + let indent = elem.indent.get(styles); + let body_indent = elem.body_indent.get(styles); + let tight = elem.tight.get(styles); + let gutter = elem.spacing.get(styles).unwrap_or_else(|| { if tight { - ParElem::leading_in(styles).into() + styles.get(ParElem::leading) } else { - ParElem::spacing_in(styles).into() + styles.get(ParElem::spacing) } }); let mut cells = vec![]; let mut locator = locator.split(); - let mut number = elem.start(styles).unwrap_or_else(|| { + let mut number = elem.start.get(styles).unwrap_or_else(|| { if reversed { elem.children.len() as u64 } else { 1 } }); - let mut parents = EnumElem::parents_in(styles); + let mut parents = styles.get_cloned(EnumElem::parents); - let full = elem.full(styles); + let full = elem.full.get(styles); // Horizontally align based on the given respective parameter. // Vertically align to the top to avoid inheriting `horizon` or `bottom` // alignment from the context and having the number be displaced in // relation to the item it refers to. - let number_align = elem.number_align(styles); + let number_align = elem.number_align.get(styles); for item in &elem.children { - number = item.number(styles).unwrap_or(number); + number = item.number.get(styles).unwrap_or(number); let context = Context::new(None, Some(styles)); let resolved = if full { @@ -133,8 +134,7 @@ pub fn layout_enum( // Disable overhang as a workaround to end-aligned dots glitching // and decreasing spacing between numbers and items. - let resolved = - resolved.aligned(number_align).styled(TextElem::set_overhang(false)); + let resolved = resolved.aligned(number_align).set(TextElem::overhang, false); // Text in wide enums shall always turn into paragraphs. let mut body = item.body.clone(); @@ -146,7 +146,7 @@ pub fn layout_enum( cells.push(Cell::new(resolved, locator.next(&()))); cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new( - body.styled(EnumElem::set_parents(smallvec![number])), + body.set(EnumElem::parents, smallvec![number]), locator.next(&item.body.span()), )); number = diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs index 159703b8e..e7f051ace 100644 --- a/crates/typst-layout/src/math/accent.rs +++ b/crates/typst-layout/src/math/accent.rs @@ -24,7 +24,7 @@ pub fn layout_accent( // Try to replace the base glyph with its dotless variant. let dtls = style_dtls(); let base_styles = - if top_accent && elem.dotless(styles) { styles.chain(&dtls) } else { styles }; + if top_accent && elem.dotless.get(styles) { styles.chain(&dtls) } else { styles }; let cramped = style_cramped(); let base = ctx.layout_into_fragment(&elem.base, base_styles.chain(&cramped))?; @@ -47,7 +47,7 @@ pub fn layout_accent( // Forcing the accent to be at least as large as the base makes it too wide // in many cases. - let width = elem.size(styles).relative_to(base.width()); + let width = elem.size.resolve(styles).relative_to(base.width()); let short_fall = ACCENT_SHORT_FALL.at(glyph.item.size); glyph.stretch_horizontal(ctx, width - short_fall); let accent_attach = glyph.accent_attach.0; diff --git a/crates/typst-layout/src/math/attach.rs b/crates/typst-layout/src/math/attach.rs index a7f3cad5f..78b6f5515 100644 --- a/crates/typst-layout/src/math/attach.rs +++ b/crates/typst-layout/src/math/attach.rs @@ -31,16 +31,16 @@ pub fn layout_attach( let mut base = ctx.layout_into_fragment(&elem.base, styles)?; let sup_style = style_for_superscript(styles); let sup_style_chain = styles.chain(&sup_style); - let tl = elem.tl(sup_style_chain); - let tr = elem.tr(sup_style_chain); + let tl = elem.tl.get_cloned(sup_style_chain); + let tr = elem.tr.get_cloned(sup_style_chain); let primed = tr.as_ref().is_some_and(|content| content.is::()); - let t = elem.t(sup_style_chain); + let t = elem.t.get_cloned(sup_style_chain); let sub_style = style_for_subscript(styles); let sub_style_chain = styles.chain(&sub_style); - let bl = elem.bl(sub_style_chain); - let br = elem.br(sub_style_chain); - let b = elem.b(sub_style_chain); + let bl = elem.bl.get_cloned(sub_style_chain); + let br = elem.br.get_cloned(sub_style_chain); + let b = elem.b.get_cloned(sub_style_chain); let limits = base.limits().active(styles); let (t, tr) = match (t, tr) { @@ -146,7 +146,7 @@ pub fn layout_limits( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let limits = if elem.inline(styles) { Limits::Always } else { Limits::Display }; + let limits = if elem.inline.get(styles) { Limits::Always } else { Limits::Display }; let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?; fragment.set_limits(limits); ctx.push(fragment); @@ -161,7 +161,8 @@ fn stretch_size(styles: StyleChain, elem: &Packed) -> Option().map(|stretch| stretch.size(styles)) + base.to_packed::() + .map(|stretch| stretch.size.resolve(styles)) } /// Lay out the attachments. @@ -397,7 +398,7 @@ fn compute_script_shifts( base: &MathFragment, [tl, tr, bl, br]: [&Option; 4], ) -> (Abs, Abs) { - let sup_shift_up = if EquationElem::cramped_in(styles) { + let sup_shift_up = if styles.get(EquationElem::cramped) { scaled!(ctx, styles, superscript_shift_up_cramped) } else { scaled!(ctx, styles, superscript_shift_up) diff --git a/crates/typst-layout/src/math/cancel.rs b/crates/typst-layout/src/math/cancel.rs index 9826397fa..57a32ca2a 100644 --- a/crates/typst-layout/src/math/cancel.rs +++ b/crates/typst-layout/src/math/cancel.rs @@ -27,16 +27,16 @@ pub fn layout_cancel( let mut body = body.into_frame(); let body_size = body.size(); let span = elem.span(); - let length = elem.length(styles); + let length = elem.length.resolve(styles); - let stroke = elem.stroke(styles).unwrap_or(FixedStroke { - paint: TextElem::fill_in(styles).as_decoration(), + let stroke = elem.stroke.resolve(styles).unwrap_or(FixedStroke { + paint: styles.get_ref(TextElem::fill).as_decoration(), ..Default::default() }); - let invert = elem.inverted(styles); - let cross = elem.cross(styles); - let angle = elem.angle(styles); + let invert = elem.inverted.get(styles); + let cross = elem.cross.get(styles); + let angle = elem.angle.get_ref(styles); let invert_first_line = !cross && invert; let first_line = draw_cancel_line( @@ -44,7 +44,7 @@ pub fn layout_cancel( length, stroke.clone(), invert_first_line, - &angle, + angle, body_size, styles, span, @@ -57,7 +57,7 @@ pub fn layout_cancel( if cross { // Draw the second line. let second_line = - draw_cancel_line(ctx, length, stroke, true, &angle, body_size, styles, span)?; + draw_cancel_line(ctx, length, stroke, true, angle, body_size, styles, span)?; body.push_frame(center, second_line); } diff --git a/crates/typst-layout/src/math/frac.rs b/crates/typst-layout/src/math/frac.rs index 091f328f6..12a2c6fd1 100644 --- a/crates/typst-layout/src/math/frac.rs +++ b/crates/typst-layout/src/math/frac.rs @@ -124,7 +124,7 @@ fn layout_frac_like( FrameItem::Shape( Geometry::Line(Point::with_x(line_width)).stroked( FixedStroke::from_pair( - TextElem::fill_in(styles).as_decoration(), + styles.get_ref(TextElem::fill).as_decoration(), thickness, ), ), diff --git a/crates/typst-layout/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs index eb85eeb5d..758dd401f 100644 --- a/crates/typst-layout/src/math/fragment.rs +++ b/crates/typst-layout/src/math/fragment.rs @@ -215,7 +215,7 @@ impl MathFragment { &glyph.item.font, GlyphId(glyph.item.glyphs[glyph_index].id), corner, - Em::from_length(height, glyph.item.size), + Em::from_abs(height, glyph.item.size), ) .unwrap_or_default() .at(glyph.item.size) @@ -315,7 +315,8 @@ impl GlyphFragment { let cluster = info.cluster as usize; let c = text[cluster..].chars().next().unwrap(); let limits = Limits::for_char(c); - let class = EquationElem::class_in(styles) + let class = styles + .get(EquationElem::class) .or_else(|| default_math_class(c)) .unwrap_or(MathClass::Normal); @@ -331,11 +332,11 @@ impl GlyphFragment { let item = TextItem { font: font.clone(), - size: TextElem::size_in(styles), - fill: TextElem::fill_in(styles).as_decoration(), - stroke: TextElem::stroke_in(styles).map(|s| s.unwrap_or_default()), - lang: TextElem::lang_in(styles), - region: TextElem::region_in(styles), + size: styles.resolve(TextElem::size), + fill: styles.get_ref(TextElem::fill).as_decoration(), + stroke: styles.resolve(TextElem::stroke).map(|s| s.unwrap_or_default()), + lang: styles.get(TextElem::lang), + region: styles.get(TextElem::region), text: text.into(), glyphs: vec![glyph.clone()], }; @@ -344,7 +345,7 @@ impl GlyphFragment { item, base_glyph: glyph, // Math - math_size: EquationElem::size_in(styles), + math_size: styles.get(EquationElem::size), class, limits, mid_stretched: None, @@ -356,7 +357,7 @@ impl GlyphFragment { baseline: None, // Misc align: Abs::zero(), - shift: TextElem::baseline_in(styles), + shift: styles.resolve(TextElem::baseline), modifiers: FrameModifiers::get_in(styles), }; fragment.update_glyph(); @@ -541,9 +542,9 @@ impl FrameFragment { let accent_attach = frame.width() / 2.0; Self { frame: frame.modified(&FrameModifiers::get_in(styles)), - font_size: TextElem::size_in(styles), - class: EquationElem::class_in(styles).unwrap_or(MathClass::Normal), - math_size: EquationElem::size_in(styles), + font_size: styles.resolve(TextElem::size), + class: styles.get(EquationElem::class).unwrap_or(MathClass::Normal), + math_size: styles.get(EquationElem::size), limits: Limits::Never, spaced: false, base_ascent, @@ -767,8 +768,8 @@ fn assemble( advance += ratio * (max_overlap - min_overlap); } let (x, y) = match axis { - Axis::X => (Em::from_length(advance, base.item.size), Em::zero()), - Axis::Y => (Em::zero(), Em::from_length(advance, base.item.size)), + Axis::X => (Em::from_abs(advance, base.item.size), Em::zero()), + Axis::Y => (Em::zero(), Em::from_abs(advance, base.item.size)), }; glyphs.push(Glyph { id: part.glyph_id.0, @@ -864,7 +865,7 @@ impl Limits { pub fn active(&self, styles: StyleChain) -> bool { match self { Self::Always => true, - Self::Display => EquationElem::size_in(styles) == MathSize::Display, + Self::Display => styles.get(EquationElem::size) == MathSize::Display, Self::Never => false, } } diff --git a/crates/typst-layout/src/math/lr.rs b/crates/typst-layout/src/math/lr.rs index a3b5cb05c..2348025e8 100644 --- a/crates/typst-layout/src/math/lr.rs +++ b/crates/typst-layout/src/math/lr.rs @@ -22,7 +22,7 @@ pub fn layout_lr( // Extract implicit LrElem. if let Some(lr) = body.to_packed::() { - if lr.size(styles).is_one() { + if lr.size.get(styles).is_one() { body = &lr.body; } } @@ -41,7 +41,7 @@ pub fn layout_lr( .unwrap_or_default(); let relative_to = 2.0 * max_extent; - let height = elem.size(styles); + let height = elem.size.resolve(styles); // Scale up fragments at both ends. match inner_fragments { diff --git a/crates/typst-layout/src/math/mat.rs b/crates/typst-layout/src/math/mat.rs index 278b1343e..4a897a03e 100644 --- a/crates/typst-layout/src/math/mat.rs +++ b/crates/typst-layout/src/math/mat.rs @@ -30,15 +30,15 @@ pub fn layout_vec( ctx, styles, &[column], - elem.align(styles), + elem.align.resolve(styles), LeftRightAlternator::Right, None, - Axes::with_y(elem.gap(styles)), + Axes::with_y(elem.gap.resolve(styles)), span, "elements", )?; - let delim = elem.delim(styles); + let delim = elem.delim.get(styles); layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), span) } @@ -59,14 +59,17 @@ pub fn layout_cases( FixedAlignment::Start, LeftRightAlternator::None, None, - Axes::with_y(elem.gap(styles)), + Axes::with_y(elem.gap.resolve(styles)), span, "branches", )?; - let delim = elem.delim(styles); - let (open, close) = - if elem.reverse(styles) { (None, delim.close()) } else { (delim.open(), None) }; + let delim = elem.delim.get(styles); + let (open, close) = if elem.reverse.get(styles) { + (None, delim.close()) + } else { + (delim.open(), None) + }; layout_delimiters(ctx, styles, frame, open, close, span) } @@ -81,7 +84,7 @@ pub fn layout_mat( let rows = &elem.rows; let ncols = rows.first().map_or(0, |row| row.len()); - let augment = elem.augment(styles); + let augment = elem.augment.resolve(styles); if let Some(aug) = &augment { for &offset in &aug.hline.0 { if offset == 0 || offset.unsigned_abs() >= rows.len() { @@ -116,15 +119,15 @@ pub fn layout_mat( ctx, styles, &columns, - elem.align(styles), + elem.align.resolve(styles), LeftRightAlternator::Right, augment, - Axes::new(elem.column_gap(styles), elem.row_gap(styles)), + Axes::new(elem.column_gap.resolve(styles), elem.row_gap.resolve(styles)), span, "cells", )?; - let delim = elem.delim(styles); + let delim = elem.delim.get(styles); layout_delimiters(ctx, styles, frame, delim.open(), delim.close(), span) } @@ -157,7 +160,7 @@ fn layout_body( let default_stroke_thickness = DEFAULT_STROKE_THICKNESS.resolve(styles); let default_stroke = FixedStroke { thickness: default_stroke_thickness, - paint: TextElem::fill_in(styles).as_decoration(), + paint: styles.get_ref(TextElem::fill).as_decoration(), cap: LineCap::Square, ..Default::default() }; diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs index 5fd22e578..390835067 100644 --- a/crates/typst-layout/src/math/mod.rs +++ b/crates/typst-layout/src/math/mod.rs @@ -51,7 +51,7 @@ pub fn layout_equation_inline( styles: StyleChain, region: Size, ) -> SourceResult> { - assert!(!elem.block(styles)); + assert!(!elem.block.get(styles)); let font = find_math_font(engine, styles, elem.span())?; @@ -78,12 +78,12 @@ pub fn layout_equation_inline( for item in &mut items { let InlineItem::Frame(frame) = item else { continue }; - let slack = ParElem::leading_in(styles) * 0.7; + let slack = styles.resolve(ParElem::leading) * 0.7; let (t, b) = font.edges( - TextElem::top_edge_in(styles), - TextElem::bottom_edge_in(styles), - TextElem::size_in(styles), + styles.get(TextElem::top_edge), + styles.get(TextElem::bottom_edge), + styles.resolve(TextElem::size), TextEdgeBounds::Frame(frame), ); @@ -105,7 +105,7 @@ pub fn layout_equation_block( styles: StyleChain, regions: Regions, ) -> SourceResult { - assert!(elem.block(styles)); + assert!(elem.block.get(styles)); let span = elem.span(); let font = find_math_font(engine, styles, span)?; @@ -121,7 +121,7 @@ pub fn layout_equation_block( .multiline_frame_builder(styles); let width = full_equation_builder.size.x; - let equation_builders = if BlockElem::breakable_in(styles) { + let equation_builders = if styles.get(BlockElem::breakable) { let mut rows = full_equation_builder.frames.into_iter().peekable(); let mut equation_builders = vec![]; let mut last_first_pos = Point::zero(); @@ -188,7 +188,7 @@ pub fn layout_equation_block( vec![full_equation_builder] }; - let Some(numbering) = (**elem).numbering(styles) else { + let Some(numbering) = elem.numbering.get_ref(styles) else { let frames = equation_builders .into_iter() .map(MathRunFrameBuilder::build) @@ -197,7 +197,7 @@ pub fn layout_equation_block( }; let pod = Region::new(regions.base(), Axes::splat(false)); - let counter = Counter::of(EquationElem::elem()) + let counter = Counter::of(EquationElem::ELEM) .display_at_loc(engine, elem.location().unwrap(), styles, numbering)? .spanned(span); let number = crate::layout_frame(engine, &counter, locator.next(&()), styles, pod)?; @@ -205,7 +205,7 @@ pub fn layout_equation_block( static NUMBER_GUTTER: Em = Em::new(0.5); let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles); - let number_align = match elem.number_align(styles) { + let number_align = match elem.number_align.get(styles) { SpecificAlignment::H(h) => SpecificAlignment::Both(h, VAlignment::Horizon), SpecificAlignment::V(v) => SpecificAlignment::Both(OuterHAlignment::End, v), SpecificAlignment::Both(h, v) => SpecificAlignment::Both(h, v), @@ -224,7 +224,7 @@ pub fn layout_equation_block( builder, number.clone(), number_align.resolve(styles), - AlignElem::alignment_in(styles).resolve(styles).x, + styles.get(AlignElem::alignment).resolve(styles).x, regions.size.x, full_number_width, ) @@ -472,7 +472,9 @@ impl<'a, 'v, 'e> MathContext<'a, 'v, 'e> { let outer = styles; for (elem, styles) in pairs { // Hack because the font is fixed in math. - if styles != outer && TextElem::font_in(styles) != TextElem::font_in(outer) { + if styles != outer + && styles.get_ref(TextElem::font) != outer.get_ref(TextElem::font) + { let frame = layout_external(elem, self, styles)?; self.push(FrameFragment::new(styles, frame).with_spaced(true)); continue; @@ -603,7 +605,10 @@ fn layout_h( ) -> SourceResult<()> { if let Spacing::Rel(rel) = elem.amount { if rel.rel.is_zero() { - ctx.push(MathFragment::Spacing(rel.abs.resolve(styles), elem.weak(styles))); + ctx.push(MathFragment::Spacing( + rel.abs.resolve(styles), + elem.weak.get(styles), + )); } } Ok(()) @@ -616,7 +621,7 @@ fn layout_class( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let style = EquationElem::set_class(Some(elem.class)).wrap(); + let style = EquationElem::class.set(Some(elem.class)).wrap(); let mut fragment = ctx.layout_into_fragment(&elem.body, styles.chain(&style))?; fragment.set_class(elem.class); fragment.set_limits(Limits::for_class(elem.class)); @@ -642,7 +647,7 @@ fn layout_op( .with_italics_correction(italics) .with_accent_attach(accent_attach) .with_text_like(text_like) - .with_limits(if elem.limits(styles) { + .with_limits(if elem.limits.get(styles) { Limits::Display } else { Limits::Never diff --git a/crates/typst-layout/src/math/root.rs b/crates/typst-layout/src/math/root.rs index 91b9b16af..30948e08e 100644 --- a/crates/typst-layout/src/math/root.rs +++ b/crates/typst-layout/src/math/root.rs @@ -17,7 +17,7 @@ pub fn layout_root( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let index = elem.index(styles); + let index = elem.index.get_ref(styles); let span = elem.span(); let gap = scaled!( @@ -54,7 +54,7 @@ pub fn layout_root( let sqrt = sqrt.into_frame(); // Layout the index. - let sscript = EquationElem::set_size(MathSize::ScriptScript).wrap(); + let sscript = EquationElem::size.set(MathSize::ScriptScript).wrap(); let index = index .as_ref() .map(|elem| ctx.layout_into_frame(elem, styles.chain(&sscript))) @@ -112,7 +112,7 @@ pub fn layout_root( FrameItem::Shape( Geometry::Line(Point::with_x(radicand.width())).stroked( FixedStroke::from_pair( - TextElem::fill_in(styles).as_decoration(), + styles.get_ref(TextElem::fill).as_decoration(), thickness, ), ), diff --git a/crates/typst-layout/src/math/run.rs b/crates/typst-layout/src/math/run.rs index 4ec76c253..161fa1062 100644 --- a/crates/typst-layout/src/math/run.rs +++ b/crates/typst-layout/src/math/run.rs @@ -194,13 +194,13 @@ impl MathRun { let row_count = rows.len(); let alignments = alignments(&rows); - let leading = if EquationElem::size_in(styles) >= MathSize::Text { - ParElem::leading_in(styles) + let leading = if styles.get(EquationElem::size) >= MathSize::Text { + styles.resolve(ParElem::leading) } else { TIGHT_LEADING.resolve(styles) }; - let align = AlignElem::alignment_in(styles).resolve(styles).x; + let align = styles.resolve(AlignElem::alignment).x; let mut frames: Vec<(Frame, Point)> = vec![]; let mut size = Size::zero(); for (i, row) in rows.into_iter().enumerate() { diff --git a/crates/typst-layout/src/math/shared.rs b/crates/typst-layout/src/math/shared.rs index 1f88d2dd7..c9d20aa68 100644 --- a/crates/typst-layout/src/math/shared.rs +++ b/crates/typst-layout/src/math/shared.rs @@ -10,7 +10,7 @@ use super::{LeftRightAlternator, MathContext, MathFragment, MathRun}; macro_rules! scaled { ($ctx:expr, $styles:expr, text: $text:ident, display: $display:ident $(,)?) => { - match typst_library::math::EquationElem::size_in($styles) { + match $styles.get(typst_library::math::EquationElem::size) { typst_library::math::MathSize::Display => scaled!($ctx, $styles, $display), _ => scaled!($ctx, $styles, $text), } @@ -19,7 +19,7 @@ macro_rules! scaled { $crate::math::Scaled::scaled( $ctx.constants.$name(), $ctx, - typst_library::text::TextElem::size_in($styles), + $styles.resolve(typst_library::text::TextElem::size), ) }; } @@ -58,55 +58,62 @@ impl Scaled for MathValue<'_> { /// Styles something as cramped. pub fn style_cramped() -> LazyHash + -
-
-

Output

- Placeholder -
- -
-

Reference

- Placeholder -
-
- ${stdoutHtml} - ${stderrHtml} + ${showRender ? renderSection(panel, name) : ""} + ${showHtml ? await htmlSection(name) : ""} + ${stdout} + ${stderr} `; } + +function renderSection(panel: vscode.WebviewPanel, name: string) { + const outputUri = getUri(name, "store", "render"); + const refUri = getUri(name, "ref", "render"); + return `
+
+ ${linkedTitle("Output", outputUri)} + Placeholder +
+ +
+ ${linkedTitle("Reference", refUri)} + Placeholder +
+
`; +} + +async function htmlSection(name: string) { + const storeHtml = await htmlSnippet( + "HTML Output", + getUri(name, "store", "html") + ); + const refHtml = await htmlSnippet( + "HTML Reference", + getUri(name, "ref", "html") + ); + return `
+ ${storeHtml} + ${refHtml} +
`; +} + +async function htmlSnippet(title: string, uri: vscode.Uri): Promise { + try { + const data = await vscode.workspace.fs.readFile(uri); + const code = new TextDecoder("utf-8").decode(data); + return `
+ ${linkedTitle(title, uri)} +
+ ${await highlight(code)} + +
+
`; + } catch { + return `

${title}

Not present
`; + } +} + +function linkedTitle(title: string, uri: vscode.Uri) { + return `

${title}

`; +} + +async function highlight(code: string): Promise { + return (await shiki).codeToHtml(code, { + lang: "html", + theme: selectTheme(), + }); +} + +function selectTheme() { + switch (vscode.window.activeColorTheme.kind) { + case vscode.ColorThemeKind.Light: + case vscode.ColorThemeKind.HighContrastLight: + return "github-light"; + case vscode.ColorThemeKind.Dark: + case vscode.ColorThemeKind.HighContrast: + return "github-dark"; + } +} + +function escape(text: string) { + return text + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} diff --git a/tools/test-helper/tsconfig.json b/tools/test-helper/tsconfig.json index 45e374553..952c40426 100644 --- a/tools/test-helper/tsconfig.json +++ b/tools/test-helper/tsconfig.json @@ -1,9 +1,10 @@ { "compilerOptions": { - "module": "Node16", + "module": "nodenext", + "lib": ["ES2022", "DOM"], "target": "ES2022", + "moduleResolution": "nodenext", "outDir": "dist", - "lib": ["ES2022"], "sourceMap": true, "rootDir": "src", "strict": true