diff --git a/Cargo.lock b/Cargo.lock index 550c4141a..5526da48c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", 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..2b659188a --- /dev/null +++ b/crates/typst-html/src/css.rs @@ -0,0 +1,135 @@ +//! Conversion from Typst data types into CSS data types. + +use std::fmt::{self, Display}; + +use typst_library::layout::Length; +use typst_library::visualize::{Color, Hsl, LinearRgb, Oklab, Oklch, Rgb}; +use typst_utils::Numeric; + +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 19eb14464..7063931b7 100644 --- a/crates/typst-html/src/lib.rs +++ b/crates/typst-html/src/lib.rs @@ -1,7 +1,9 @@ //! Typst's HTML exporter. +mod css; mod encode; mod rules; +mod typed; pub use self::encode::html; pub use self::rules::register; @@ -9,7 +11,9 @@ 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, }; @@ -20,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 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 8240b2963..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) { @@ -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-library/src/html/mod.rs b/crates/typst-library/src/html/mod.rs index f98352061..ca2cc0311 100644 --- a/crates/typst-library/src/html/mod.rs +++ b/crates/typst-library/src/html/mod.rs @@ -1,23 +1,12 @@ //! HTML output. mod dom; -mod typed; pub use self::dom::*; use ecow::EcoString; -use crate::foundations::{elem, Content, Module, Scope}; - -/// Create a module with all HTML definitions. -pub fn module() -> Module { - let mut html = Scope::deduplicating(); - html.start_category(crate::Category::Html); - html.define_elem::(); - html.define_elem::(); - self::typed::define(&mut html); - Module::new("html", html) -} +use crate::foundations::{elem, Content}; /// An HTML element that can contain Typst content. /// diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs index 3e2ce99ec..5d047570b 100644 --- a/crates/typst-library/src/lib.rs +++ b/crates/typst-library/src/lib.rs @@ -165,7 +165,6 @@ pub struct Library { /// Constructed via the `LibraryExt` trait. #[derive(Debug, Clone)] pub struct LibraryBuilder { - #[expect(unused, reason = "will be used in the future")] routines: &'static Routines, inputs: Option, features: Features, @@ -200,7 +199,7 @@ impl LibraryBuilder { pub fn build(self) -> Library { let math = math::module(); let inputs = self.inputs.unwrap_or_default(); - let global = global(math.clone(), inputs, &self.features); + let global = global(self.routines, math.clone(), inputs, &self.features); Library { global: global.clone(), math, @@ -282,7 +281,12 @@ impl Category { } /// Construct the module with global definitions. -fn global(math: Module, inputs: Dict, features: &Features) -> Module { +fn global( + routines: &Routines, + math: Module, + inputs: Dict, + features: &Features, +) -> Module { let mut global = Scope::deduplicating(); self::foundations::define(&mut global, inputs, features); @@ -297,7 +301,7 @@ fn global(math: Module, inputs: Dict, features: &Features) -> Module { global.define("math", math); global.define("pdf", self::pdf::module()); if features.is_enabled(Feature::Html) { - global.define("html", self::html::module()); + global.define("html", (routines.html_module)()); } prelude(&mut global); diff --git a/crates/typst-library/src/routines.rs b/crates/typst-library/src/routines.rs index 6db99ba51..a81806fd5 100644 --- a/crates/typst-library/src/routines.rs +++ b/crates/typst-library/src/routines.rs @@ -8,8 +8,8 @@ use typst_utils::LazyHash; use crate::diag::SourceResult; use crate::engine::{Engine, Route, Sink, Traced}; use crate::foundations::{ - Args, Closure, Content, Context, Func, NativeRuleMap, Scope, StyleChain, Styles, - Value, + Args, Closure, Content, Context, Func, Module, NativeRuleMap, Scope, StyleChain, + Styles, Value, }; use crate::introspection::{Introspector, Locator, SplitLocator}; use crate::layout::{Frame, Region}; @@ -92,6 +92,9 @@ routines! { styles: StyleChain, region: Region, ) -> SourceResult + + /// Constructs the `html` module. + fn html_module() -> Module } /// Defines what kind of realization we are performing. diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs index 11d5c9e05..591e5a9b9 100644 --- a/crates/typst/src/lib.rs +++ b/crates/typst/src/lib.rs @@ -357,4 +357,5 @@ pub static ROUTINES: LazyLock = LazyLock::new(|| Routines { eval_closure: typst_eval::eval_closure, realize: typst_realize::realize, layout_frame: typst_layout::layout_frame, + html_module: typst_html::module, });