mirror of
https://github.com/typst/typst
synced 2025-07-11 22:52:53 +08:00
Move html
module to typst-html
crate (#6577)
This commit is contained in:
parent
e71674f6b3
commit
52a708b988
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -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",
|
||||
|
@ -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
|
||||
|
135
crates/typst-html/src/css.rs
Normal file
135
crates/typst-html/src/css.rs
Normal file
@ -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<palette::rgb::Rgba<palette::encoding::Srgb, u8>> {
|
||||
let l = v.into_format::<u8, u8>();
|
||||
let h = l.into_format::<f32, f32>();
|
||||
(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
|
||||
}
|
@ -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::<HtmlElem>();
|
||||
html.define_elem::<FrameElem>();
|
||||
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
|
||||
|
@ -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<palette::rgb::Rgba<palette::encoding::Srgb, u8>> {
|
||||
let l = v.into_format::<u8, u8>();
|
||||
let h = l.into_format::<f32, f32>();
|
||||
(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::*;
|
@ -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::<HtmlElem>();
|
||||
html.define_elem::<FrameElem>();
|
||||
self::typed::define(&mut html);
|
||||
Module::new("html", html)
|
||||
}
|
||||
use crate::foundations::{elem, Content};
|
||||
|
||||
/// An HTML element that can contain Typst content.
|
||||
///
|
||||
|
@ -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<Dict>,
|
||||
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);
|
||||
|
@ -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<Frame>
|
||||
|
||||
/// Constructs the `html` module.
|
||||
fn html_module() -> Module
|
||||
}
|
||||
|
||||
/// Defines what kind of realization we are performing.
|
||||
|
@ -357,4 +357,5 @@ pub static ROUTINES: LazyLock<Routines> = LazyLock::new(|| Routines {
|
||||
eval_closure: typst_eval::eval_closure,
|
||||
realize: typst_realize::realize,
|
||||
layout_frame: typst_layout::layout_frame,
|
||||
html_module: typst_html::module,
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user