diff --git a/Cargo.lock b/Cargo.lock index c68d357ca..1ef8c3d19 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=a5428cb#a5428cb9c81a41354d44b44dbd5a16a710bbd928" +source = "git+https://github.com/typst/codex?rev=9ac86f9#9ac86f96af5b89fce555e6bba8b6d1ac7b44ef00" [[package]] name = "color-print" @@ -2878,7 +2878,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" @@ -2988,8 +2988,12 @@ dependencies = [ name = "typst-html" version = "0.13.1" dependencies = [ + "bumpalo", "comemo", "ecow", + "palette", + "time", + "typst-assets", "typst-library", "typst-macros", "typst-svg", @@ -3045,6 +3049,7 @@ version = "0.13.1" dependencies = [ "az", "bumpalo", + "codex", "comemo", "ecow", "hypher", diff --git a/Cargo.toml b/Cargo.toml index b9aefde09..b1358d2b8 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 = "a5428cb" } +codex = { git = "https://github.com/typst/codex", rev = "9ac86f9" } color-print = "0.3.6" comemo = "0.4" csv = "1" 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 cb708028e..092c09f4c 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 { diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index 8e420eecf..bea41bc08 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -14,10 +14,10 @@ use typst::diag::{ bail, At, Severity, SourceDiagnostic, SourceResult, StrResult, Warned, }; use typst::foundations::{Datetime, Smart}; -use typst::html::HtmlDocument; use typst::layout::{Frame, Page, PageRanges, PagedDocument}; use typst::syntax::{FileId, Lines, Span}; use typst::WorldExt; +use typst_html::HtmlDocument; use typst_pdf::{PdfOptions, PdfStandards, Timestamp}; use crate::args::{ 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-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/attr.rs b/crates/typst-html/src/attr.rs new file mode 100644 index 000000000..0fec3955d --- /dev/null +++ b/crates/typst-html/src/attr.rs @@ -0,0 +1,195 @@ + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(non_upper_case_globals)] +#![allow(dead_code)] + +use crate::HtmlAttr; + +pub const abbr: HtmlAttr = HtmlAttr::constant("abbr"); +pub const accept: HtmlAttr = HtmlAttr::constant("accept"); +pub const accept_charset: HtmlAttr = HtmlAttr::constant("accept-charset"); +pub const accesskey: HtmlAttr = HtmlAttr::constant("accesskey"); +pub const action: HtmlAttr = HtmlAttr::constant("action"); +pub const allow: HtmlAttr = HtmlAttr::constant("allow"); +pub const allowfullscreen: HtmlAttr = HtmlAttr::constant("allowfullscreen"); +pub const alpha: HtmlAttr = HtmlAttr::constant("alpha"); +pub const alt: HtmlAttr = HtmlAttr::constant("alt"); +pub const aria_activedescendant: HtmlAttr = HtmlAttr::constant("aria-activedescendant"); +pub const aria_atomic: HtmlAttr = HtmlAttr::constant("aria-atomic"); +pub const aria_autocomplete: HtmlAttr = HtmlAttr::constant("aria-autocomplete"); +pub const aria_busy: HtmlAttr = HtmlAttr::constant("aria-busy"); +pub const aria_checked: HtmlAttr = HtmlAttr::constant("aria-checked"); +pub const aria_colcount: HtmlAttr = HtmlAttr::constant("aria-colcount"); +pub const aria_colindex: HtmlAttr = HtmlAttr::constant("aria-colindex"); +pub const aria_colspan: HtmlAttr = HtmlAttr::constant("aria-colspan"); +pub const aria_controls: HtmlAttr = HtmlAttr::constant("aria-controls"); +pub const aria_current: HtmlAttr = HtmlAttr::constant("aria-current"); +pub const aria_describedby: HtmlAttr = HtmlAttr::constant("aria-describedby"); +pub const aria_details: HtmlAttr = HtmlAttr::constant("aria-details"); +pub const aria_disabled: HtmlAttr = HtmlAttr::constant("aria-disabled"); +pub const aria_errormessage: HtmlAttr = HtmlAttr::constant("aria-errormessage"); +pub const aria_expanded: HtmlAttr = HtmlAttr::constant("aria-expanded"); +pub const aria_flowto: HtmlAttr = HtmlAttr::constant("aria-flowto"); +pub const aria_haspopup: HtmlAttr = HtmlAttr::constant("aria-haspopup"); +pub const aria_hidden: HtmlAttr = HtmlAttr::constant("aria-hidden"); +pub const aria_invalid: HtmlAttr = HtmlAttr::constant("aria-invalid"); +pub const aria_keyshortcuts: HtmlAttr = HtmlAttr::constant("aria-keyshortcuts"); +pub const aria_label: HtmlAttr = HtmlAttr::constant("aria-label"); +pub const aria_labelledby: HtmlAttr = HtmlAttr::constant("aria-labelledby"); +pub const aria_level: HtmlAttr = HtmlAttr::constant("aria-level"); +pub const aria_live: HtmlAttr = HtmlAttr::constant("aria-live"); +pub const aria_modal: HtmlAttr = HtmlAttr::constant("aria-modal"); +pub const aria_multiline: HtmlAttr = HtmlAttr::constant("aria-multiline"); +pub const aria_multiselectable: HtmlAttr = HtmlAttr::constant("aria-multiselectable"); +pub const aria_orientation: HtmlAttr = HtmlAttr::constant("aria-orientation"); +pub const aria_owns: HtmlAttr = HtmlAttr::constant("aria-owns"); +pub const aria_placeholder: HtmlAttr = HtmlAttr::constant("aria-placeholder"); +pub const aria_posinset: HtmlAttr = HtmlAttr::constant("aria-posinset"); +pub const aria_pressed: HtmlAttr = HtmlAttr::constant("aria-pressed"); +pub const aria_readonly: HtmlAttr = HtmlAttr::constant("aria-readonly"); +pub const aria_relevant: HtmlAttr = HtmlAttr::constant("aria-relevant"); +pub const aria_required: HtmlAttr = HtmlAttr::constant("aria-required"); +pub const aria_roledescription: HtmlAttr = HtmlAttr::constant("aria-roledescription"); +pub const aria_rowcount: HtmlAttr = HtmlAttr::constant("aria-rowcount"); +pub const aria_rowindex: HtmlAttr = HtmlAttr::constant("aria-rowindex"); +pub const aria_rowspan: HtmlAttr = HtmlAttr::constant("aria-rowspan"); +pub const aria_selected: HtmlAttr = HtmlAttr::constant("aria-selected"); +pub const aria_setsize: HtmlAttr = HtmlAttr::constant("aria-setsize"); +pub const aria_sort: HtmlAttr = HtmlAttr::constant("aria-sort"); +pub const aria_valuemax: HtmlAttr = HtmlAttr::constant("aria-valuemax"); +pub const aria_valuemin: HtmlAttr = HtmlAttr::constant("aria-valuemin"); +pub const aria_valuenow: HtmlAttr = HtmlAttr::constant("aria-valuenow"); +pub const aria_valuetext: HtmlAttr = HtmlAttr::constant("aria-valuetext"); +pub const r#as: HtmlAttr = HtmlAttr::constant("as"); +pub const r#async: HtmlAttr = HtmlAttr::constant("async"); +pub const autocapitalize: HtmlAttr = HtmlAttr::constant("autocapitalize"); +pub const autocomplete: HtmlAttr = HtmlAttr::constant("autocomplete"); +pub const autocorrect: HtmlAttr = HtmlAttr::constant("autocorrect"); +pub const autofocus: HtmlAttr = HtmlAttr::constant("autofocus"); +pub const autoplay: HtmlAttr = HtmlAttr::constant("autoplay"); +pub const blocking: HtmlAttr = HtmlAttr::constant("blocking"); +pub const charset: HtmlAttr = HtmlAttr::constant("charset"); +pub const checked: HtmlAttr = HtmlAttr::constant("checked"); +pub const cite: HtmlAttr = HtmlAttr::constant("cite"); +pub const class: HtmlAttr = HtmlAttr::constant("class"); +pub const closedby: HtmlAttr = HtmlAttr::constant("closedby"); +pub const color: HtmlAttr = HtmlAttr::constant("color"); +pub const colorspace: HtmlAttr = HtmlAttr::constant("colorspace"); +pub const cols: HtmlAttr = HtmlAttr::constant("cols"); +pub const colspan: HtmlAttr = HtmlAttr::constant("colspan"); +pub const command: HtmlAttr = HtmlAttr::constant("command"); +pub const commandfor: HtmlAttr = HtmlAttr::constant("commandfor"); +pub const content: HtmlAttr = HtmlAttr::constant("content"); +pub const contenteditable: HtmlAttr = HtmlAttr::constant("contenteditable"); +pub const controls: HtmlAttr = HtmlAttr::constant("controls"); +pub const coords: HtmlAttr = HtmlAttr::constant("coords"); +pub const crossorigin: HtmlAttr = HtmlAttr::constant("crossorigin"); +pub const data: HtmlAttr = HtmlAttr::constant("data"); +pub const datetime: HtmlAttr = HtmlAttr::constant("datetime"); +pub const decoding: HtmlAttr = HtmlAttr::constant("decoding"); +pub const default: HtmlAttr = HtmlAttr::constant("default"); +pub const defer: HtmlAttr = HtmlAttr::constant("defer"); +pub const dir: HtmlAttr = HtmlAttr::constant("dir"); +pub const dirname: HtmlAttr = HtmlAttr::constant("dirname"); +pub const disabled: HtmlAttr = HtmlAttr::constant("disabled"); +pub const download: HtmlAttr = HtmlAttr::constant("download"); +pub const draggable: HtmlAttr = HtmlAttr::constant("draggable"); +pub const enctype: HtmlAttr = HtmlAttr::constant("enctype"); +pub const enterkeyhint: HtmlAttr = HtmlAttr::constant("enterkeyhint"); +pub const fetchpriority: HtmlAttr = HtmlAttr::constant("fetchpriority"); +pub const r#for: HtmlAttr = HtmlAttr::constant("for"); +pub const form: HtmlAttr = HtmlAttr::constant("form"); +pub const formaction: HtmlAttr = HtmlAttr::constant("formaction"); +pub const formenctype: HtmlAttr = HtmlAttr::constant("formenctype"); +pub const formmethod: HtmlAttr = HtmlAttr::constant("formmethod"); +pub const formnovalidate: HtmlAttr = HtmlAttr::constant("formnovalidate"); +pub const formtarget: HtmlAttr = HtmlAttr::constant("formtarget"); +pub const headers: HtmlAttr = HtmlAttr::constant("headers"); +pub const height: HtmlAttr = HtmlAttr::constant("height"); +pub const hidden: HtmlAttr = HtmlAttr::constant("hidden"); +pub const high: HtmlAttr = HtmlAttr::constant("high"); +pub const href: HtmlAttr = HtmlAttr::constant("href"); +pub const hreflang: HtmlAttr = HtmlAttr::constant("hreflang"); +pub const http_equiv: HtmlAttr = HtmlAttr::constant("http-equiv"); +pub const id: HtmlAttr = HtmlAttr::constant("id"); +pub const imagesizes: HtmlAttr = HtmlAttr::constant("imagesizes"); +pub const imagesrcset: HtmlAttr = HtmlAttr::constant("imagesrcset"); +pub const inert: HtmlAttr = HtmlAttr::constant("inert"); +pub const inputmode: HtmlAttr = HtmlAttr::constant("inputmode"); +pub const integrity: HtmlAttr = HtmlAttr::constant("integrity"); +pub const is: HtmlAttr = HtmlAttr::constant("is"); +pub const ismap: HtmlAttr = HtmlAttr::constant("ismap"); +pub const itemid: HtmlAttr = HtmlAttr::constant("itemid"); +pub const itemprop: HtmlAttr = HtmlAttr::constant("itemprop"); +pub const itemref: HtmlAttr = HtmlAttr::constant("itemref"); +pub const itemscope: HtmlAttr = HtmlAttr::constant("itemscope"); +pub const itemtype: HtmlAttr = HtmlAttr::constant("itemtype"); +pub const kind: HtmlAttr = HtmlAttr::constant("kind"); +pub const label: HtmlAttr = HtmlAttr::constant("label"); +pub const lang: HtmlAttr = HtmlAttr::constant("lang"); +pub const list: HtmlAttr = HtmlAttr::constant("list"); +pub const loading: HtmlAttr = HtmlAttr::constant("loading"); +pub const r#loop: HtmlAttr = HtmlAttr::constant("loop"); +pub const low: HtmlAttr = HtmlAttr::constant("low"); +pub const max: HtmlAttr = HtmlAttr::constant("max"); +pub const maxlength: HtmlAttr = HtmlAttr::constant("maxlength"); +pub const media: HtmlAttr = HtmlAttr::constant("media"); +pub const method: HtmlAttr = HtmlAttr::constant("method"); +pub const min: HtmlAttr = HtmlAttr::constant("min"); +pub const minlength: HtmlAttr = HtmlAttr::constant("minlength"); +pub const multiple: HtmlAttr = HtmlAttr::constant("multiple"); +pub const muted: HtmlAttr = HtmlAttr::constant("muted"); +pub const name: HtmlAttr = HtmlAttr::constant("name"); +pub const nomodule: HtmlAttr = HtmlAttr::constant("nomodule"); +pub const nonce: HtmlAttr = HtmlAttr::constant("nonce"); +pub const novalidate: HtmlAttr = HtmlAttr::constant("novalidate"); +pub const open: HtmlAttr = HtmlAttr::constant("open"); +pub const optimum: HtmlAttr = HtmlAttr::constant("optimum"); +pub const pattern: HtmlAttr = HtmlAttr::constant("pattern"); +pub const ping: HtmlAttr = HtmlAttr::constant("ping"); +pub const placeholder: HtmlAttr = HtmlAttr::constant("placeholder"); +pub const playsinline: HtmlAttr = HtmlAttr::constant("playsinline"); +pub const popover: HtmlAttr = HtmlAttr::constant("popover"); +pub const popovertarget: HtmlAttr = HtmlAttr::constant("popovertarget"); +pub const popovertargetaction: HtmlAttr = HtmlAttr::constant("popovertargetaction"); +pub const poster: HtmlAttr = HtmlAttr::constant("poster"); +pub const preload: HtmlAttr = HtmlAttr::constant("preload"); +pub const readonly: HtmlAttr = HtmlAttr::constant("readonly"); +pub const referrerpolicy: HtmlAttr = HtmlAttr::constant("referrerpolicy"); +pub const rel: HtmlAttr = HtmlAttr::constant("rel"); +pub const required: HtmlAttr = HtmlAttr::constant("required"); +pub const reversed: HtmlAttr = HtmlAttr::constant("reversed"); +pub const role: HtmlAttr = HtmlAttr::constant("role"); +pub const rows: HtmlAttr = HtmlAttr::constant("rows"); +pub const rowspan: HtmlAttr = HtmlAttr::constant("rowspan"); +pub const sandbox: HtmlAttr = HtmlAttr::constant("sandbox"); +pub const scope: HtmlAttr = HtmlAttr::constant("scope"); +pub const selected: HtmlAttr = HtmlAttr::constant("selected"); +pub const shadowrootclonable: HtmlAttr = HtmlAttr::constant("shadowrootclonable"); +pub const shadowrootcustomelementregistry: HtmlAttr = HtmlAttr::constant("shadowrootcustomelementregistry"); +pub const shadowrootdelegatesfocus: HtmlAttr = HtmlAttr::constant("shadowrootdelegatesfocus"); +pub const shadowrootmode: HtmlAttr = HtmlAttr::constant("shadowrootmode"); +pub const shadowrootserializable: HtmlAttr = HtmlAttr::constant("shadowrootserializable"); +pub const shape: HtmlAttr = HtmlAttr::constant("shape"); +pub const size: HtmlAttr = HtmlAttr::constant("size"); +pub const sizes: HtmlAttr = HtmlAttr::constant("sizes"); +pub const slot: HtmlAttr = HtmlAttr::constant("slot"); +pub const span: HtmlAttr = HtmlAttr::constant("span"); +pub const spellcheck: HtmlAttr = HtmlAttr::constant("spellcheck"); +pub const src: HtmlAttr = HtmlAttr::constant("src"); +pub const srcdoc: HtmlAttr = HtmlAttr::constant("srcdoc"); +pub const srclang: HtmlAttr = HtmlAttr::constant("srclang"); +pub const srcset: HtmlAttr = HtmlAttr::constant("srcset"); +pub const start: HtmlAttr = HtmlAttr::constant("start"); +pub const step: HtmlAttr = HtmlAttr::constant("step"); +pub const style: HtmlAttr = HtmlAttr::constant("style"); +pub const tabindex: HtmlAttr = HtmlAttr::constant("tabindex"); +pub const target: HtmlAttr = HtmlAttr::constant("target"); +pub const title: HtmlAttr = HtmlAttr::constant("title"); +pub const translate: HtmlAttr = HtmlAttr::constant("translate"); +pub const r#type: HtmlAttr = HtmlAttr::constant("type"); +pub const usemap: HtmlAttr = HtmlAttr::constant("usemap"); +pub const value: HtmlAttr = HtmlAttr::constant("value"); +pub const width: HtmlAttr = HtmlAttr::constant("width"); +pub const wrap: HtmlAttr = HtmlAttr::constant("wrap"); +pub const writingsuggestions: HtmlAttr = HtmlAttr::constant("writingsuggestions"); diff --git a/crates/typst-html/src/charsets.rs b/crates/typst-html/src/charsets.rs new file mode 100644 index 000000000..251ff15c9 --- /dev/null +++ b/crates/typst-html/src/charsets.rs @@ -0,0 +1,81 @@ +//! Defines syntactical properties of HTML tags, attributes, and text. + +/// Check whether a character is in a tag name. +pub const fn is_valid_in_tag_name(c: char) -> bool { + c.is_ascii_alphanumeric() +} + +/// Check whether a character is valid in an attribute name. +pub const fn is_valid_in_attribute_name(c: char) -> bool { + match c { + // These are forbidden. + '\0' | ' ' | '"' | '\'' | '>' | '/' | '=' => false, + c if is_whatwg_control_char(c) => false, + c if is_whatwg_non_char(c) => false, + // _Everything_ else is allowed, including U+2029 paragraph + // separator. Go wild. + _ => true, + } +} + +/// Check whether a character can be an used in an attribute value without +/// escaping. +/// +/// See +pub const fn is_valid_in_attribute_value(c: char) -> bool { + match c { + // Ampersands are sometimes legal (i.e. when they are not _ambiguous + // ampersands_) but it is not worth the trouble to check for that. + '&' => false, + // Quotation marks are not allowed in double-quote-delimited attribute + // values. + '"' => false, + // All other text characters are allowed. + c => is_w3c_text_char(c), + } +} + +/// Check whether a character can be an used in normal text without +/// escaping. +pub const fn is_valid_in_normal_element_text(c: char) -> bool { + match c { + // Ampersands are sometimes legal (i.e. when they are not _ambiguous + // ampersands_) but it is not worth the trouble to check for that. + '&' => false, + // Less-than signs are not allowed in text. + '<' => false, + // All other text characters are allowed. + c => is_w3c_text_char(c), + } +} + +/// Check if something is valid text in HTML. +pub const fn is_w3c_text_char(c: char) -> bool { + match c { + // Non-characters are obviously not text characters. + c if is_whatwg_non_char(c) => false, + // Control characters are disallowed, except for whitespace. + c if is_whatwg_control_char(c) => c.is_ascii_whitespace(), + // Everything else is allowed. + _ => true, + } +} + +const fn is_whatwg_non_char(c: char) -> bool { + match c { + '\u{fdd0}'..='\u{fdef}' => true, + // Non-characters matching xxFFFE or xxFFFF up to x10FFFF (inclusive). + c if c as u32 & 0xfffe == 0xfffe && c as u32 <= 0x10ffff => true, + _ => false, + } +} + +const fn is_whatwg_control_char(c: char) -> bool { + match c { + // C0 control characters. + '\u{00}'..='\u{1f}' => true, + // Other control characters. + '\u{7f}'..='\u{9f}' => true, + _ => false, + } +} diff --git a/crates/typst-html/src/convert.rs b/crates/typst-html/src/convert.rs new file mode 100644 index 000000000..171b4cb7e --- /dev/null +++ b/crates/typst-html/src/convert.rs @@ -0,0 +1,130 @@ +use typst_library::diag::{warning, SourceResult}; +use typst_library::engine::Engine; +use typst_library::foundations::{Content, StyleChain, Target, TargetElem}; +use typst_library::introspection::{SplitLocator, TagElem}; +use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size}; +use typst_library::model::ParElem; +use typst_library::routines::Pair; +use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; + +use crate::fragment::html_fragment; +use crate::{attr, tag, FrameElem, HtmlElem, HtmlElement, HtmlFrame, HtmlNode}; + +/// Converts realized content into HTML nodes. +pub fn convert_to_nodes<'a>( + engine: &mut Engine, + locator: &mut SplitLocator, + children: impl IntoIterator>, +) -> SourceResult> { + let mut output = Vec::new(); + for (child, styles) in children { + handle(engine, child, locator, styles, &mut output)?; + } + Ok(output) +} + +/// Convert one element into HTML node(s). +fn handle( + engine: &mut Engine, + child: &Content, + locator: &mut SplitLocator, + styles: StyleChain, + output: &mut Vec, +) -> SourceResult<()> { + if let Some(elem) = child.to_packed::() { + 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.get_ref(styles) { + children = html_fragment(engine, body, locator.next(&elem.span()), styles)?; + } + let element = HtmlElement { + tag: elem.tag, + attrs: elem.attrs.get_cloned(styles), + children, + span: elem.span(), + }; + output.push(element.into()); + } else if let Some(elem) = child.to_packed::() { + let children = + html_fragment(engine, &elem.body, locator.next(&elem.span()), styles)?; + output.push( + HtmlElement::new(tag::p) + .with_children(children) + .spanned(elem.span()) + .into(), + ); + } else if let Some(elem) = child.to_packed::() { + // TODO: This is rather incomplete. + if let Some(body) = elem.body.get_ref(styles) { + let children = + html_fragment(engine, body, locator.next(&elem.span()), styles)?; + output.push( + HtmlElement::new(tag::span) + .with_attr(attr::style, "display: inline-block;") + .with_children(children) + .spanned(elem.span()) + .into(), + ) + } + } else if let Some((elem, body)) = + child + .to_packed::() + .and_then(|elem| match elem.body.get_ref(styles) { + Some(BlockBody::Content(body)) => Some((elem, body)), + _ => None, + }) + { + // TODO: This is rather incomplete. + let children = html_fragment(engine, body, locator.next(&elem.span()), styles)?; + output.push( + HtmlElement::new(tag::div) + .with_children(children) + .spanned(elem.span()) + .into(), + ); + } else if child.is::() { + output.push(HtmlNode::text(' ', child.span())); + } else if let Some(elem) = child.to_packed::() { + let text = if let Some(case) = styles.get(TextElem::case) { + case.apply(&elem.text).into() + } else { + elem.text.clone() + }; + output.push(HtmlNode::text(text, elem.span())); + } else if let Some(elem) = child.to_packed::() { + 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.get(styles) { '"' } else { '\'' }, + child.span(), + )); + } else if let Some(elem) = child.to_packed::() { + let locator = locator.next(&elem.span()); + let style = TargetElem::target.set(Target::Paged).wrap(); + let frame = (engine.routines.layout_frame)( + engine, + &elem.body, + locator, + styles.chain(&style), + Region::new(Size::splat(Abs::inf()), Axes::splat(false)), + )?; + output.push(HtmlNode::Frame(HtmlFrame { + inner: frame, + text_size: styles.resolve(TextElem::size), + })); + } else { + engine.sink.warn(warning!( + child.span(), + "{} was ignored during HTML export", + child.elem().name() + )); + } + Ok(()) +} + +/// Checks whether the given element is an inline-level HTML element. +pub fn is_inline(elem: &Content) -> bool { + elem.to_packed::() + .is_some_and(|elem| tag::is_inline_by_default(elem.tag)) +} diff --git a/crates/typst-html/src/css.rs b/crates/typst-html/src/css.rs new file mode 100644 index 000000000..5916d3147 --- /dev/null +++ b/crates/typst-html/src/css.rs @@ -0,0 +1,178 @@ +//! Conversion from Typst data types into CSS data types. + +use std::fmt::{self, Display, Write}; + +use ecow::EcoString; +use typst_library::layout::{Length, Rel}; +use typst_library::visualize::{Color, Hsl, LinearRgb, Oklab, Oklch, Rgb}; +use typst_utils::Numeric; + +/// 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/document.rs b/crates/typst-html/src/document.rs new file mode 100644 index 000000000..9f0124e57 --- /dev/null +++ b/crates/typst-html/src/document.rs @@ -0,0 +1,219 @@ +use std::num::NonZeroUsize; + +use comemo::{Tracked, TrackedMut}; +use typst_library::diag::{bail, SourceResult}; +use typst_library::engine::{Engine, Route, Sink, Traced}; +use typst_library::foundations::{Content, StyleChain}; +use typst_library::introspection::{Introspector, IntrospectorBuilder, Locator}; +use typst_library::layout::{Point, Position, Transform}; +use typst_library::model::DocumentInfo; +use typst_library::routines::{Arenas, RealizationKind, Routines}; +use typst_library::World; +use typst_syntax::Span; +use typst_utils::NonZeroExt; + +use crate::{attr, tag, HtmlDocument, HtmlElement, HtmlNode}; + +/// Produce an HTML document from content. +/// +/// This first performs root-level realization and then turns the resulting +/// elements into HTML. +#[typst_macros::time(name = "html document")] +pub fn html_document( + engine: &mut Engine, + content: &Content, + styles: StyleChain, +) -> SourceResult { + html_document_impl( + engine.routines, + engine.world, + engine.introspector, + engine.traced, + TrackedMut::reborrow_mut(&mut engine.sink), + engine.route.track(), + content, + styles, + ) +} + +/// The internal implementation of `html_document`. +#[comemo::memoize] +#[allow(clippy::too_many_arguments)] +fn html_document_impl( + routines: &Routines, + world: Tracked, + introspector: Tracked, + traced: Tracked, + sink: TrackedMut, + route: Tracked, + content: &Content, + styles: StyleChain, +) -> SourceResult { + let mut locator = Locator::root().split(); + let mut engine = Engine { + routines, + world, + introspector, + traced, + sink, + route: Route::extend(route).unnested(), + }; + + // Mark the external styles as "outside" so that they are valid at the page + // level. + let styles = styles.to_map().outside(); + let styles = StyleChain::new(&styles); + + let arenas = Arenas::default(); + let mut info = DocumentInfo::default(); + let children = (engine.routines.realize)( + RealizationKind::HtmlDocument { + info: &mut info, + is_inline: crate::convert::is_inline, + }, + &mut engine, + &mut locator, + &arenas, + content, + styles, + )?; + + let output = crate::convert::convert_to_nodes( + &mut engine, + &mut locator, + children.iter().copied(), + )?; + let introspector = introspect_html(&output); + let root = root_element(output, &info)?; + + Ok(HtmlDocument { info, root, introspector }) +} + +/// Introspects HTML nodes. +#[typst_macros::time(name = "introspect html")] +fn introspect_html(output: &[HtmlNode]) -> Introspector { + fn discover( + builder: &mut IntrospectorBuilder, + sink: &mut Vec<(Content, Position)>, + nodes: &[HtmlNode], + ) { + for node in nodes { + match node { + HtmlNode::Tag(tag) => builder.discover_in_tag( + sink, + tag, + Position { page: NonZeroUsize::ONE, point: Point::zero() }, + ), + HtmlNode::Text(_, _) => {} + HtmlNode::Element(elem) => discover(builder, sink, &elem.children), + HtmlNode::Frame(frame) => builder.discover_in_frame( + sink, + &frame.inner, + NonZeroUsize::ONE, + Transform::identity(), + ), + } + } + } + + let mut elems = Vec::new(); + let mut builder = IntrospectorBuilder::new(); + discover(&mut builder, &mut elems, output); + builder.finalize(elems) +} + +/// Wrap the nodes in `` and `` if they are not yet rooted, +/// supplying a suitable ``. +fn root_element(output: Vec, info: &DocumentInfo) -> SourceResult { + let head = head_element(info); + let body = match classify_output(output)? { + OutputKind::Html(element) => return Ok(element), + OutputKind::Body(body) => body, + OutputKind::Leafs(leafs) => HtmlElement::new(tag::body).with_children(leafs), + }; + Ok(HtmlElement::new(tag::html).with_children(vec![head.into(), body.into()])) +} + +/// Generate a `` element. +fn head_element(info: &DocumentInfo) -> HtmlElement { + let mut children = vec![]; + + children.push(HtmlElement::new(tag::meta).with_attr(attr::charset, "utf-8").into()); + + children.push( + HtmlElement::new(tag::meta) + .with_attr(attr::name, "viewport") + .with_attr(attr::content, "width=device-width, initial-scale=1") + .into(), + ); + + if let Some(title) = &info.title { + children.push( + HtmlElement::new(tag::title) + .with_children(vec![HtmlNode::Text(title.clone(), Span::detached())]) + .into(), + ); + } + + if let Some(description) = &info.description { + children.push( + HtmlElement::new(tag::meta) + .with_attr(attr::name, "description") + .with_attr(attr::content, description.clone()) + .into(), + ); + } + + if !info.author.is_empty() { + children.push( + HtmlElement::new(tag::meta) + .with_attr(attr::name, "authors") + .with_attr(attr::content, info.author.join(", ")) + .into(), + ) + } + + if !info.keywords.is_empty() { + children.push( + HtmlElement::new(tag::meta) + .with_attr(attr::name, "keywords") + .with_attr(attr::content, info.keywords.join(", ")) + .into(), + ) + } + + HtmlElement::new(tag::head).with_children(children) +} + +/// Determine which kind of output the user generated. +fn classify_output(mut output: Vec) -> SourceResult { + let count = output.iter().filter(|node| !matches!(node, HtmlNode::Tag(_))).count(); + for node in &mut output { + let HtmlNode::Element(elem) = node else { continue }; + let tag = elem.tag; + let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html)); + match (tag, count) { + (tag::html, 1) => return Ok(OutputKind::Html(take())), + (tag::body, 1) => return Ok(OutputKind::Body(take())), + (tag::html | tag::body, _) => bail!( + elem.span, + "`{}` element must be the only element in the document", + elem.tag, + ), + _ => {} + } + } + Ok(OutputKind::Leafs(output)) +} + +/// What kinds of output the user generated. +enum OutputKind { + /// The user generated their own `` element. We do not need to supply + /// one. + Html(HtmlElement), + /// The user generate their own `` element. We do not need to supply + /// one, but need supply the `` element. + Body(HtmlElement), + /// The user generated leafs which we wrap in a `` and ``. + Leafs(Vec), +} diff --git a/crates/typst-html/src/dom.rs b/crates/typst-html/src/dom.rs new file mode 100644 index 000000000..cf74e1bfc --- /dev/null +++ b/crates/typst-html/src/dom.rs @@ -0,0 +1,281 @@ +use std::fmt::{self, Debug, Display, Formatter}; + +use ecow::{EcoString, EcoVec}; +use typst_library::diag::{bail, HintedStrResult, StrResult}; +use typst_library::foundations::{cast, Dict, Repr, Str}; +use typst_library::introspection::{Introspector, Tag}; +use typst_library::layout::{Abs, Frame}; +use typst_library::model::DocumentInfo; +use typst_syntax::Span; +use typst_utils::{PicoStr, ResolvedPicoStr}; + +use crate::charsets; + +/// An HTML document. +#[derive(Debug, Clone)] +pub struct HtmlDocument { + /// The document's root HTML element. + pub root: HtmlElement, + /// Details about the document. + pub info: DocumentInfo, + /// Provides the ability to execute queries on the document. + pub introspector: Introspector, +} + +/// A child of an HTML element. +#[derive(Debug, Clone, Hash)] +pub enum HtmlNode { + /// An introspectable element that produced something within this node. + Tag(Tag), + /// Plain text. + Text(EcoString, Span), + /// Another element. + Element(HtmlElement), + /// Layouted content that will be embedded into HTML as an SVG. + Frame(HtmlFrame), +} + +impl HtmlNode { + /// Create a plain text node. + pub fn text(text: impl Into, span: Span) -> Self { + Self::Text(text.into(), span) + } +} + +impl From for HtmlNode { + fn from(element: HtmlElement) -> Self { + Self::Element(element) + } +} + +/// An HTML element. +#[derive(Debug, Clone, Hash)] +pub struct HtmlElement { + /// The HTML tag. + pub tag: HtmlTag, + /// The element's attributes. + pub attrs: HtmlAttrs, + /// The element's children. + pub children: Vec, + /// The span from which the element originated, if any. + pub span: Span, +} + +impl HtmlElement { + /// Create a new, blank element without attributes or children. + pub fn new(tag: HtmlTag) -> Self { + Self { + tag, + attrs: HtmlAttrs::default(), + children: vec![], + span: Span::detached(), + } + } + + /// Attach children to the element. + /// + /// Note: This overwrites potential previous children. + pub fn with_children(mut self, children: Vec) -> Self { + self.children = children; + self + } + + /// Add an atribute to the element. + pub fn with_attr(mut self, key: HtmlAttr, value: impl Into) -> Self { + self.attrs.push(key, value); + self + } + + /// Attach a span to the element. + pub fn spanned(mut self, span: Span) -> Self { + self.span = span; + self + } +} + +/// The tag of an HTML element. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct HtmlTag(PicoStr); + +impl HtmlTag { + /// Intern an HTML tag string at runtime. + pub fn intern(string: &str) -> StrResult { + if string.is_empty() { + bail!("tag name must not be empty"); + } + + if let Some(c) = string.chars().find(|&c| !charsets::is_valid_in_tag_name(c)) { + bail!("the character {} is not valid in a tag name", c.repr()); + } + + Ok(Self(PicoStr::intern(string))) + } + + /// Creates a compile-time constant `HtmlTag`. + /// + /// Should only be used in const contexts because it can panic. + #[track_caller] + pub const fn constant(string: &'static str) -> Self { + if string.is_empty() { + panic!("tag name must not be empty"); + } + + let bytes = string.as_bytes(); + let mut i = 0; + while i < bytes.len() { + if !bytes[i].is_ascii() || !charsets::is_valid_in_tag_name(bytes[i] as char) { + panic!("not all characters are valid in a tag name"); + } + i += 1; + } + + Self(PicoStr::constant(string)) + } + + /// Resolves the tag to a string. + pub fn resolve(self) -> ResolvedPicoStr { + self.0.resolve() + } + + /// Turns the tag into its inner interned string. + pub const fn into_inner(self) -> PicoStr { + self.0 + } +} + +impl Debug for HtmlTag { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(self, f) + } +} + +impl Display for HtmlTag { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "<{}>", self.resolve()) + } +} + +cast! { + HtmlTag, + self => self.0.resolve().as_str().into_value(), + v: Str => Self::intern(&v)?, +} + +/// Attributes of an HTML element. +#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] +pub struct HtmlAttrs(pub EcoVec<(HtmlAttr, EcoString)>); + +impl HtmlAttrs { + /// Creates an empty attribute list. + pub fn new() -> Self { + Self::default() + } + + /// Add an attribute. + pub fn push(&mut self, attr: HtmlAttr, value: impl Into) { + self.0.push((attr, value.into())); + } +} + +cast! { + HtmlAttrs, + self => self.0 + .into_iter() + .map(|(key, value)| (key.resolve().as_str().into(), value.into_value())) + .collect::() + .into_value(), + values: Dict => Self(values + .into_iter() + .map(|(k, v)| { + let attr = HtmlAttr::intern(&k)?; + let value = v.cast::()?; + Ok((attr, value)) + }) + .collect::>()?), +} + +/// An attribute of an HTML element. +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct HtmlAttr(PicoStr); + +impl HtmlAttr { + /// Intern an HTML attribute string at runtime. + pub fn intern(string: &str) -> StrResult { + if string.is_empty() { + bail!("attribute name must not be empty"); + } + + if let Some(c) = + string.chars().find(|&c| !charsets::is_valid_in_attribute_name(c)) + { + bail!("the character {} is not valid in an attribute name", c.repr()); + } + + Ok(Self(PicoStr::intern(string))) + } + + /// Creates a compile-time constant `HtmlAttr`. + /// + /// Must only be used in const contexts (in a constant definition or + /// explicit `const { .. }` block) because otherwise a panic for a malformed + /// attribute or not auto-internible constant will only be caught at + /// runtime. + #[track_caller] + pub const fn constant(string: &'static str) -> Self { + if string.is_empty() { + panic!("attribute name must not be empty"); + } + + let bytes = string.as_bytes(); + let mut i = 0; + while i < bytes.len() { + if !bytes[i].is_ascii() + || !charsets::is_valid_in_attribute_name(bytes[i] as char) + { + panic!("not all characters are valid in an attribute name"); + } + i += 1; + } + + Self(PicoStr::constant(string)) + } + + /// Resolves the attribute to a string. + pub fn resolve(self) -> ResolvedPicoStr { + self.0.resolve() + } + + /// Turns the attribute into its inner interned string. + pub const fn into_inner(self) -> PicoStr { + self.0 + } +} + +impl Debug for HtmlAttr { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(self, f) + } +} + +impl Display for HtmlAttr { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.resolve()) + } +} + +cast! { + HtmlAttr, + self => self.0.resolve().as_str().into_value(), + v: Str => Self::intern(&v)?, +} + +/// Layouted content that will be embedded into HTML as an SVG. +#[derive(Debug, Clone, Hash)] +pub struct HtmlFrame { + /// The frame that will be displayed as an SVG. + pub inner: Frame, + /// The text size where the frame was defined. This is used to size the + /// frame with em units to make text in and outside of the frame sized + /// consistently. + pub text_size: Abs, +} diff --git a/crates/typst-html/src/encode.rs b/crates/typst-html/src/encode.rs index 84860dbe9..be8137399 100644 --- a/crates/typst-html/src/encode.rs +++ b/crates/typst-html/src/encode.rs @@ -2,10 +2,11 @@ use std::fmt::Write; use typst_library::diag::{bail, At, SourceResult, StrResult}; use typst_library::foundations::Repr; -use typst_library::html::{ +use typst_syntax::Span; + +use crate::{ attr, charsets, tag, HtmlDocument, HtmlElement, HtmlFrame, HtmlNode, HtmlTag, }; -use typst_syntax::Span; /// Encodes an HTML document into a string. pub fn html(document: &HtmlDocument) -> SourceResult { diff --git a/crates/typst-html/src/fragment.rs b/crates/typst-html/src/fragment.rs new file mode 100644 index 000000000..78ae7dee0 --- /dev/null +++ b/crates/typst-html/src/fragment.rs @@ -0,0 +1,76 @@ +use comemo::{Track, Tracked, TrackedMut}; +use typst_library::diag::{At, SourceResult}; +use typst_library::engine::{Engine, Route, Sink, Traced}; +use typst_library::foundations::{Content, StyleChain}; +use typst_library::introspection::{Introspector, Locator, LocatorLink}; + +use typst_library::routines::{Arenas, FragmentKind, RealizationKind, Routines}; +use typst_library::World; + +use crate::HtmlNode; + +/// Produce HTML nodes from content. +#[typst_macros::time(name = "html fragment")] +pub fn html_fragment( + engine: &mut Engine, + content: &Content, + locator: Locator, + styles: StyleChain, +) -> SourceResult> { + html_fragment_impl( + engine.routines, + engine.world, + engine.introspector, + engine.traced, + TrackedMut::reborrow_mut(&mut engine.sink), + engine.route.track(), + content, + locator.track(), + styles, + ) +} + +/// The cached, internal implementation of [`html_fragment`]. +#[comemo::memoize] +#[allow(clippy::too_many_arguments)] +fn html_fragment_impl( + routines: &Routines, + world: Tracked, + introspector: Tracked, + traced: Tracked, + sink: TrackedMut, + route: Tracked, + content: &Content, + locator: Tracked, + styles: StyleChain, +) -> SourceResult> { + let link = LocatorLink::new(locator); + let mut locator = Locator::link(&link).split(); + let mut engine = Engine { + routines, + world, + introspector, + traced, + sink, + route: Route::extend(route), + }; + + engine.route.check_html_depth().at(content.span())?; + + let arenas = Arenas::default(); + let children = (engine.routines.realize)( + // No need to know about the `FragmentKind` because we handle both + // uniformly. + RealizationKind::HtmlFragment { + kind: &mut FragmentKind::Block, + is_inline: crate::convert::is_inline, + }, + &mut engine, + &mut locator, + &arenas, + content, + styles, + )?; + + crate::convert::convert_to_nodes(&mut engine, &mut locator, children.iter().copied()) +} diff --git a/crates/typst-html/src/lib.rs b/crates/typst-html/src/lib.rs index 60ffa78ee..d7b29dbbc 100644 --- a/crates/typst-html/src/lib.rs +++ b/crates/typst-html/src/lib.rs @@ -1,357 +1,108 @@ //! Typst's HTML exporter. +mod attr; +mod charsets; +mod convert; +mod css; +mod document; +mod dom; mod encode; +mod fragment; +mod rules; +mod tag; +mod typed; +pub use self::document::html_document; +pub use self::dom::*; 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::html::{ - attr, tag, FrameElem, HtmlDocument, HtmlElem, HtmlElement, HtmlFrame, HtmlNode, -}; -use typst_library::introspection::{ - Introspector, Locator, LocatorLink, SplitLocator, TagElem, -}; -use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size}; -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_syntax::Span; +use ecow::EcoString; +use typst_library::foundations::{Content, Module, Scope}; +use typst_library::Category; +use typst_macros::elem; -/// Produce an HTML document from content. +/// Creates the 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) +} + +/// An HTML element that can contain Typst content. /// -/// This first performs root-level realization and then turns the resulting -/// elements into HTML. -#[typst_macros::time(name = "html document")] -pub fn html_document( - engine: &mut Engine, - content: &Content, - styles: StyleChain, -) -> SourceResult { - html_document_impl( - engine.routines, - engine.world, - engine.introspector, - engine.traced, - TrackedMut::reborrow_mut(&mut engine.sink), - engine.route.track(), - content, - styles, - ) +/// Typst's HTML export automatically generates the appropriate tags for most +/// elements. However, sometimes, it is desirable to retain more control. For +/// example, when using Typst to generate your blog, you could use this function +/// to wrap each article in an `
` tag. +/// +/// Typst is aware of what is valid HTML. A tag and its attributes must form +/// syntactically valid HTML. Some tags, like `meta` do not accept content. +/// Hence, you must not provide a body for them. We may add more checks in the +/// future, so be sure that you are generating valid HTML when using this +/// function. +/// +/// Normally, Typst will generate `html`, `head`, and `body` tags for you. If +/// you instead create them with this function, Typst will omit its own tags. +/// +/// ```typ +/// #html.elem("div", attrs: (style: "background: aqua"))[ +/// A div with _Typst content_ inside! +/// ] +/// ``` +#[elem(name = "elem")] +pub struct HtmlElem { + /// The element's tag. + #[required] + pub tag: HtmlTag, + + /// The element's HTML attributes. + pub attrs: HtmlAttrs, + + /// The contents of the HTML element. + /// + /// The body can be arbitrary Typst content. + #[positional] + pub body: Option, } -/// The internal implementation of `html_document`. -#[comemo::memoize] -#[allow(clippy::too_many_arguments)] -fn html_document_impl( - routines: &Routines, - world: Tracked, - introspector: Tracked, - traced: Tracked, - sink: TrackedMut, - route: Tracked, - content: &Content, - styles: StyleChain, -) -> SourceResult { - let mut locator = Locator::root().split(); - let mut engine = Engine { - routines, - world, - introspector, - traced, - sink, - route: Route::extend(route).unnested(), - }; - - // Mark the external styles as "outside" so that they are valid at the page - // level. - let styles = styles.to_map().outside(); - let styles = StyleChain::new(&styles); - - let arenas = Arenas::default(); - let mut info = DocumentInfo::default(); - let children = (engine.routines.realize)( - RealizationKind::HtmlDocument(&mut info), - &mut engine, - &mut locator, - &arenas, - content, - styles, - )?; - - let output = handle_list(&mut engine, &mut locator, children.iter().copied())?; - let introspector = Introspector::html(&output); - let root = root_element(output, &info)?; - - Ok(HtmlDocument { info, root, introspector }) -} - -/// Produce HTML nodes from content. -#[typst_macros::time(name = "html fragment")] -pub fn html_fragment( - engine: &mut Engine, - content: &Content, - locator: Locator, - styles: StyleChain, -) -> SourceResult> { - html_fragment_impl( - engine.routines, - engine.world, - engine.introspector, - engine.traced, - TrackedMut::reborrow_mut(&mut engine.sink), - engine.route.track(), - content, - locator.track(), - styles, - ) -} - -/// The cached, internal implementation of [`html_fragment`]. -#[comemo::memoize] -#[allow(clippy::too_many_arguments)] -fn html_fragment_impl( - routines: &Routines, - world: Tracked, - introspector: Tracked, - traced: Tracked, - sink: TrackedMut, - route: Tracked, - content: &Content, - locator: Tracked, - styles: StyleChain, -) -> SourceResult> { - let link = LocatorLink::new(locator); - let mut locator = Locator::link(&link).split(); - let mut engine = Engine { - routines, - world, - introspector, - traced, - sink, - route: Route::extend(route), - }; - - engine.route.check_html_depth().at(content.span())?; - - let arenas = Arenas::default(); - let children = (engine.routines.realize)( - // No need to know about the `FragmentKind` because we handle both - // uniformly. - RealizationKind::HtmlFragment(&mut FragmentKind::Block), - &mut engine, - &mut locator, - &arenas, - content, - styles, - )?; - - handle_list(&mut engine, &mut locator, children.iter().copied()) -} - -/// Convert children into HTML nodes. -fn handle_list<'a>( - engine: &mut Engine, - locator: &mut SplitLocator, - children: impl IntoIterator>, -) -> SourceResult> { - let mut output = Vec::new(); - for (child, styles) in children { - handle(engine, child, locator, styles, &mut output)?; - } - Ok(output) -} - -/// Convert a child into HTML node(s). -fn handle( - engine: &mut Engine, - child: &Content, - locator: &mut SplitLocator, - styles: StyleChain, - output: &mut Vec, -) -> SourceResult<()> { - if let Some(elem) = child.to_packed::() { - 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.get_ref(styles) { - children = html_fragment(engine, body, locator.next(&elem.span()), styles)?; - } - let element = HtmlElement { - tag: elem.tag, - attrs: elem.attrs.get_cloned(styles), - children, - span: elem.span(), - }; - output.push(element.into()); - } else if let Some(elem) = child.to_packed::() { - let children = - html_fragment(engine, &elem.body, locator.next(&elem.span()), styles)?; - output.push( - HtmlElement::new(tag::p) - .with_children(children) - .spanned(elem.span()) - .into(), - ); - } else if let Some(elem) = child.to_packed::() { - // TODO: This is rather incomplete. - if let Some(body) = elem.body.get_ref(styles) { - let children = - html_fragment(engine, body, locator.next(&elem.span()), styles)?; - output.push( - HtmlElement::new(tag::span) - .with_attr(attr::style, "display: inline-block;") - .with_children(children) - .spanned(elem.span()) - .into(), - ) - } - } else if let Some((elem, body)) = - child - .to_packed::() - .and_then(|elem| match elem.body.get_ref(styles) { - Some(BlockBody::Content(body)) => Some((elem, body)), - _ => None, - }) - { - // TODO: This is rather incomplete. - let children = html_fragment(engine, body, locator.next(&elem.span()), styles)?; - output.push( - HtmlElement::new(tag::div) - .with_children(children) - .spanned(elem.span()) - .into(), - ); - } else if child.is::() { - output.push(HtmlNode::text(' ', child.span())); - } else if let Some(elem) = child.to_packed::() { - output.push(HtmlNode::text(elem.text.clone(), elem.span())); - } else if let Some(elem) = child.to_packed::() { - 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.get(styles) { '"' } else { '\'' }, - child.span(), - )); - } else if let Some(elem) = child.to_packed::() { - let locator = locator.next(&elem.span()); - let style = TargetElem::target.set(Target::Paged).wrap(); - let frame = (engine.routines.layout_frame)( - engine, - &elem.body, - locator, - styles.chain(&style), - Region::new(Size::splat(Abs::inf()), Axes::splat(false)), - )?; - output.push(HtmlNode::Frame(HtmlFrame { - inner: frame, - text_size: styles.resolve(TextElem::size), - })); - } else { - engine.sink.warn(warning!( - child.span(), - "{} was ignored during HTML export", - child.elem().name() - )); - } - Ok(()) -} - -/// Wrap the nodes in `` and `` if they are not yet rooted, -/// supplying a suitable ``. -fn root_element(output: Vec, info: &DocumentInfo) -> SourceResult { - let head = head_element(info); - let body = match classify_output(output)? { - OutputKind::Html(element) => return Ok(element), - OutputKind::Body(body) => body, - OutputKind::Leafs(leafs) => HtmlElement::new(tag::body).with_children(leafs), - }; - Ok(HtmlElement::new(tag::html).with_children(vec![head.into(), body.into()])) -} - -/// Generate a `` element. -fn head_element(info: &DocumentInfo) -> HtmlElement { - let mut children = vec![]; - - children.push(HtmlElement::new(tag::meta).with_attr(attr::charset, "utf-8").into()); - - children.push( - HtmlElement::new(tag::meta) - .with_attr(attr::name, "viewport") - .with_attr(attr::content, "width=device-width, initial-scale=1") - .into(), - ); - - if let Some(title) = &info.title { - children.push( - HtmlElement::new(tag::title) - .with_children(vec![HtmlNode::Text(title.clone(), Span::detached())]) - .into(), - ); +impl HtmlElem { + /// Add an attribute to the element. + pub fn with_attr(mut self, attr: HtmlAttr, value: impl Into) -> Self { + self.attrs + .as_option_mut() + .get_or_insert_with(Default::default) + .push(attr, value); + self } - if let Some(description) = &info.description { - children.push( - HtmlElement::new(tag::meta) - .with_attr(attr::name, "description") - .with_attr(attr::content, description.clone()) - .into(), - ); - } - - if !info.author.is_empty() { - children.push( - HtmlElement::new(tag::meta) - .with_attr(attr::name, "authors") - .with_attr(attr::content, info.author.join(", ")) - .into(), - ) - } - - if !info.keywords.is_empty() { - children.push( - HtmlElement::new(tag::meta) - .with_attr(attr::name, "keywords") - .with_attr(attr::content, info.keywords.join(", ")) - .into(), - ) - } - - HtmlElement::new(tag::head).with_children(children) -} - -/// Determine which kind of output the user generated. -fn classify_output(mut output: Vec) -> SourceResult { - let count = output.iter().filter(|node| !matches!(node, HtmlNode::Tag(_))).count(); - for node in &mut output { - let HtmlNode::Element(elem) = node else { continue }; - let tag = elem.tag; - let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html)); - match (tag, count) { - (tag::html, 1) => return Ok(OutputKind::Html(take())), - (tag::body, 1) => return Ok(OutputKind::Body(take())), - (tag::html | tag::body, _) => bail!( - elem.span, - "`{}` element must be the only element in the document", - elem.tag, - ), - _ => {} + /// Adds CSS styles to an element. + fn with_styles(self, properties: css::Properties) -> Self { + if let Some(value) = properties.into_inline_styles() { + self.with_attr(attr::style, value) + } else { + self } } - Ok(OutputKind::Leafs(output)) } -/// What kinds of output the user generated. -enum OutputKind { - /// The user generated their own `` element. We do not need to supply - /// one. - Html(HtmlElement), - /// The user generate their own `` element. We do not need to supply - /// one, but need supply the `` element. - Body(HtmlElement), - /// The user generated leafs which we wrap in a `` and ``. - Leafs(Vec), +/// An element that lays out its content as an inline SVG. +/// +/// Sometimes, converting Typst content to HTML is not desirable. This can be +/// the case for plots and other content that relies on positioning and styling +/// to convey its message. +/// +/// This function allows you to use the Typst layout engine that would also be +/// used for PDF, SVG, and PNG export to render a part of your document exactly +/// how it would appear when exported in one of these formats. It embeds the +/// content as an inline SVG. +#[elem] +pub struct FrameElem { + /// The content that shall be laid out. + #[positional] + #[required] + pub body: Content, } diff --git a/crates/typst-html/src/rules.rs b/crates/typst-html/src/rules.rs new file mode 100644 index 000000000..04a58ca47 --- /dev/null +++ b/crates/typst-html/src/rules.rs @@ -0,0 +1,454 @@ +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::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::{attr, css, tag, FrameElem, HtmlAttrs, HtmlElem, HtmlTag}; + +/// Registers show rules for the [HTML target](Target::Html). +pub fn register(rules: &mut NativeRuleMap) { + use Target::{Html, Paged}; + + // 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); + + // For the HTML target, `html.frame` is a primitive. In the laid-out target, + // it should be a no-op so that nested frames don't break (things like `show + // math.equation: html.frame` can result in nested ones). + rules.register::(Paged, |elem, _, _| Ok(elem.body.clone())); +} + +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-html/src/tag.rs b/crates/typst-html/src/tag.rs new file mode 100644 index 000000000..89c50e1a8 --- /dev/null +++ b/crates/typst-html/src/tag.rs @@ -0,0 +1,271 @@ +//! Predefined constants for HTML tags. + +#![allow(non_upper_case_globals)] +#![allow(dead_code)] + +use crate::HtmlTag; + +pub const a: HtmlTag = HtmlTag::constant("a"); +pub const abbr: HtmlTag = HtmlTag::constant("abbr"); +pub const address: HtmlTag = HtmlTag::constant("address"); +pub const area: HtmlTag = HtmlTag::constant("area"); +pub const article: HtmlTag = HtmlTag::constant("article"); +pub const aside: HtmlTag = HtmlTag::constant("aside"); +pub const audio: HtmlTag = HtmlTag::constant("audio"); +pub const b: HtmlTag = HtmlTag::constant("b"); +pub const base: HtmlTag = HtmlTag::constant("base"); +pub const bdi: HtmlTag = HtmlTag::constant("bdi"); +pub const bdo: HtmlTag = HtmlTag::constant("bdo"); +pub const blockquote: HtmlTag = HtmlTag::constant("blockquote"); +pub const body: HtmlTag = HtmlTag::constant("body"); +pub const br: HtmlTag = HtmlTag::constant("br"); +pub const button: HtmlTag = HtmlTag::constant("button"); +pub const canvas: HtmlTag = HtmlTag::constant("canvas"); +pub const caption: HtmlTag = HtmlTag::constant("caption"); +pub const cite: HtmlTag = HtmlTag::constant("cite"); +pub const code: HtmlTag = HtmlTag::constant("code"); +pub const col: HtmlTag = HtmlTag::constant("col"); +pub const colgroup: HtmlTag = HtmlTag::constant("colgroup"); +pub const data: HtmlTag = HtmlTag::constant("data"); +pub const datalist: HtmlTag = HtmlTag::constant("datalist"); +pub const dd: HtmlTag = HtmlTag::constant("dd"); +pub const del: HtmlTag = HtmlTag::constant("del"); +pub const details: HtmlTag = HtmlTag::constant("details"); +pub const dfn: HtmlTag = HtmlTag::constant("dfn"); +pub const dialog: HtmlTag = HtmlTag::constant("dialog"); +pub const div: HtmlTag = HtmlTag::constant("div"); +pub const dl: HtmlTag = HtmlTag::constant("dl"); +pub const dt: HtmlTag = HtmlTag::constant("dt"); +pub const em: HtmlTag = HtmlTag::constant("em"); +pub const embed: HtmlTag = HtmlTag::constant("embed"); +pub const fieldset: HtmlTag = HtmlTag::constant("fieldset"); +pub const figcaption: HtmlTag = HtmlTag::constant("figcaption"); +pub const figure: HtmlTag = HtmlTag::constant("figure"); +pub const footer: HtmlTag = HtmlTag::constant("footer"); +pub const form: HtmlTag = HtmlTag::constant("form"); +pub const h1: HtmlTag = HtmlTag::constant("h1"); +pub const h2: HtmlTag = HtmlTag::constant("h2"); +pub const h3: HtmlTag = HtmlTag::constant("h3"); +pub const h4: HtmlTag = HtmlTag::constant("h4"); +pub const h5: HtmlTag = HtmlTag::constant("h5"); +pub const h6: HtmlTag = HtmlTag::constant("h6"); +pub const head: HtmlTag = HtmlTag::constant("head"); +pub const header: HtmlTag = HtmlTag::constant("header"); +pub const hgroup: HtmlTag = HtmlTag::constant("hgroup"); +pub const hr: HtmlTag = HtmlTag::constant("hr"); +pub const html: HtmlTag = HtmlTag::constant("html"); +pub const i: HtmlTag = HtmlTag::constant("i"); +pub const iframe: HtmlTag = HtmlTag::constant("iframe"); +pub const img: HtmlTag = HtmlTag::constant("img"); +pub const input: HtmlTag = HtmlTag::constant("input"); +pub const ins: HtmlTag = HtmlTag::constant("ins"); +pub const kbd: HtmlTag = HtmlTag::constant("kbd"); +pub const label: HtmlTag = HtmlTag::constant("label"); +pub const legend: HtmlTag = HtmlTag::constant("legend"); +pub const li: HtmlTag = HtmlTag::constant("li"); +pub const link: HtmlTag = HtmlTag::constant("link"); +pub const main: HtmlTag = HtmlTag::constant("main"); +pub const map: HtmlTag = HtmlTag::constant("map"); +pub const mark: HtmlTag = HtmlTag::constant("mark"); +pub const menu: HtmlTag = HtmlTag::constant("menu"); +pub const meta: HtmlTag = HtmlTag::constant("meta"); +pub const meter: HtmlTag = HtmlTag::constant("meter"); +pub const nav: HtmlTag = HtmlTag::constant("nav"); +pub const noscript: HtmlTag = HtmlTag::constant("noscript"); +pub const object: HtmlTag = HtmlTag::constant("object"); +pub const ol: HtmlTag = HtmlTag::constant("ol"); +pub const optgroup: HtmlTag = HtmlTag::constant("optgroup"); +pub const option: HtmlTag = HtmlTag::constant("option"); +pub const output: HtmlTag = HtmlTag::constant("output"); +pub const p: HtmlTag = HtmlTag::constant("p"); +pub const picture: HtmlTag = HtmlTag::constant("picture"); +pub const pre: HtmlTag = HtmlTag::constant("pre"); +pub const progress: HtmlTag = HtmlTag::constant("progress"); +pub const q: HtmlTag = HtmlTag::constant("q"); +pub const rp: HtmlTag = HtmlTag::constant("rp"); +pub const rt: HtmlTag = HtmlTag::constant("rt"); +pub const ruby: HtmlTag = HtmlTag::constant("ruby"); +pub const s: HtmlTag = HtmlTag::constant("s"); +pub const samp: HtmlTag = HtmlTag::constant("samp"); +pub const script: HtmlTag = HtmlTag::constant("script"); +pub const search: HtmlTag = HtmlTag::constant("search"); +pub const section: HtmlTag = HtmlTag::constant("section"); +pub const select: HtmlTag = HtmlTag::constant("select"); +pub const slot: HtmlTag = HtmlTag::constant("slot"); +pub const small: HtmlTag = HtmlTag::constant("small"); +pub const source: HtmlTag = HtmlTag::constant("source"); +pub const span: HtmlTag = HtmlTag::constant("span"); +pub const strong: HtmlTag = HtmlTag::constant("strong"); +pub const style: HtmlTag = HtmlTag::constant("style"); +pub const sub: HtmlTag = HtmlTag::constant("sub"); +pub const summary: HtmlTag = HtmlTag::constant("summary"); +pub const sup: HtmlTag = HtmlTag::constant("sup"); +pub const table: HtmlTag = HtmlTag::constant("table"); +pub const tbody: HtmlTag = HtmlTag::constant("tbody"); +pub const td: HtmlTag = HtmlTag::constant("td"); +pub const template: HtmlTag = HtmlTag::constant("template"); +pub const textarea: HtmlTag = HtmlTag::constant("textarea"); +pub const tfoot: HtmlTag = HtmlTag::constant("tfoot"); +pub const th: HtmlTag = HtmlTag::constant("th"); +pub const thead: HtmlTag = HtmlTag::constant("thead"); +pub const time: HtmlTag = HtmlTag::constant("time"); +pub const title: HtmlTag = HtmlTag::constant("title"); +pub const tr: HtmlTag = HtmlTag::constant("tr"); +pub const track: HtmlTag = HtmlTag::constant("track"); +pub const u: HtmlTag = HtmlTag::constant("u"); +pub const ul: HtmlTag = HtmlTag::constant("ul"); +pub const var: HtmlTag = HtmlTag::constant("var"); +pub const video: HtmlTag = HtmlTag::constant("video"); +pub const wbr: HtmlTag = HtmlTag::constant("wbr"); + +/// Whether this is a void tag whose associated element may not have +/// children. +pub fn is_void(tag: HtmlTag) -> bool { + matches!( + tag, + self::area + | self::base + | self::br + | self::col + | self::embed + | self::hr + | self::img + | self::input + | self::link + | self::meta + | self::source + | self::track + | self::wbr + ) +} + +/// Whether this is a tag containing raw text. +pub fn is_raw(tag: HtmlTag) -> bool { + matches!(tag, self::script | self::style) +} + +/// Whether this is a tag containing escapable raw text. +pub fn is_escapable_raw(tag: HtmlTag) -> bool { + matches!(tag, self::textarea | self::title) +} + +/// Whether an element is considered metadata. +pub fn is_metadata(tag: HtmlTag) -> bool { + matches!( + tag, + self::base + | self::link + | self::meta + | self::noscript + | self::script + | self::style + | self::template + | self::title + ) +} + +/// Whether nodes with the tag have the CSS property `display: block` by +/// default. +pub fn is_block_by_default(tag: HtmlTag) -> bool { + matches!( + tag, + self::html + | self::head + | self::body + | self::article + | self::aside + | self::h1 + | self::h2 + | self::h3 + | self::h4 + | self::h5 + | self::h6 + | self::hgroup + | self::nav + | self::section + | self::dd + | self::dl + | self::dt + | self::menu + | self::ol + | self::ul + | self::address + | self::blockquote + | self::dialog + | self::div + | self::fieldset + | self::figure + | self::figcaption + | self::footer + | self::form + | self::header + | self::hr + | self::legend + | self::main + | self::p + | self::pre + | self::search + ) +} + +/// Whether the element is inline-level as opposed to being block-level. +/// +/// Not sure whether this distinction really makes sense. But we somehow +/// need to decide what to put into automatic paragraphs. A `` +/// should merged into a paragraph created by realization, but a `
` +/// shouldn't. +/// +/// +/// +/// +pub fn is_inline_by_default(tag: HtmlTag) -> bool { + matches!( + tag, + self::abbr + | self::a + | self::bdi + | self::b + | self::br + | self::bdo + | self::code + | self::cite + | self::dfn + | self::data + | self::i + | self::em + | self::mark + | self::kbd + | self::rp + | self::q + | self::ruby + | self::rt + | self::samp + | self::s + | self::span + | self::small + | self::sub + | self::strong + | self::time + | self::sup + | self::var + | self::u + ) +} + +/// Whether nodes with the tag have the CSS property `display: table(-.*)?` +/// by default. +pub fn is_tabular_by_default(tag: HtmlTag) -> bool { + matches!( + tag, + self::table + | self::thead + | self::tbody + | self::tfoot + | self::tr + | self::th + | self::td + | self::caption + | self::col + | self::colgroup + ) +} 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..190ff4f16 100644 --- a/crates/typst-library/src/html/typed.rs +++ b/crates/typst-html/src/typed.rs @@ -11,19 +11,18 @@ 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::layout::{Axes, Axis, Dir, Length}; +use typst_library::visualize::Color; +use typst_macros::cast; + +use crate::{css, tag, HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag}; /// Hook up all typed HTML definitions. pub(super) fn define(html: &mut Scope) { @@ -705,153 +704,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..e9fb8a7d7 100644 --- a/crates/typst-ide/src/analyze.rs +++ b/crates/typst-ide/src/analyze.rs @@ -1,8 +1,10 @@ +use std::collections::HashSet; + 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; @@ -66,17 +68,30 @@ pub fn analyze_import(world: &dyn IdeWorld, source: &LinkedNode) -> Option (Vec<(Label, Option)>, usize) { let mut output = vec![]; + let mut seen_labels = HashSet::new(); // Labels in the document. for elem in document.introspector.all() { let Some(label) = elem.label() else { continue }; + if !seen_labels.insert(label) { + 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/complete.rs b/crates/typst-ide/src/complete.rs index bc5b3e10e..5b6d6fd97 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -76,7 +76,7 @@ pub struct Completion { } /// A kind of item that can be completed. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum CompletionKind { /// A syntactical structure. @@ -130,7 +130,14 @@ fn complete_markup(ctx: &mut CompletionContext) -> bool { return true; } - // Start of a reference: "@|" or "@he|". + // Start of a reference: "@|". + if ctx.leaf.kind() == SyntaxKind::Text && ctx.before.ends_with("@") { + ctx.from = ctx.cursor; + ctx.label_completions(); + return true; + } + + // An existing reference: "@he|". if ctx.leaf.kind() == SyntaxKind::RefMarker { ctx.from = ctx.leaf.offset() + 1; ctx.label_completions(); @@ -1564,7 +1571,7 @@ mod tests { use typst::layout::PagedDocument; - use super::{autocomplete, Completion}; + use super::{autocomplete, Completion, CompletionKind}; use crate::tests::{FilePos, TestWorld, WorldLike}; /// Quote a string. @@ -1644,6 +1651,19 @@ mod tests { test_with_doc(world, pos, doc.as_ref()) } + #[track_caller] + fn test_with_addition( + initial_text: &str, + addition: &str, + pos: impl FilePos, + ) -> Response { + let mut world = TestWorld::new(initial_text); + let doc = typst::compile(&world).output.ok(); + let end = world.main.text().len(); + world.main.edit(end..end, addition); + test_with_doc(&world, pos, doc.as_ref()) + } + #[track_caller] fn test_with_doc( world: impl WorldLike, @@ -1709,6 +1729,30 @@ mod tests { .must_exclude(["bib"]); } + #[test] + fn test_autocomplete_ref_function() { + test_with_addition("x", " #ref(<)", -2).must_include(["test"]); + } + + #[test] + fn test_autocomplete_ref_shorthand() { + test_with_addition("x", " @", -1).must_include(["test"]); + } + + #[test] + fn test_autocomplete_ref_shorthand_with_partial_identifier() { + test_with_addition("x", " @te", -1).must_include(["test"]); + } + + #[test] + fn test_autocomplete_ref_identical_labels_returns_single_completion() { + let result = test_with_addition("x y", " @t", -1); + let completions = result.completions(); + let label_count = + completions.iter().filter(|c| c.kind == CompletionKind::Label).count(); + assert_eq!(label_count, 1); + } + /// Test what kind of brackets we autocomplete for function calls depending /// on the function and existing parens. #[test] diff --git a/crates/typst-ide/src/tests.rs b/crates/typst-ide/src/tests.rs index b3f368f2e..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; 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-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/mod.rs b/crates/typst-layout/src/flow/mod.rs index f4f1c0915..cb029dce8 100644 --- a/crates/typst-layout/src/flow/mod.rs +++ b/crates/typst-layout/src/flow/mod.rs @@ -143,7 +143,7 @@ fn layout_fragment_impl( let mut kind = FragmentKind::Block; let arenas = Arenas::default(); let children = (engine.routines.realize)( - RealizationKind::LayoutFragment(&mut kind), + RealizationKind::LayoutFragment { kind: &mut kind }, &mut engine, &mut locator, &arenas, diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index 261a58fa3..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.get(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.get_ref(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.get_cloned(styles), elem.scaling.get(styles)); + let image = elem.decode(engine, styles)?; // Determine the image's pixel aspect ratio. let pxw = image.width(); @@ -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/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/math/text.rs b/crates/typst-layout/src/math/text.rs index 53f88f2b6..634969cd4 100644 --- a/crates/typst-layout/src/math/text.rs +++ b/crates/typst-layout/src/math/text.rs @@ -1,10 +1,11 @@ use std::f64::consts::SQRT_2; +use codex::styling::{to_style, MathStyle}; use ecow::EcoString; use typst_library::diag::SourceResult; use typst_library::foundations::{Packed, StyleChain, SymbolElem}; use typst_library::layout::{Abs, Size}; -use typst_library::math::{EquationElem, MathSize, MathVariant}; +use typst_library::math::{EquationElem, MathSize}; use typst_library::text::{ BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric, }; @@ -64,12 +65,21 @@ fn layout_inline_text( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult { + let variant = styles.get(EquationElem::variant); + let bold = styles.get(EquationElem::bold); + // Disable auto-italic. + let italic = styles.get(EquationElem::italic).or(Some(false)); + if text.chars().all(|c| c.is_ascii_digit() || c == '.') { // Small optimization for numbers. Note that this lays out slightly // differently to normal text and is worth re-evaluating in the future. let mut fragments = vec![]; for unstyled_c in text.chars() { - let c = styled_char(styles, unstyled_c, false); + // This is fine as ascii digits and '.' can never end up as more + // than a single char after styling. + let style = MathStyle::select(unstyled_c, variant, bold, italic); + let c = to_style(unstyled_c, style).next().unwrap(); + let glyph = GlyphFragment::new_char(ctx.font, styles, c, span)?; fragments.push(glyph.into()); } @@ -83,8 +93,10 @@ fn layout_inline_text( .map(|p| p.wrap()); let styles = styles.chain(&local); - let styled_text: EcoString = - text.chars().map(|c| styled_char(styles, c, false)).collect(); + let styled_text: EcoString = text + .chars() + .flat_map(|c| to_style(c, MathStyle::select(c, variant, bold, italic))) + .collect(); let spaced = styled_text.graphemes(true).nth(1).is_some(); let elem = TextElem::packed(styled_text).spanned(span); @@ -124,9 +136,16 @@ pub fn layout_symbol( Some(c) if has_dtls_feat(ctx.font) => (c, styles.chain(&dtls)), _ => (elem.text, styles), }; - let c = styled_char(styles, unstyled_c, true); + + let variant = styles.get(EquationElem::variant); + let bold = styles.get(EquationElem::bold); + let italic = styles.get(EquationElem::italic); + + let style = MathStyle::select(unstyled_c, variant, bold, italic); + let text: EcoString = to_style(unstyled_c, style).collect(); + let fragment: MathFragment = - match GlyphFragment::new_char(ctx.font, symbol_styles, c, elem.span()) { + match GlyphFragment::new(ctx.font, symbol_styles, &text, elem.span()) { Ok(mut glyph) => { adjust_glyph_layout(&mut glyph, ctx, styles); glyph.into() @@ -134,8 +153,7 @@ pub fn layout_symbol( Err(_) => { // Not in the math font, fallback to normal inline text layout. // TODO: Should replace this with proper fallback in [`GlyphFragment::new`]. - layout_inline_text(c.encode_utf8(&mut [0; 4]), elem.span(), ctx, styles)? - .into() + layout_inline_text(&text, elem.span(), ctx, styles)?.into() } }; ctx.push(fragment); @@ -161,226 +179,6 @@ fn adjust_glyph_layout( } } -/// Style the character by selecting the unicode codepoint for italic, bold, -/// caligraphic, etc. -/// -/// -/// -fn styled_char(styles: StyleChain, c: char, auto_italic: bool) -> char { - use MathVariant::*; - - let variant = styles.get(EquationElem::variant); - let bold = styles.get(EquationElem::bold); - let italic = styles.get(EquationElem::italic).unwrap_or( - auto_italic - && matches!( - c, - 'a'..='z' | 'ħ' | 'ı' | 'ȷ' | 'A'..='Z' | - 'α'..='ω' | '∂' | 'ϵ' | 'ϑ' | 'ϰ' | 'ϕ' | 'ϱ' | 'ϖ' - ) - && matches!(variant, Sans | Serif), - ); - - if let Some(c) = basic_exception(c) { - return c; - } - - if let Some(c) = latin_exception(c, variant, bold, italic) { - return c; - } - - if let Some(c) = greek_exception(c, variant, bold, italic) { - return c; - } - - let base = match c { - 'A'..='Z' => 'A', - 'a'..='z' => 'a', - 'Α'..='Ω' => 'Α', - 'α'..='ω' => 'α', - '0'..='9' => '0', - // Hebrew Alef -> Dalet. - '\u{05D0}'..='\u{05D3}' => '\u{05D0}', - _ => return c, - }; - - let tuple = (variant, bold, italic); - let start = match c { - // Latin upper. - 'A'..='Z' => match tuple { - (Serif, false, false) => 0x0041, - (Serif, true, false) => 0x1D400, - (Serif, false, true) => 0x1D434, - (Serif, true, true) => 0x1D468, - (Sans, false, false) => 0x1D5A0, - (Sans, true, false) => 0x1D5D4, - (Sans, false, true) => 0x1D608, - (Sans, true, true) => 0x1D63C, - (Cal, false, _) => 0x1D49C, - (Cal, true, _) => 0x1D4D0, - (Frak, false, _) => 0x1D504, - (Frak, true, _) => 0x1D56C, - (Mono, _, _) => 0x1D670, - (Bb, _, _) => 0x1D538, - }, - - // Latin lower. - 'a'..='z' => match tuple { - (Serif, false, false) => 0x0061, - (Serif, true, false) => 0x1D41A, - (Serif, false, true) => 0x1D44E, - (Serif, true, true) => 0x1D482, - (Sans, false, false) => 0x1D5BA, - (Sans, true, false) => 0x1D5EE, - (Sans, false, true) => 0x1D622, - (Sans, true, true) => 0x1D656, - (Cal, false, _) => 0x1D4B6, - (Cal, true, _) => 0x1D4EA, - (Frak, false, _) => 0x1D51E, - (Frak, true, _) => 0x1D586, - (Mono, _, _) => 0x1D68A, - (Bb, _, _) => 0x1D552, - }, - - // Greek upper. - 'Α'..='Ω' => match tuple { - (Serif, false, false) => 0x0391, - (Serif, true, false) => 0x1D6A8, - (Serif, false, true) => 0x1D6E2, - (Serif, true, true) => 0x1D71C, - (Sans, _, false) => 0x1D756, - (Sans, _, true) => 0x1D790, - (Cal | Frak | Mono | Bb, _, _) => return c, - }, - - // Greek lower. - 'α'..='ω' => match tuple { - (Serif, false, false) => 0x03B1, - (Serif, true, false) => 0x1D6C2, - (Serif, false, true) => 0x1D6FC, - (Serif, true, true) => 0x1D736, - (Sans, _, false) => 0x1D770, - (Sans, _, true) => 0x1D7AA, - (Cal | Frak | Mono | Bb, _, _) => return c, - }, - - // Hebrew Alef -> Dalet. - '\u{05D0}'..='\u{05D3}' => 0x2135, - - // Numbers. - '0'..='9' => match tuple { - (Serif, false, _) => 0x0030, - (Serif, true, _) => 0x1D7CE, - (Bb, _, _) => 0x1D7D8, - (Sans, false, _) => 0x1D7E2, - (Sans, true, _) => 0x1D7EC, - (Mono, _, _) => 0x1D7F6, - (Cal | Frak, _, _) => return c, - }, - - _ => unreachable!(), - }; - - std::char::from_u32(start + (c as u32 - base as u32)).unwrap() -} - -fn basic_exception(c: char) -> Option { - Some(match c { - '〈' => '⟨', - '〉' => '⟩', - '《' => '⟪', - '》' => '⟫', - _ => return None, - }) -} - -fn latin_exception( - c: char, - variant: MathVariant, - bold: bool, - italic: bool, -) -> Option { - use MathVariant::*; - Some(match (c, variant, bold, italic) { - ('B', Cal, false, _) => 'ℬ', - ('E', Cal, false, _) => 'ℰ', - ('F', Cal, false, _) => 'ℱ', - ('H', Cal, false, _) => 'ℋ', - ('I', Cal, false, _) => 'ℐ', - ('L', Cal, false, _) => 'ℒ', - ('M', Cal, false, _) => 'ℳ', - ('R', Cal, false, _) => 'ℛ', - ('C', Frak, false, _) => 'ℭ', - ('H', Frak, false, _) => 'ℌ', - ('I', Frak, false, _) => 'ℑ', - ('R', Frak, false, _) => 'ℜ', - ('Z', Frak, false, _) => 'ℨ', - ('C', Bb, ..) => 'ℂ', - ('H', Bb, ..) => 'ℍ', - ('N', Bb, ..) => 'ℕ', - ('P', Bb, ..) => 'ℙ', - ('Q', Bb, ..) => 'ℚ', - ('R', Bb, ..) => 'ℝ', - ('Z', Bb, ..) => 'ℤ', - ('D', Bb, _, true) => 'ⅅ', - ('d', Bb, _, true) => 'ⅆ', - ('e', Bb, _, true) => 'ⅇ', - ('i', Bb, _, true) => 'ⅈ', - ('j', Bb, _, true) => 'ⅉ', - ('h', Serif, false, true) => 'ℎ', - ('e', Cal, false, _) => 'ℯ', - ('g', Cal, false, _) => 'ℊ', - ('o', Cal, false, _) => 'ℴ', - ('ħ', Serif, .., true) => 'ℏ', - ('ı', Serif, .., true) => '𝚤', - ('ȷ', Serif, .., true) => '𝚥', - _ => return None, - }) -} - -fn greek_exception( - c: char, - variant: MathVariant, - bold: bool, - italic: bool, -) -> Option { - use MathVariant::*; - if c == 'Ϝ' && variant == Serif && bold { - return Some('𝟊'); - } - if c == 'ϝ' && variant == Serif && bold { - return Some('𝟋'); - } - - let list = match c { - 'ϴ' => ['𝚹', '𝛳', '𝜭', '𝝧', '𝞡', 'ϴ'], - '∇' => ['𝛁', '𝛻', '𝜵', '𝝯', '𝞩', '∇'], - '∂' => ['𝛛', '𝜕', '𝝏', '𝞉', '𝟃', '∂'], - 'ϵ' => ['𝛜', '𝜖', '𝝐', '𝞊', '𝟄', 'ϵ'], - 'ϑ' => ['𝛝', '𝜗', '𝝑', '𝞋', '𝟅', 'ϑ'], - 'ϰ' => ['𝛞', '𝜘', '𝝒', '𝞌', '𝟆', 'ϰ'], - 'ϕ' => ['𝛟', '𝜙', '𝝓', '𝞍', '𝟇', 'ϕ'], - 'ϱ' => ['𝛠', '𝜚', '𝝔', '𝞎', '𝟈', 'ϱ'], - 'ϖ' => ['𝛡', '𝜛', '𝝕', '𝞏', '𝟉', 'ϖ'], - 'Γ' => ['𝚪', '𝛤', '𝜞', '𝝘', '𝞒', 'ℾ'], - 'γ' => ['𝛄', '𝛾', '𝜸', '𝝲', '𝞬', 'ℽ'], - 'Π' => ['𝚷', '𝛱', '𝜫', '𝝥', '𝞟', 'ℿ'], - 'π' => ['𝛑', '𝜋', '𝝅', '𝝿', '𝞹', 'ℼ'], - '∑' => ['∑', '∑', '∑', '∑', '∑', '⅀'], - _ => return None, - }; - - Some(match (variant, bold, italic) { - (Serif, true, false) => list[0], - (Serif, false, true) => list[1], - (Serif, true, true) => list[2], - (Sans, _, false) => list[3], - (Sans, _, true) => list[4], - (Bb, ..) => list[5], - _ => return None, - }) -} - /// The non-dotless version of a dotless character that can be used with the /// `dtls` OpenType feature. pub fn try_dotless(c: char) -> Option { diff --git a/crates/typst-layout/src/pages/mod.rs b/crates/typst-layout/src/pages/mod.rs index 14dc0f3fb..ec0dc2c05 100644 --- a/crates/typst-layout/src/pages/mod.rs +++ b/crates/typst-layout/src/pages/mod.rs @@ -4,14 +4,16 @@ mod collect; mod finalize; mod run; +use std::num::NonZeroUsize; + use comemo::{Tracked, TrackedMut}; use typst_library::diag::SourceResult; use typst_library::engine::{Engine, Route, Sink, Traced}; use typst_library::foundations::{Content, StyleChain}; use typst_library::introspection::{ - Introspector, Locator, ManualPageCounter, SplitLocator, TagElem, + Introspector, IntrospectorBuilder, Locator, ManualPageCounter, SplitLocator, TagElem, }; -use typst_library::layout::{FrameItem, Page, PagedDocument, Point}; +use typst_library::layout::{FrameItem, Page, PagedDocument, Point, Transform}; use typst_library::model::DocumentInfo; use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; use typst_library::World; @@ -75,7 +77,7 @@ fn layout_document_impl( let arenas = Arenas::default(); let mut info = DocumentInfo::default(); let mut children = (engine.routines.realize)( - RealizationKind::LayoutDocument(&mut info), + RealizationKind::LayoutDocument { info: &mut info }, &mut engine, &mut locator, &arenas, @@ -84,7 +86,7 @@ fn layout_document_impl( )?; let pages = layout_pages(&mut engine, &mut children, &mut locator, styles)?; - let introspector = Introspector::paged(&pages); + let introspector = introspect_pages(&pages); Ok(PagedDocument { pages, info, introspector }) } @@ -157,3 +159,27 @@ fn layout_pages<'a>( Ok(pages) } + +/// Introspects pages. +#[typst_macros::time(name = "introspect pages")] +fn introspect_pages(pages: &[Page]) -> Introspector { + let mut builder = IntrospectorBuilder::new(); + builder.pages = pages.len(); + builder.page_numberings.reserve(pages.len()); + builder.page_supplements.reserve(pages.len()); + + // Discover all elements. + let mut elems = Vec::new(); + for (i, page) in pages.iter().enumerate() { + builder.page_numberings.push(page.numbering.clone()); + builder.page_supplements.push(page.supplement.clone()); + builder.discover_in_frame( + &mut elems, + &page.frame, + NonZeroUsize::new(1 + i).unwrap(), + Transform::identity(), + ); + } + + builder.finalize(elems) +} diff --git a/crates/typst-layout/src/rules.rs b/crates/typst-layout/src/rules.rs new file mode 100644 index 000000000..97c8c11ea --- /dev/null +++ b/crates/typst-layout/src/rules.rs @@ -0,0 +1,933 @@ +use std::num::NonZeroUsize; + +use comemo::Track; +use ecow::{eco_format, EcoVec}; +use smallvec::smallvec; +use typst_library::diag::{bail, At, SourceResult}; +use typst_library::foundations::{ + dict, Content, Context, LinkMarker, NativeElement, NativeRuleMap, Packed, Resolve, + ShowFn, Smart, StyleChain, Target, +}; +use typst_library::introspection::{Counter, Locator, LocatorLink}; +use typst_library::layout::{ + Abs, AlignElem, Alignment, Axes, BlockBody, BlockElem, ColumnsElem, Em, GridCell, + GridChild, GridElem, GridItem, HAlignment, HElem, HideElem, InlineElem, LayoutElem, + Length, MoveElem, OuterVAlignment, PadElem, PageElem, PlaceElem, PlacementScope, + Region, Rel, RepeatElem, RotateElem, ScaleElem, Sides, Size, Sizing, SkewElem, + Spacing, StackChild, StackElem, TrackSizings, VElem, +}; +use typst_library::math::EquationElem; +use typst_library::model::{ + Attribution, BibliographyElem, CiteElem, CiteGroup, CslSource, Destination, EmphElem, + EnumElem, FigureCaption, FigureElem, FootnoteElem, FootnoteEntry, HeadingElem, + LinkElem, LinkTarget, ListElem, Outlinable, OutlineBody, OutlineElem, OutlineEntry, + ParElem, ParbreakElem, QuoteElem, RefElem, StrongElem, TableCell, TableElem, + TermsElem, Works, +}; +use typst_library::pdf::{ArtifactElem, EmbedElem, PdfTagElem}; +use typst_library::text::{ + DecoLine, Decoration, HighlightElem, ItalicToggle, LinebreakElem, LocalName, + OverlineElem, RawElem, RawLine, ScriptKind, ShiftSettings, Smallcaps, SmallcapsElem, + SmartQuoteElem, SmartQuotes, SpaceElem, StrikeElem, SubElem, SuperElem, TextElem, + TextSize, UnderlineElem, WeightDelta, +}; +use typst_library::visualize::{ + CircleElem, CurveElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, + RectElem, SquareElem, Stroke, +}; +use typst_utils::{Get, NonZeroExt, Numeric}; + +/// Register show rules for the [paged target](Target::Paged). +pub fn register(rules: &mut NativeRuleMap) { + use Target::Paged; + + // Model. + rules.register(Paged, STRONG_RULE); + rules.register(Paged, EMPH_RULE); + rules.register(Paged, LIST_RULE); + rules.register(Paged, ENUM_RULE); + rules.register(Paged, TERMS_RULE); + rules.register(Paged, LINK_MARKER_RULE); + rules.register(Paged, LINK_RULE); + rules.register(Paged, HEADING_RULE); + rules.register(Paged, FIGURE_RULE); + rules.register(Paged, FIGURE_CAPTION_RULE); + rules.register(Paged, QUOTE_RULE); + rules.register(Paged, FOOTNOTE_RULE); + rules.register(Paged, FOOTNOTE_ENTRY_RULE); + rules.register(Paged, OUTLINE_RULE); + rules.register(Paged, OUTLINE_BODY_RULE); + rules.register(Paged, OUTLINE_ENTRY_RULE); + rules.register(Paged, REF_RULE); + rules.register(Paged, CITE_GROUP_RULE); + rules.register(Paged, BIBLIOGRAPHY_RULE); + rules.register(Paged, TABLE_RULE); + rules.register(Paged, TABLE_CELL_RULE); + + // Text. + rules.register(Paged, SUB_RULE); + rules.register(Paged, SUPER_RULE); + rules.register(Paged, UNDERLINE_RULE); + rules.register(Paged, OVERLINE_RULE); + rules.register(Paged, STRIKE_RULE); + rules.register(Paged, HIGHLIGHT_RULE); + rules.register(Paged, SMALLCAPS_RULE); + rules.register(Paged, RAW_RULE); + rules.register(Paged, RAW_LINE_RULE); + + // Layout. + rules.register(Paged, ALIGN_RULE); + rules.register(Paged, PAD_RULE); + rules.register(Paged, COLUMNS_RULE); + rules.register(Paged, STACK_RULE); + rules.register(Paged, GRID_RULE); + rules.register(Paged, GRID_CELL_RULE); + rules.register(Paged, MOVE_RULE); + rules.register(Paged, SCALE_RULE); + rules.register(Paged, ROTATE_RULE); + rules.register(Paged, SKEW_RULE); + rules.register(Paged, REPEAT_RULE); + rules.register(Paged, HIDE_RULE); + rules.register(Paged, LAYOUT_RULE); + + // Visualize. + rules.register(Paged, IMAGE_RULE); + rules.register(Paged, LINE_RULE); + rules.register(Paged, RECT_RULE); + rules.register(Paged, SQUARE_RULE); + rules.register(Paged, ELLIPSE_RULE); + rules.register(Paged, CIRCLE_RULE); + rules.register(Paged, POLYGON_RULE); + rules.register(Paged, CURVE_RULE); + rules.register(Paged, PATH_RULE); + + // Math. + rules.register(Paged, EQUATION_RULE); + + // PDF. + rules.register(Paged, EMBED_RULE); + rules.register(Paged, PDF_TAG_RULE); + rules.register(Paged, PDF_ARTIFACT_RULE); +} + +const STRONG_RULE: ShowFn = |elem, _, styles| { + Ok(elem + .body + .clone() + .set(TextElem::delta, WeightDelta(elem.delta.get(styles)))) +}; + +const EMPH_RULE: ShowFn = + |elem, _, _| Ok(elem.body.clone().set(TextElem::emph, ItalicToggle(true))); + +const LIST_RULE: ShowFn = |elem, _, styles| { + let tight = elem.tight.get(styles); + + let mut realized = BlockElem::multi_layouter(elem.clone(), crate::lists::layout_list) + .pack() + .spanned(elem.span()); + + if tight { + let spacing = elem + .spacing + .get(styles) + .unwrap_or_else(|| styles.get(ParElem::leading)); + let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack(); + realized = v + realized; + } + + Ok(realized) +}; + +const ENUM_RULE: ShowFn = |elem, _, styles| { + let tight = elem.tight.get(styles); + + let mut realized = BlockElem::multi_layouter(elem.clone(), crate::lists::layout_enum) + .pack() + .spanned(elem.span()); + + if tight { + let spacing = elem + .spacing + .get(styles) + .unwrap_or_else(|| styles.get(ParElem::leading)); + let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack(); + realized = v + realized; + } + + Ok(realized) +}; + +const TERMS_RULE: ShowFn = |elem, _, styles| { + let span = elem.span(); + let tight = elem.tight.get(styles); + + let separator = elem.separator.get_ref(styles); + let indent = elem.indent.get(styles); + let hanging_indent = elem.hanging_indent.get(styles); + let gutter = elem.spacing.get(styles).unwrap_or_else(|| { + if tight { + styles.get(ParElem::leading) + } else { + styles.get(ParElem::spacing) + } + }); + + let pad = hanging_indent + indent; + let unpad = (!hanging_indent.is_zero()) + .then(|| HElem::new((-hanging_indent).into()).pack().spanned(span)); + + let mut children = vec![]; + for child in elem.children.iter() { + let mut seq = vec![]; + seq.extend(unpad.clone()); + seq.push(child.term.clone().strong()); + seq.push(separator.clone()); + seq.push(child.description.clone()); + + // Text in wide term lists shall always turn into paragraphs. + if !tight { + seq.push(ParbreakElem::shared().clone()); + } + + children.push(StackChild::Block(Content::sequence(seq))); + } + + let padding = + Sides::default().with(styles.resolve(TextElem::dir).start(), pad.into()); + + let mut realized = StackElem::new(children) + .with_spacing(Some(gutter.into())) + .pack() + .spanned(span) + .padded(padding) + .set(TermsElem::within, true); + + if tight { + let spacing = elem + .spacing + .get(styles) + .unwrap_or_else(|| styles.get(ParElem::leading)); + let v = VElem::new(spacing.into()) + .with_weak(true) + .with_attach(true) + .pack() + .spanned(span); + realized = v + realized; + } + + Ok(realized) +}; + +const LINK_MARKER_RULE: ShowFn = |elem, _, _| Ok(elem.body.clone()); + +const LINK_RULE: ShowFn = |elem, engine, styles| { + let body = elem.body.clone(); + // TODO(accessibility): remove custom alt field + let alt = elem.alt.get_cloned(styles); + Ok(match &elem.dest { + LinkTarget::Dest(dest) => { + let url = || dest.as_url().map(|url| url.clone().into_inner()); + body.linked(dest.clone(), alt.or_else(url)) + } + LinkTarget::Label(label) => { + let elem = engine.introspector.query_label(*label).at(elem.span())?; + let dest = Destination::Location(elem.location().unwrap()); + // TODO(accessibility): generate alt text + body.linked(dest, alt) + } + }) +}; + +const HEADING_RULE: ShowFn = |elem, engine, styles| { + const SPACING_TO_NUMBERING: Em = Em::new(0.3); + + let span = elem.span(); + let mut realized = elem.body.clone(); + + let hanging_indent = elem.hanging_indent.get(styles); + let mut indent = match hanging_indent { + Smart::Custom(length) => length.resolve(styles), + Smart::Auto => Abs::zero(), + }; + + 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); + + if hanging_indent.is_auto() { + let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false)); + + // We don't have a locator for the numbering here, so we just + // use the measurement infrastructure for now. + let link = LocatorLink::measure(location); + let size = (engine.routines.layout_frame)( + engine, + &numbering, + Locator::link(&link), + styles, + pod, + )? + .size(); + + indent = size.x + SPACING_TO_NUMBERING.resolve(styles); + } + + let spacing = HElem::new(SPACING_TO_NUMBERING.into()).with_weak(true).pack(); + + realized = numbering + spacing + realized; + } + + let block = if indent != Abs::zero() { + let body = HElem::new((-indent).into()).pack() + realized; + let inset = Sides::default() + .with(styles.resolve(TextElem::dir).start(), Some(indent.into())); + BlockElem::new() + .with_body(Some(BlockBody::Content(body))) + .with_inset(inset) + } else { + BlockElem::new().with_body(Some(BlockBody::Content(realized))) + }; + + Ok(block.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) { + let (first, second) = match caption.position.get(styles) { + OuterVAlignment::Top => (caption.pack(), realized), + OuterVAlignment::Bottom => (realized, caption.pack()), + }; + realized = Content::sequence(vec![ + first, + VElem::new(elem.gap.get(styles).into()) + .with_weak(true) + .pack() + .spanned(span), + second, + ]); + } + + // Ensure that the body is considered a paragraph. + realized += ParbreakElem::shared().clone().spanned(span); + + // Wrap the contents in a block. + realized = BlockElem::new() + .with_body(Some(BlockBody::Content(realized))) + .pack() + .spanned(span); + + // Wrap in a float. + if let Some(align) = elem.placement.get(styles) { + realized = PlaceElem::new(realized) + .with_alignment(align.map(|align| HAlignment::Center + align)) + .with_scope(elem.scope.get(styles)) + .with_float(true) + .pack() + .spanned(span); + } else if elem.scope.get(styles) == PlacementScope::Parent { + bail!( + span, + "parent-scoped placement is only available for floating figures"; + hint: "you can enable floating placement with `figure(placement: auto, ..)`" + ); + } + + Ok(realized) +}; + +const FIGURE_CAPTION_RULE: ShowFn = |elem, engine, styles| { + Ok(BlockElem::new() + .with_body(Some(BlockBody::Content(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) { + // Add zero-width weak spacing to make the quotes "sticky". + let hole = HElem::hole().pack(); + let sticky = Content::sequence([hole.clone(), realized, hole]); + realized = QuoteElem::quoted(sticky, styles); + } + + let attribution = elem.attribution.get_ref(styles); + + if block { + realized = BlockElem::new() + .with_body(Some(BlockBody::Content(realized))) + .pack() + .spanned(span); + + if let Some(attribution) = attribution.as_ref() { + // Bring the attribution a bit closer to the quote. + let gap = Spacing::Rel(Em::new(0.9).into()); + let v = VElem::new(gap).with_weak(true).pack(); + realized += v; + realized += BlockElem::new() + .with_body(Some(BlockBody::Content(attribution.realize(span)))) + .pack() + .aligned(Alignment::END); + } + + realized = PadElem::new(realized).pack(); + } else if let Some(Attribution::Label(label)) = attribution { + realized += SpaceElem::shared().clone(); + realized += CiteElem::new(*label).pack().spanned(span); + } + + Ok(realized) +}; + +const FOOTNOTE_RULE: ShowFn = |elem, engine, styles| { + let span = elem.span(); + let loc = elem.declaration_location(engine).at(span)?; + let numbering = elem.numbering.get_ref(styles); + let counter = Counter::of(FootnoteElem::ELEM); + let num = counter.display_at_loc(engine, loc, styles, numbering)?; + let sup = SuperElem::new(num).pack().spanned(span); + let loc = loc.variant(1); + // Add zero-width weak spacing to make the footnote "sticky". + // TODO(accessibility): generate alt text + Ok(HElem::hole().pack() + sup.linked(Destination::Location(loc), None)) +}; + +const FOOTNOTE_ENTRY_RULE: ShowFn = |elem, engine, styles| { + let span = elem.span(); + let number_gap = Em::new(0.05); + let default = StyleChain::default(); + let numbering = elem.note.numbering.get_ref(default); + let counter = Counter::of(FootnoteElem::ELEM); + let Some(loc) = elem.note.location() else { + bail!( + span, "footnote entry must have a location"; + hint: "try using a query or a show rule to customize the footnote instead" + ); + }; + + let num = counter.display_at_loc(engine, loc, styles, numbering)?; + let sup = SuperElem::new(num) + .pack() + .spanned(span) + // TODO(accessibility): generate alt text + .linked(Destination::Location(loc), None) + .located(loc.variant(1)); + + Ok(Content::sequence([ + HElem::new(elem.indent.get(styles).into()).pack(), + sup, + HElem::new(number_gap.into()).with_weak(true).pack(), + elem.note.body_content().unwrap().clone(), + ])) +}; + +const OUTLINE_RULE: ShowFn = |elem, engine, styles| { + let span = elem.span(); + + // Build the outline title. + let mut seq = vec![]; + if let Some(title) = elem.title.get_cloned(styles).unwrap_or_else(|| { + Some(TextElem::packed(Packed::::local_name_in(styles)).spanned(span)) + }) { + seq.push( + HeadingElem::new(title) + .with_depth(NonZeroUsize::ONE) + .pack() + .spanned(span), + ); + } + + let elems = engine.introspector.query(&elem.target.get_ref(styles).0); + let depth = elem.depth.get(styles).unwrap_or(NonZeroUsize::MAX); + + // Build the outline entries. + let mut entries = vec![]; + for elem in elems { + let Some(outlinable) = elem.with::() else { + bail!(span, "cannot outline {}", elem.func().name()); + }; + + let level = outlinable.level(); + if outlinable.outlined() && level <= depth { + let entry = OutlineEntry::new(level, elem); + entries.push(entry.pack().spanned(span)); + } + } + + // Wrap the entries into a marker for pdf tagging. + seq.push(OutlineBody::new(Content::sequence(entries)).pack()); + + Ok(Content::sequence(seq)) +}; + +const OUTLINE_BODY_RULE: ShowFn = |elem, _, _| Ok(elem.body.clone()); + +const OUTLINE_ENTRY_RULE: ShowFn = |elem, engine, styles| { + let span = elem.span(); + let context = Context::new(None, Some(styles)); + let context = context.track(); + + // TODO(accessibility): prefix should be wrapped in a `Lbl` structure element + let prefix = elem.prefix(engine, context, span)?; + let body = elem.body().at(span)?; + let page = elem.page(engine, context, span)?; + let alt = { + let prefix = prefix.as_ref().map(|p| p.plain_text()).unwrap_or_default(); + let body = body.plain_text(); + let page_str = PageElem::local_name_in(styles); + let page_nr = page.plain_text(); + let quotes = SmartQuotes::get( + styles.get_ref(SmartQuoteElem::quotes), + styles.get(TextElem::lang), + styles.get(TextElem::region), + styles.get(SmartQuoteElem::alternative), + ); + let open = quotes.double_open; + let close = quotes.double_close; + eco_format!("{prefix} {open}{body}{close} {page_str} {page_nr}",) + }; + let inner = elem.inner(context, span, body, page)?; + let block = if elem.element.is::() { + let body = prefix.unwrap_or_default() + inner; + BlockElem::new() + .with_body(Some(BlockBody::Content(body))) + .pack() + .spanned(span) + } else { + elem.indented(engine, context, span, prefix, inner, Em::new(0.5).into())? + }; + + let loc = elem.element_location().at(span)?; + Ok(block.linked(Destination::Location(loc), Some(alt))) +}; + +const REF_RULE: ShowFn = |elem, engine, styles| elem.realize(engine, styles); + +const CITE_GROUP_RULE: ShowFn = |elem, engine, _| elem.realize(engine); + +const BIBLIOGRAPHY_RULE: ShowFn = |elem, engine, styles| { + const COLUMN_GUTTER: Em = Em::new(0.65); + const INDENT: Em = Em::new(1.5); + + let span = elem.span(); + + let mut seq = vec![]; + if let Some(title) = elem.title.get_ref(styles).clone().unwrap_or_else(|| { + Some( + TextElem::packed(Packed::::local_name_in(styles)) + .spanned(span), + ) + }) { + seq.push( + HeadingElem::new(title) + .with_depth(NonZeroUsize::ONE) + .pack() + .spanned(span), + ); + } + + let works = Works::generate(engine).at(span)?; + let references = works + .references + .as_ref() + .ok_or_else(|| match elem.style.get_ref(styles).source { + CslSource::Named(style) => eco_format!( + "CSL style \"{}\" is not suitable for bibliographies", + style.display_name() + ), + CslSource::Normal(..) => { + "CSL style is not suitable for bibliographies".into() + } + }) + .at(span)?; + + if references.iter().any(|(prefix, _)| prefix.is_some()) { + let row_gutter = styles.get(ParElem::spacing); + + let mut cells = vec![]; + for (prefix, reference) in references { + cells.push(GridChild::Item(GridItem::Cell( + Packed::new(GridCell::new(prefix.clone().unwrap_or_default())) + .spanned(span), + ))); + cells.push(GridChild::Item(GridItem::Cell( + Packed::new(GridCell::new(reference.clone())).spanned(span), + ))); + } + seq.push( + GridElem::new(cells) + .with_columns(TrackSizings(smallvec![Sizing::Auto; 2])) + .with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()])) + .with_row_gutter(TrackSizings(smallvec![row_gutter.into()])) + .pack() + .spanned(span), + ); + } else { + for (_, reference) in references { + let realized = reference.clone(); + let block = if works.hanging_indent { + let body = HElem::new((-INDENT).into()).pack() + realized; + let inset = Sides::default() + .with(styles.resolve(TextElem::dir).start(), Some(INDENT.into())); + BlockElem::new() + .with_body(Some(BlockBody::Content(body))) + .with_inset(inset) + } else { + BlockElem::new().with_body(Some(BlockBody::Content(realized))) + }; + + seq.push(block.pack().spanned(span)); + } + } + + Ok(Content::sequence(seq)) +}; + +const TABLE_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::multi_layouter(elem.clone(), crate::grid::layout_table) + .pack() + .spanned(elem.span())) +}; + +const TABLE_CELL_RULE: ShowFn = |elem, _, styles| { + show_cell(elem.body.clone(), elem.inset.get(styles), elem.align.get(styles)) +}; + +const SUB_RULE: ShowFn = |elem, _, styles| { + show_script( + styles, + elem.body.clone(), + elem.typographic.get(styles), + elem.baseline.get(styles), + elem.size.get(styles), + ScriptKind::Sub, + ) +}; + +const SUPER_RULE: ShowFn = |elem, _, styles| { + show_script( + styles, + elem.body.clone(), + elem.typographic.get(styles), + elem.baseline.get(styles), + elem.size.get(styles), + ScriptKind::Super, + ) +}; + +fn show_script( + styles: StyleChain, + body: Content, + typographic: bool, + baseline: Smart, + size: Smart, + kind: ScriptKind, +) -> SourceResult { + let font_size = styles.resolve(TextElem::size); + Ok(body.set( + TextElem::shift_settings, + Some(ShiftSettings { + typographic, + shift: baseline.map(|l| -Em::from_length(l, font_size)), + size: size.map(|t| Em::from_length(t.0, font_size)), + kind, + }), + )) +} + +const UNDERLINE_RULE: ShowFn = |elem, _, styles| { + Ok(elem.body.clone().set( + TextElem::deco, + smallvec![Decoration { + line: DecoLine::Underline { + stroke: elem.stroke.resolve(styles).unwrap_or_default(), + offset: elem.offset.resolve(styles), + evade: elem.evade.get(styles), + background: elem.background.get(styles), + }, + extent: elem.extent.resolve(styles), + }], + )) +}; + +const OVERLINE_RULE: ShowFn = |elem, _, styles| { + Ok(elem.body.clone().set( + TextElem::deco, + smallvec![Decoration { + line: DecoLine::Overline { + stroke: elem.stroke.resolve(styles).unwrap_or_default(), + offset: elem.offset.resolve(styles), + evade: elem.evade.get(styles), + background: elem.background.get(styles), + }, + extent: elem.extent.resolve(styles), + }], + )) +}; + +const STRIKE_RULE: ShowFn = |elem, _, styles| { + Ok(elem.body.clone().set( + TextElem::deco, + smallvec![Decoration { + // Note that we do not support evade option for strikethrough. + line: DecoLine::Strikethrough { + stroke: elem.stroke.resolve(styles).unwrap_or_default(), + offset: elem.offset.resolve(styles), + background: elem.background.get(styles), + }, + extent: elem.extent.resolve(styles), + }], + )) +}; + +const HIGHLIGHT_RULE: ShowFn = |elem, _, styles| { + Ok(elem.body.clone().set( + TextElem::deco, + smallvec![Decoration { + line: DecoLine::Highlight { + fill: elem.fill.get_cloned(styles), + stroke: elem + .stroke + .resolve(styles) + .unwrap_or_default() + .map(|stroke| stroke.map(Stroke::unwrap_or_default)), + top_edge: elem.top_edge.get(styles), + bottom_edge: elem.bottom_edge.get(styles), + radius: elem.radius.resolve(styles).unwrap_or_default(), + }, + extent: elem.extent.resolve(styles), + }], + )) +}; + +const SMALLCAPS_RULE: ShowFn = |elem, _, styles| { + let sc = if elem.all.get(styles) { Smallcaps::All } else { Smallcaps::Minuscules }; + Ok(elem.body.clone().set(TextElem::smallcaps, Some(sc))) +}; + +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()); + } + + let mut realized = Content::sequence(seq); + + if elem.block.get(styles) { + // Align the text before inserting it into the block. + realized = realized.aligned(elem.align.get(styles).into()); + realized = BlockElem::new() + .with_body(Some(BlockBody::Content(realized))) + .pack() + .spanned(elem.span()); + } + + Ok(realized) +}; + +const RAW_LINE_RULE: ShowFn = |elem, _, _| Ok(elem.body.clone()); + +const ALIGN_RULE: ShowFn = + |elem, _, styles| Ok(elem.body.clone().aligned(elem.alignment.get(styles))); + +const PAD_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::multi_layouter(elem.clone(), crate::pad::layout_pad) + .pack() + .spanned(elem.span())) +}; + +const COLUMNS_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::multi_layouter(elem.clone(), crate::flow::layout_columns) + .pack() + .spanned(elem.span())) +}; + +const STACK_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::multi_layouter(elem.clone(), crate::stack::layout_stack) + .pack() + .spanned(elem.span())) +}; + +const GRID_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::multi_layouter(elem.clone(), crate::grid::layout_grid) + .pack() + .spanned(elem.span())) +}; + +const GRID_CELL_RULE: ShowFn = |elem, _, styles| { + show_cell(elem.body.clone(), elem.inset.get(styles), elem.align.get(styles)) +}; + +/// Function with common code to display a grid cell or table cell. +fn show_cell( + mut body: Content, + inset: Smart>>>, + align: Smart, +) -> SourceResult { + let inset = inset.unwrap_or_default().map(Option::unwrap_or_default); + + if inset != Sides::default() { + // Only pad if some inset is not 0pt. + // Avoids a bug where using .padded() in any way inside Show causes + // alignment in align(...) to break. + body = body.padded(inset); + } + + if let Smart::Custom(alignment) = align { + body = body.aligned(alignment); + } + + Ok(body) +} + +const MOVE_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_move) + .pack() + .spanned(elem.span())) +}; + +const SCALE_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_scale) + .pack() + .spanned(elem.span())) +}; + +const ROTATE_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_rotate) + .pack() + .spanned(elem.span())) +}; + +const SKEW_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::single_layouter(elem.clone(), crate::transforms::layout_skew) + .pack() + .spanned(elem.span())) +}; + +const REPEAT_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::single_layouter(elem.clone(), crate::repeat::layout_repeat) + .pack() + .spanned(elem.span())) +}; + +const HIDE_RULE: ShowFn = + |elem, _, _| Ok(elem.body.clone().set(HideElem::hidden, true)); + +const LAYOUT_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::multi_layouter( + elem.clone(), + |elem, engine, locator, styles, regions| { + // Gets the current region's base size, which will be the size of the + // outer container, or of the page if there is no such container. + let Size { x, y } = regions.base(); + let loc = elem.location().unwrap(); + let context = Context::new(Some(loc), Some(styles)); + let result = elem + .func + .call(engine, context.track(), [dict! { "width" => x, "height" => y }])? + .display(); + crate::flow::layout_fragment(engine, &result, locator, styles, regions) + }, + ) + .pack() + .spanned(elem.span())) +}; + +const IMAGE_RULE: ShowFn = |elem, _, styles| { + Ok(BlockElem::single_layouter(elem.clone(), crate::image::layout_image) + .with_width(elem.width.get(styles)) + .with_height(elem.height.get(styles)) + .pack() + .spanned(elem.span())) +}; + +const LINE_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_line) + .pack() + .spanned(elem.span())) +}; + +const RECT_RULE: ShowFn = |elem, _, styles| { + Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_rect) + .with_width(elem.width.get(styles)) + .with_height(elem.height.get(styles)) + .pack() + .spanned(elem.span())) +}; + +const SQUARE_RULE: ShowFn = |elem, _, styles| { + Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_square) + .with_width(elem.width.get(styles)) + .with_height(elem.height.get(styles)) + .pack() + .spanned(elem.span())) +}; + +const ELLIPSE_RULE: ShowFn = |elem, _, styles| { + Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_ellipse) + .with_width(elem.width.get(styles)) + .with_height(elem.height.get(styles)) + .pack() + .spanned(elem.span())) +}; + +const CIRCLE_RULE: ShowFn = |elem, _, styles| { + Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_circle) + .with_width(elem.width.get(styles)) + .with_height(elem.height.get(styles)) + .pack() + .spanned(elem.span())) +}; + +const POLYGON_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_polygon) + .pack() + .spanned(elem.span())) +}; + +const CURVE_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_curve) + .pack() + .spanned(elem.span())) +}; + +const PATH_RULE: ShowFn = |elem, _, _| { + Ok(BlockElem::single_layouter(elem.clone(), crate::shapes::layout_path) + .pack() + .spanned(elem.span())) +}; + +const EQUATION_RULE: ShowFn = |elem, _, styles| { + if elem.block.get(styles) { + Ok(BlockElem::multi_layouter(elem.clone(), crate::math::layout_equation_block) + .pack() + .spanned(elem.span())) + } else { + Ok(InlineElem::layouter(elem.clone(), crate::math::layout_equation_inline) + .pack() + .spanned(elem.span())) + } +}; + +const EMBED_RULE: ShowFn = |_, _, _| Ok(Content::empty()); + +const PDF_TAG_RULE: ShowFn = |elem, _, _| Ok(elem.body.clone()); + +const PDF_ARTIFACT_RULE: ShowFn = |elem, _, _| Ok(elem.body.clone()); diff --git a/crates/typst-library/src/foundations/content/element.rs b/crates/typst-library/src/foundations/content/element.rs index 49b0b0f9b..65c2e28b5 100644 --- a/crates/typst-library/src/foundations/content/element.rs +++ b/crates/typst-library/src/foundations/content/element.rs @@ -246,12 +246,6 @@ pub trait Synthesize { -> SourceResult<()>; } -/// Defines a built-in show rule for an element. -pub trait Show { - /// Execute the base recipe for this element. - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult; -} - /// Defines built-in show set rules for an element. /// /// This is a bit more powerful than a user-defined show-set because it can diff --git a/crates/typst-library/src/foundations/content/mod.rs b/crates/typst-library/src/foundations/content/mod.rs index 5fc4cee4c..84ab0c00a 100644 --- a/crates/typst-library/src/foundations/content/mod.rs +++ b/crates/typst-library/src/foundations/content/mod.rs @@ -785,7 +785,7 @@ impl Repr for StyledElem { } /// An element that associates the body of a link with the destination. -#[elem(Show, Locatable)] +#[elem(Locatable)] pub struct LinkMarker { /// The content. #[required] @@ -796,12 +796,6 @@ pub struct LinkMarker { pub alt: Option, } -impl Show for Packed { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(self.body.clone()) - } -} - impl IntoValue for T { fn into_value(self) -> Value { Value::Content(self.pack()) diff --git a/crates/typst-library/src/foundations/content/packed.rs b/crates/typst-library/src/foundations/content/packed.rs index 71bb66a94..e99162629 100644 --- a/crates/typst-library/src/foundations/content/packed.rs +++ b/crates/typst-library/src/foundations/content/packed.rs @@ -64,6 +64,16 @@ impl Packed { self.0 } + /// Pack back into a reference to content. + pub fn pack_ref(&self) -> &Content { + &self.0 + } + + /// Pack back into a mutable reference to content. + pub fn pack_mut(&mut self) -> &mut Content { + &mut self.0 + } + /// Extract the raw underlying element. pub fn unpack(self) -> T { // This function doesn't yet need owned self, but might in the future. @@ -94,10 +104,6 @@ impl Packed { pub fn set_location(&mut self, location: Location) { self.0.set_location(location); } - - pub fn as_content(&self) -> &Content { - &self.0 - } } impl AsRef for Packed { diff --git a/crates/typst-library/src/foundations/content/raw.rs b/crates/typst-library/src/foundations/content/raw.rs index f5dfffd73..dde26bd79 100644 --- a/crates/typst-library/src/foundations/content/raw.rs +++ b/crates/typst-library/src/foundations/content/raw.rs @@ -141,7 +141,7 @@ impl RawContent { /// Clones a packed element into new raw content. pub(super) fn clone_impl(elem: &Packed) -> Self { - let raw = &elem.as_content().0; + let raw = &elem.pack_ref().0; let header = raw.header(); RawContent::create( elem.as_ref().clone(), diff --git a/crates/typst-library/src/foundations/context.rs b/crates/typst-library/src/foundations/context.rs index bf4bdcd25..56d87775e 100644 --- a/crates/typst-library/src/foundations/context.rs +++ b/crates/typst-library/src/foundations/context.rs @@ -3,7 +3,7 @@ use comemo::Track; use crate::diag::{bail, Hint, HintedStrResult, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - elem, Args, Construct, Content, Func, Packed, Show, StyleChain, Value, + elem, Args, Construct, Content, Func, ShowFn, StyleChain, Value, }; use crate::introspection::{Locatable, Location}; @@ -61,7 +61,7 @@ fn require(val: Option) -> HintedStrResult { } /// Executes a `context` block. -#[elem(Construct, Locatable, Show)] +#[elem(Construct, Locatable)] pub struct ContextElem { /// The function to call with the context. #[required] @@ -75,11 +75,8 @@ impl Construct for ContextElem { } } -impl Show for Packed { - #[typst_macros::time(name = "context", span = self.span())] - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let loc = self.location().unwrap(); - let context = Context::new(Some(loc), Some(styles)); - Ok(self.func.call::<[Value; 0]>(engine, context.track(), [])?.display()) - } -} +pub const CONTEXT_RULE: ShowFn = |elem, engine, styles| { + let loc = elem.location().unwrap(); + let context = Context::new(Some(loc), Some(styles)); + Ok(elem.func.call::<[Value; 0]>(engine, context.track(), [])?.display()) +}; diff --git a/crates/typst-library/src/foundations/str.rs b/crates/typst-library/src/foundations/str.rs index 23a1bd4cf..e500b1a4d 100644 --- a/crates/typst-library/src/foundations/str.rs +++ b/crates/typst-library/src/foundations/str.rs @@ -179,24 +179,40 @@ impl Str { } /// Extracts the first grapheme cluster of the string. - /// Fails with an error if the string is empty. + /// + /// Returns the provided default value if the string is empty or fails with + /// an error if no default value was specified. #[func] - pub fn first(&self) -> StrResult { + pub fn first( + &self, + /// A default value to return if the string is empty. + #[named] + default: Option, + ) -> StrResult { self.0 .graphemes(true) .next() .map(Into::into) + .or(default) .ok_or_else(string_is_empty) } /// Extracts the last grapheme cluster of the string. - /// Fails with an error if the string is empty. + /// + /// Returns the provided default value if the string is empty or fails with + /// an error if no default value was specified. #[func] - pub fn last(&self) -> StrResult { + pub fn last( + &self, + /// A default value to return if the string is empty. + #[named] + default: Option, + ) -> StrResult { self.0 .graphemes(true) .next_back() .map(Into::into) + .or(default) .ok_or_else(string_is_empty) } diff --git a/crates/typst-library/src/foundations/styles.rs b/crates/typst-library/src/foundations/styles.rs index 978b47d5f..0da036f6f 100644 --- a/crates/typst-library/src/foundations/styles.rs +++ b/crates/typst-library/src/foundations/styles.rs @@ -1,4 +1,5 @@ use std::any::{Any, TypeId}; +use std::collections::HashMap; use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use std::{mem, ptr}; @@ -13,7 +14,7 @@ use crate::diag::{SourceResult, Trace, Tracepoint}; use crate::engine::Engine; use crate::foundations::{ cast, ty, Content, Context, Element, Field, Func, NativeElement, OneOrMultiple, - RefableProperty, Repr, Selector, SettableProperty, + Packed, RefableProperty, Repr, Selector, SettableProperty, Target, }; use crate::text::{FontFamily, FontList, TextElem}; @@ -938,3 +939,129 @@ fn block_wrong_type(func: Element, id: u8, value: &Block) -> ! { value ) } + +/// Holds native show rules. +pub struct NativeRuleMap { + rules: HashMap<(Element, Target), NativeShowRule>, +} + +/// The signature of a native show rule. +pub type ShowFn = fn( + elem: &Packed, + engine: &mut Engine, + styles: StyleChain, +) -> SourceResult; + +impl NativeRuleMap { + /// Creates a new rule map. + /// + /// Should be populated with rules for all target-element combinations that + /// are supported. + /// + /// Contains built-in rules for a few special elements. + pub fn new() -> Self { + let mut rules = Self { rules: HashMap::new() }; + + // ContextElem is as special as SequenceElem and StyledElem and could, + // in theory, also be special cased in realization. + rules.register_builtin(crate::foundations::CONTEXT_RULE); + + // CounterDisplayElem only exists because the compiler can't currently + // express the equivalent of `context counter(..).display(..)` in native + // code (no native closures). + rules.register_builtin(crate::introspection::COUNTER_DISPLAY_RULE); + + // These are all only for introspection and empty on all targets. + rules.register_empty::(); + rules.register_empty::(); + rules.register_empty::(); + rules.register_empty::(); + + rules + } + + /// Registers a rule for all targets. + fn register_empty(&mut self) { + self.register_builtin::(|_, _, _| Ok(Content::empty())); + } + + /// Registers a rule for all targets. + fn register_builtin(&mut self, f: ShowFn) { + self.register(Target::Paged, f); + self.register(Target::Html, f); + } + + /// Registers a rule for a target. + /// + /// Panics if a rule already exists for this target-element combination. + pub fn register(&mut self, target: Target, f: ShowFn) { + let res = self.rules.insert((T::ELEM, target), NativeShowRule::new(f)); + if res.is_some() { + panic!( + "duplicate native show rule for `{}` on {target:?} target", + T::ELEM.name() + ) + } + } + + /// Retrieves the rule that applies to the `content` on the current + /// `target`. + pub fn get(&self, target: Target, content: &Content) -> Option { + self.rules.get(&(content.func(), target)).copied() + } +} + +impl Default for NativeRuleMap { + fn default() -> Self { + Self::new() + } +} + +pub use rule::NativeShowRule; + +mod rule { + use super::*; + + /// The show rule for a native element. + #[derive(Copy, Clone)] + pub struct NativeShowRule { + /// The element to which this rule applies. + elem: Element, + /// Must only be called with content of the appropriate type. + f: unsafe fn( + elem: &Content, + engine: &mut Engine, + styles: StyleChain, + ) -> SourceResult, + } + + impl NativeShowRule { + /// Create a new type-erased show rule. + pub fn new(f: ShowFn) -> Self { + Self { + elem: T::ELEM, + // Safety: The two function pointer types only differ in the + // first argument, which changes from `&Packed` to + // `&Content`. `Packed` is a transparent wrapper around + // `Content`. The resulting function is unsafe to call because + // content of the correct type must be passed to it. + #[allow(clippy::missing_transmute_annotations)] + f: unsafe { std::mem::transmute(f) }, + } + } + + /// Applies the rule to content. Panics if the content is of the wrong + /// type. + pub fn apply( + &self, + content: &Content, + engine: &mut Engine, + styles: StyleChain, + ) -> SourceResult { + assert_eq!(content.elem(), self.elem); + + // Safety: We just checked that the element is of the correct type. + unsafe { (self.f)(content, engine, styles) } + } + } +} diff --git a/crates/typst-library/src/foundations/target.rs b/crates/typst-library/src/foundations/target.rs index 71e7554e0..ff90f1f7b 100644 --- a/crates/typst-library/src/foundations/target.rs +++ b/crates/typst-library/src/foundations/target.rs @@ -4,7 +4,7 @@ use crate::diag::HintedStrResult; use crate::foundations::{elem, func, Cast, Context}; /// The export target. -#[derive(Debug, Default, Copy, Clone, PartialEq, Hash, Cast)] +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum Target { /// The target that is used for paged, fully laid-out content. #[default] diff --git a/crates/typst-library/src/html/dom.rs b/crates/typst-library/src/html/dom.rs deleted file mode 100644 index 47bcf9954..000000000 --- a/crates/typst-library/src/html/dom.rs +++ /dev/null @@ -1,823 +0,0 @@ -use std::fmt::{self, Debug, Display, Formatter}; - -use ecow::{EcoString, EcoVec}; -use typst_syntax::Span; -use typst_utils::{PicoStr, ResolvedPicoStr}; - -use crate::diag::{bail, HintedStrResult, StrResult}; -use crate::foundations::{cast, Dict, Repr, Str}; -use crate::introspection::{Introspector, Tag}; -use crate::layout::{Abs, Frame}; -use crate::model::DocumentInfo; - -/// An HTML document. -#[derive(Debug, Clone)] -pub struct HtmlDocument { - /// The document's root HTML element. - pub root: HtmlElement, - /// Details about the document. - pub info: DocumentInfo, - /// Provides the ability to execute queries on the document. - pub introspector: Introspector, -} - -/// A child of an HTML element. -#[derive(Debug, Clone, Hash)] -pub enum HtmlNode { - /// An introspectable element that produced something within this node. - Tag(Tag), - /// Plain text. - Text(EcoString, Span), - /// Another element. - Element(HtmlElement), - /// Layouted content that will be embedded into HTML as an SVG. - Frame(HtmlFrame), -} - -impl HtmlNode { - /// Create a plain text node. - pub fn text(text: impl Into, span: Span) -> Self { - Self::Text(text.into(), span) - } -} - -impl From for HtmlNode { - fn from(element: HtmlElement) -> Self { - Self::Element(element) - } -} - -/// An HTML element. -#[derive(Debug, Clone, Hash)] -pub struct HtmlElement { - /// The HTML tag. - pub tag: HtmlTag, - /// The element's attributes. - pub attrs: HtmlAttrs, - /// The element's children. - pub children: Vec, - /// The span from which the element originated, if any. - pub span: Span, -} - -impl HtmlElement { - /// Create a new, blank element without attributes or children. - pub fn new(tag: HtmlTag) -> Self { - Self { - tag, - attrs: HtmlAttrs::default(), - children: vec![], - span: Span::detached(), - } - } - - /// Attach children to the element. - /// - /// Note: This overwrites potential previous children. - pub fn with_children(mut self, children: Vec) -> Self { - self.children = children; - self - } - - /// Add an atribute to the element. - pub fn with_attr(mut self, key: HtmlAttr, value: impl Into) -> Self { - self.attrs.push(key, value); - self - } - - /// Attach a span to the element. - pub fn spanned(mut self, span: Span) -> Self { - self.span = span; - self - } -} - -/// The tag of an HTML element. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct HtmlTag(PicoStr); - -impl HtmlTag { - /// Intern an HTML tag string at runtime. - pub fn intern(string: &str) -> StrResult { - if string.is_empty() { - bail!("tag name must not be empty"); - } - - if let Some(c) = string.chars().find(|&c| !charsets::is_valid_in_tag_name(c)) { - bail!("the character {} is not valid in a tag name", c.repr()); - } - - Ok(Self(PicoStr::intern(string))) - } - - /// Creates a compile-time constant `HtmlTag`. - /// - /// Should only be used in const contexts because it can panic. - #[track_caller] - pub const fn constant(string: &'static str) -> Self { - if string.is_empty() { - panic!("tag name must not be empty"); - } - - let bytes = string.as_bytes(); - let mut i = 0; - while i < bytes.len() { - if !bytes[i].is_ascii() || !charsets::is_valid_in_tag_name(bytes[i] as char) { - panic!("not all characters are valid in a tag name"); - } - i += 1; - } - - Self(PicoStr::constant(string)) - } - - /// Resolves the tag to a string. - pub fn resolve(self) -> ResolvedPicoStr { - self.0.resolve() - } - - /// Turns the tag into its inner interned string. - pub const fn into_inner(self) -> PicoStr { - self.0 - } -} - -impl Debug for HtmlTag { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - Display::fmt(self, f) - } -} - -impl Display for HtmlTag { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "<{}>", self.resolve()) - } -} - -cast! { - HtmlTag, - self => self.0.resolve().as_str().into_value(), - v: Str => Self::intern(&v)?, -} - -/// Attributes of an HTML element. -#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] -pub struct HtmlAttrs(pub EcoVec<(HtmlAttr, EcoString)>); - -impl HtmlAttrs { - /// Add an attribute. - pub fn push(&mut self, attr: HtmlAttr, value: impl Into) { - self.0.push((attr, value.into())); - } -} - -cast! { - HtmlAttrs, - self => self.0 - .into_iter() - .map(|(key, value)| (key.resolve().as_str().into(), value.into_value())) - .collect::() - .into_value(), - values: Dict => Self(values - .into_iter() - .map(|(k, v)| { - let attr = HtmlAttr::intern(&k)?; - let value = v.cast::()?; - Ok((attr, value)) - }) - .collect::>()?), -} - -/// An attribute of an HTML element. -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct HtmlAttr(PicoStr); - -impl HtmlAttr { - /// Intern an HTML attribute string at runtime. - pub fn intern(string: &str) -> StrResult { - if string.is_empty() { - bail!("attribute name must not be empty"); - } - - if let Some(c) = - string.chars().find(|&c| !charsets::is_valid_in_attribute_name(c)) - { - bail!("the character {} is not valid in an attribute name", c.repr()); - } - - Ok(Self(PicoStr::intern(string))) - } - - /// Creates a compile-time constant `HtmlAttr`. - /// - /// Must only be used in const contexts (in a constant definition or - /// explicit `const { .. }` block) because otherwise a panic for a malformed - /// attribute or not auto-internible constant will only be caught at - /// runtime. - #[track_caller] - pub const fn constant(string: &'static str) -> Self { - if string.is_empty() { - panic!("attribute name must not be empty"); - } - - let bytes = string.as_bytes(); - let mut i = 0; - while i < bytes.len() { - if !bytes[i].is_ascii() - || !charsets::is_valid_in_attribute_name(bytes[i] as char) - { - panic!("not all characters are valid in an attribute name"); - } - i += 1; - } - - Self(PicoStr::constant(string)) - } - - /// Resolves the attribute to a string. - pub fn resolve(self) -> ResolvedPicoStr { - self.0.resolve() - } - - /// Turns the attribute into its inner interned string. - pub const fn into_inner(self) -> PicoStr { - self.0 - } -} - -impl Debug for HtmlAttr { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - Display::fmt(self, f) - } -} - -impl Display for HtmlAttr { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.resolve()) - } -} - -cast! { - HtmlAttr, - self => self.0.resolve().as_str().into_value(), - v: Str => Self::intern(&v)?, -} - -/// Layouted content that will be embedded into HTML as an SVG. -#[derive(Debug, Clone, Hash)] -pub struct HtmlFrame { - /// The frame that will be displayed as an SVG. - pub inner: Frame, - /// The text size where the frame was defined. This is used to size the - /// frame with em units to make text in and outside of the frame sized - /// consistently. - pub text_size: Abs, -} - -/// Defines syntactical properties of HTML tags, attributes, and text. -pub mod charsets { - /// Check whether a character is in a tag name. - pub const fn is_valid_in_tag_name(c: char) -> bool { - c.is_ascii_alphanumeric() - } - - /// Check whether a character is valid in an attribute name. - pub const fn is_valid_in_attribute_name(c: char) -> bool { - match c { - // These are forbidden. - '\0' | ' ' | '"' | '\'' | '>' | '/' | '=' => false, - c if is_whatwg_control_char(c) => false, - c if is_whatwg_non_char(c) => false, - // _Everything_ else is allowed, including U+2029 paragraph - // separator. Go wild. - _ => true, - } - } - - /// Check whether a character can be an used in an attribute value without - /// escaping. - /// - /// See - pub const fn is_valid_in_attribute_value(c: char) -> bool { - match c { - // Ampersands are sometimes legal (i.e. when they are not _ambiguous - // ampersands_) but it is not worth the trouble to check for that. - '&' => false, - // Quotation marks are not allowed in double-quote-delimited attribute - // values. - '"' => false, - // All other text characters are allowed. - c => is_w3c_text_char(c), - } - } - - /// Check whether a character can be an used in normal text without - /// escaping. - pub const fn is_valid_in_normal_element_text(c: char) -> bool { - match c { - // Ampersands are sometimes legal (i.e. when they are not _ambiguous - // ampersands_) but it is not worth the trouble to check for that. - '&' => false, - // Less-than signs are not allowed in text. - '<' => false, - // All other text characters are allowed. - c => is_w3c_text_char(c), - } - } - - /// Check if something is valid text in HTML. - pub const fn is_w3c_text_char(c: char) -> bool { - match c { - // Non-characters are obviously not text characters. - c if is_whatwg_non_char(c) => false, - // Control characters are disallowed, except for whitespace. - c if is_whatwg_control_char(c) => c.is_ascii_whitespace(), - // Everything else is allowed. - _ => true, - } - } - - const fn is_whatwg_non_char(c: char) -> bool { - match c { - '\u{fdd0}'..='\u{fdef}' => true, - // Non-characters matching xxFFFE or xxFFFF up to x10FFFF (inclusive). - c if c as u32 & 0xfffe == 0xfffe && c as u32 <= 0x10ffff => true, - _ => false, - } - } - - const fn is_whatwg_control_char(c: char) -> bool { - match c { - // C0 control characters. - '\u{00}'..='\u{1f}' => true, - // Other control characters. - '\u{7f}'..='\u{9f}' => true, - _ => false, - } - } -} - -/// Predefined constants for HTML tags. -#[allow(non_upper_case_globals)] -pub mod tag { - use super::HtmlTag; - - pub const a: HtmlTag = HtmlTag::constant("a"); - pub const abbr: HtmlTag = HtmlTag::constant("abbr"); - pub const address: HtmlTag = HtmlTag::constant("address"); - pub const area: HtmlTag = HtmlTag::constant("area"); - pub const article: HtmlTag = HtmlTag::constant("article"); - pub const aside: HtmlTag = HtmlTag::constant("aside"); - pub const audio: HtmlTag = HtmlTag::constant("audio"); - pub const b: HtmlTag = HtmlTag::constant("b"); - pub const base: HtmlTag = HtmlTag::constant("base"); - pub const bdi: HtmlTag = HtmlTag::constant("bdi"); - pub const bdo: HtmlTag = HtmlTag::constant("bdo"); - pub const blockquote: HtmlTag = HtmlTag::constant("blockquote"); - pub const body: HtmlTag = HtmlTag::constant("body"); - pub const br: HtmlTag = HtmlTag::constant("br"); - pub const button: HtmlTag = HtmlTag::constant("button"); - pub const canvas: HtmlTag = HtmlTag::constant("canvas"); - pub const caption: HtmlTag = HtmlTag::constant("caption"); - pub const cite: HtmlTag = HtmlTag::constant("cite"); - pub const code: HtmlTag = HtmlTag::constant("code"); - pub const col: HtmlTag = HtmlTag::constant("col"); - pub const colgroup: HtmlTag = HtmlTag::constant("colgroup"); - pub const data: HtmlTag = HtmlTag::constant("data"); - pub const datalist: HtmlTag = HtmlTag::constant("datalist"); - pub const dd: HtmlTag = HtmlTag::constant("dd"); - pub const del: HtmlTag = HtmlTag::constant("del"); - pub const details: HtmlTag = HtmlTag::constant("details"); - pub const dfn: HtmlTag = HtmlTag::constant("dfn"); - pub const dialog: HtmlTag = HtmlTag::constant("dialog"); - pub const div: HtmlTag = HtmlTag::constant("div"); - pub const dl: HtmlTag = HtmlTag::constant("dl"); - pub const dt: HtmlTag = HtmlTag::constant("dt"); - pub const em: HtmlTag = HtmlTag::constant("em"); - pub const embed: HtmlTag = HtmlTag::constant("embed"); - pub const fieldset: HtmlTag = HtmlTag::constant("fieldset"); - pub const figcaption: HtmlTag = HtmlTag::constant("figcaption"); - pub const figure: HtmlTag = HtmlTag::constant("figure"); - pub const footer: HtmlTag = HtmlTag::constant("footer"); - pub const form: HtmlTag = HtmlTag::constant("form"); - pub const h1: HtmlTag = HtmlTag::constant("h1"); - pub const h2: HtmlTag = HtmlTag::constant("h2"); - pub const h3: HtmlTag = HtmlTag::constant("h3"); - pub const h4: HtmlTag = HtmlTag::constant("h4"); - pub const h5: HtmlTag = HtmlTag::constant("h5"); - pub const h6: HtmlTag = HtmlTag::constant("h6"); - pub const head: HtmlTag = HtmlTag::constant("head"); - pub const header: HtmlTag = HtmlTag::constant("header"); - pub const hgroup: HtmlTag = HtmlTag::constant("hgroup"); - pub const hr: HtmlTag = HtmlTag::constant("hr"); - pub const html: HtmlTag = HtmlTag::constant("html"); - pub const i: HtmlTag = HtmlTag::constant("i"); - pub const iframe: HtmlTag = HtmlTag::constant("iframe"); - pub const img: HtmlTag = HtmlTag::constant("img"); - pub const input: HtmlTag = HtmlTag::constant("input"); - pub const ins: HtmlTag = HtmlTag::constant("ins"); - pub const kbd: HtmlTag = HtmlTag::constant("kbd"); - pub const label: HtmlTag = HtmlTag::constant("label"); - pub const legend: HtmlTag = HtmlTag::constant("legend"); - pub const li: HtmlTag = HtmlTag::constant("li"); - pub const link: HtmlTag = HtmlTag::constant("link"); - pub const main: HtmlTag = HtmlTag::constant("main"); - pub const map: HtmlTag = HtmlTag::constant("map"); - pub const mark: HtmlTag = HtmlTag::constant("mark"); - pub const menu: HtmlTag = HtmlTag::constant("menu"); - pub const meta: HtmlTag = HtmlTag::constant("meta"); - pub const meter: HtmlTag = HtmlTag::constant("meter"); - pub const nav: HtmlTag = HtmlTag::constant("nav"); - pub const noscript: HtmlTag = HtmlTag::constant("noscript"); - pub const object: HtmlTag = HtmlTag::constant("object"); - pub const ol: HtmlTag = HtmlTag::constant("ol"); - pub const optgroup: HtmlTag = HtmlTag::constant("optgroup"); - pub const option: HtmlTag = HtmlTag::constant("option"); - pub const output: HtmlTag = HtmlTag::constant("output"); - pub const p: HtmlTag = HtmlTag::constant("p"); - pub const picture: HtmlTag = HtmlTag::constant("picture"); - pub const pre: HtmlTag = HtmlTag::constant("pre"); - pub const progress: HtmlTag = HtmlTag::constant("progress"); - pub const q: HtmlTag = HtmlTag::constant("q"); - pub const rp: HtmlTag = HtmlTag::constant("rp"); - pub const rt: HtmlTag = HtmlTag::constant("rt"); - pub const ruby: HtmlTag = HtmlTag::constant("ruby"); - pub const s: HtmlTag = HtmlTag::constant("s"); - pub const samp: HtmlTag = HtmlTag::constant("samp"); - pub const script: HtmlTag = HtmlTag::constant("script"); - pub const search: HtmlTag = HtmlTag::constant("search"); - pub const section: HtmlTag = HtmlTag::constant("section"); - pub const select: HtmlTag = HtmlTag::constant("select"); - pub const slot: HtmlTag = HtmlTag::constant("slot"); - pub const small: HtmlTag = HtmlTag::constant("small"); - pub const source: HtmlTag = HtmlTag::constant("source"); - pub const span: HtmlTag = HtmlTag::constant("span"); - pub const strong: HtmlTag = HtmlTag::constant("strong"); - pub const style: HtmlTag = HtmlTag::constant("style"); - pub const sub: HtmlTag = HtmlTag::constant("sub"); - pub const summary: HtmlTag = HtmlTag::constant("summary"); - pub const sup: HtmlTag = HtmlTag::constant("sup"); - pub const table: HtmlTag = HtmlTag::constant("table"); - pub const tbody: HtmlTag = HtmlTag::constant("tbody"); - pub const td: HtmlTag = HtmlTag::constant("td"); - pub const template: HtmlTag = HtmlTag::constant("template"); - pub const textarea: HtmlTag = HtmlTag::constant("textarea"); - pub const tfoot: HtmlTag = HtmlTag::constant("tfoot"); - pub const th: HtmlTag = HtmlTag::constant("th"); - pub const thead: HtmlTag = HtmlTag::constant("thead"); - pub const time: HtmlTag = HtmlTag::constant("time"); - pub const title: HtmlTag = HtmlTag::constant("title"); - pub const tr: HtmlTag = HtmlTag::constant("tr"); - pub const track: HtmlTag = HtmlTag::constant("track"); - pub const u: HtmlTag = HtmlTag::constant("u"); - pub const ul: HtmlTag = HtmlTag::constant("ul"); - pub const var: HtmlTag = HtmlTag::constant("var"); - pub const video: HtmlTag = HtmlTag::constant("video"); - pub const wbr: HtmlTag = HtmlTag::constant("wbr"); - - /// Whether this is a void tag whose associated element may not have - /// children. - pub fn is_void(tag: HtmlTag) -> bool { - matches!( - tag, - self::area - | self::base - | self::br - | self::col - | self::embed - | self::hr - | self::img - | self::input - | self::link - | self::meta - | self::source - | self::track - | self::wbr - ) - } - - /// Whether this is a tag containing raw text. - pub fn is_raw(tag: HtmlTag) -> bool { - matches!(tag, self::script | self::style) - } - - /// Whether this is a tag containing escapable raw text. - pub fn is_escapable_raw(tag: HtmlTag) -> bool { - matches!(tag, self::textarea | self::title) - } - - /// Whether an element is considered metadata. - pub fn is_metadata(tag: HtmlTag) -> bool { - matches!( - tag, - self::base - | self::link - | self::meta - | self::noscript - | self::script - | self::style - | self::template - | self::title - ) - } - - /// Whether nodes with the tag have the CSS property `display: block` by - /// default. - pub fn is_block_by_default(tag: HtmlTag) -> bool { - matches!( - tag, - self::html - | self::head - | self::body - | self::article - | self::aside - | self::h1 - | self::h2 - | self::h3 - | self::h4 - | self::h5 - | self::h6 - | self::hgroup - | self::nav - | self::section - | self::dd - | self::dl - | self::dt - | self::menu - | self::ol - | self::ul - | self::address - | self::blockquote - | self::dialog - | self::div - | self::fieldset - | self::figure - | self::figcaption - | self::footer - | self::form - | self::header - | self::hr - | self::legend - | self::main - | self::p - | self::pre - | self::search - ) - } - - /// Whether the element is inline-level as opposed to being block-level. - /// - /// Not sure whether this distinction really makes sense. But we somehow - /// need to decide what to put into automatic paragraphs. A `` - /// should merged into a paragraph created by realization, but a `
` - /// shouldn't. - /// - /// - /// - /// - pub fn is_inline_by_default(tag: HtmlTag) -> bool { - matches!( - tag, - self::abbr - | self::a - | self::bdi - | self::b - | self::br - | self::bdo - | self::code - | self::cite - | self::dfn - | self::data - | self::i - | self::em - | self::mark - | self::kbd - | self::rp - | self::q - | self::ruby - | self::rt - | self::samp - | self::s - | self::span - | self::small - | self::sub - | self::strong - | self::time - | self::sup - | self::var - | self::u - ) - } - - /// Whether nodes with the tag have the CSS property `display: table(-.*)?` - /// by default. - pub fn is_tabular_by_default(tag: HtmlTag) -> bool { - matches!( - tag, - self::table - | self::thead - | self::tbody - | self::tfoot - | self::tr - | self::th - | self::td - | self::caption - | self::col - | self::colgroup - ) - } -} - -#[allow(non_upper_case_globals)] -#[rustfmt::skip] -pub mod attr { - use crate::html::HtmlAttr; - pub const abbr: HtmlAttr = HtmlAttr::constant("abbr"); - pub const accept: HtmlAttr = HtmlAttr::constant("accept"); - pub const accept_charset: HtmlAttr = HtmlAttr::constant("accept-charset"); - pub const accesskey: HtmlAttr = HtmlAttr::constant("accesskey"); - pub const action: HtmlAttr = HtmlAttr::constant("action"); - pub const allow: HtmlAttr = HtmlAttr::constant("allow"); - pub const allowfullscreen: HtmlAttr = HtmlAttr::constant("allowfullscreen"); - pub const alpha: HtmlAttr = HtmlAttr::constant("alpha"); - pub const alt: HtmlAttr = HtmlAttr::constant("alt"); - pub const aria_activedescendant: HtmlAttr = HtmlAttr::constant("aria-activedescendant"); - pub const aria_atomic: HtmlAttr = HtmlAttr::constant("aria-atomic"); - pub const aria_autocomplete: HtmlAttr = HtmlAttr::constant("aria-autocomplete"); - pub const aria_busy: HtmlAttr = HtmlAttr::constant("aria-busy"); - pub const aria_checked: HtmlAttr = HtmlAttr::constant("aria-checked"); - pub const aria_colcount: HtmlAttr = HtmlAttr::constant("aria-colcount"); - pub const aria_colindex: HtmlAttr = HtmlAttr::constant("aria-colindex"); - pub const aria_colspan: HtmlAttr = HtmlAttr::constant("aria-colspan"); - pub const aria_controls: HtmlAttr = HtmlAttr::constant("aria-controls"); - pub const aria_current: HtmlAttr = HtmlAttr::constant("aria-current"); - pub const aria_describedby: HtmlAttr = HtmlAttr::constant("aria-describedby"); - pub const aria_details: HtmlAttr = HtmlAttr::constant("aria-details"); - pub const aria_disabled: HtmlAttr = HtmlAttr::constant("aria-disabled"); - pub const aria_errormessage: HtmlAttr = HtmlAttr::constant("aria-errormessage"); - pub const aria_expanded: HtmlAttr = HtmlAttr::constant("aria-expanded"); - pub const aria_flowto: HtmlAttr = HtmlAttr::constant("aria-flowto"); - pub const aria_haspopup: HtmlAttr = HtmlAttr::constant("aria-haspopup"); - pub const aria_hidden: HtmlAttr = HtmlAttr::constant("aria-hidden"); - pub const aria_invalid: HtmlAttr = HtmlAttr::constant("aria-invalid"); - pub const aria_keyshortcuts: HtmlAttr = HtmlAttr::constant("aria-keyshortcuts"); - pub const aria_label: HtmlAttr = HtmlAttr::constant("aria-label"); - pub const aria_labelledby: HtmlAttr = HtmlAttr::constant("aria-labelledby"); - pub const aria_level: HtmlAttr = HtmlAttr::constant("aria-level"); - pub const aria_live: HtmlAttr = HtmlAttr::constant("aria-live"); - pub const aria_modal: HtmlAttr = HtmlAttr::constant("aria-modal"); - pub const aria_multiline: HtmlAttr = HtmlAttr::constant("aria-multiline"); - pub const aria_multiselectable: HtmlAttr = HtmlAttr::constant("aria-multiselectable"); - pub const aria_orientation: HtmlAttr = HtmlAttr::constant("aria-orientation"); - pub const aria_owns: HtmlAttr = HtmlAttr::constant("aria-owns"); - pub const aria_placeholder: HtmlAttr = HtmlAttr::constant("aria-placeholder"); - pub const aria_posinset: HtmlAttr = HtmlAttr::constant("aria-posinset"); - pub const aria_pressed: HtmlAttr = HtmlAttr::constant("aria-pressed"); - pub const aria_readonly: HtmlAttr = HtmlAttr::constant("aria-readonly"); - pub const aria_relevant: HtmlAttr = HtmlAttr::constant("aria-relevant"); - pub const aria_required: HtmlAttr = HtmlAttr::constant("aria-required"); - pub const aria_roledescription: HtmlAttr = HtmlAttr::constant("aria-roledescription"); - pub const aria_rowcount: HtmlAttr = HtmlAttr::constant("aria-rowcount"); - pub const aria_rowindex: HtmlAttr = HtmlAttr::constant("aria-rowindex"); - pub const aria_rowspan: HtmlAttr = HtmlAttr::constant("aria-rowspan"); - pub const aria_selected: HtmlAttr = HtmlAttr::constant("aria-selected"); - pub const aria_setsize: HtmlAttr = HtmlAttr::constant("aria-setsize"); - pub const aria_sort: HtmlAttr = HtmlAttr::constant("aria-sort"); - pub const aria_valuemax: HtmlAttr = HtmlAttr::constant("aria-valuemax"); - pub const aria_valuemin: HtmlAttr = HtmlAttr::constant("aria-valuemin"); - pub const aria_valuenow: HtmlAttr = HtmlAttr::constant("aria-valuenow"); - pub const aria_valuetext: HtmlAttr = HtmlAttr::constant("aria-valuetext"); - pub const r#as: HtmlAttr = HtmlAttr::constant("as"); - pub const r#async: HtmlAttr = HtmlAttr::constant("async"); - pub const autocapitalize: HtmlAttr = HtmlAttr::constant("autocapitalize"); - pub const autocomplete: HtmlAttr = HtmlAttr::constant("autocomplete"); - pub const autocorrect: HtmlAttr = HtmlAttr::constant("autocorrect"); - pub const autofocus: HtmlAttr = HtmlAttr::constant("autofocus"); - pub const autoplay: HtmlAttr = HtmlAttr::constant("autoplay"); - pub const blocking: HtmlAttr = HtmlAttr::constant("blocking"); - pub const charset: HtmlAttr = HtmlAttr::constant("charset"); - pub const checked: HtmlAttr = HtmlAttr::constant("checked"); - pub const cite: HtmlAttr = HtmlAttr::constant("cite"); - pub const class: HtmlAttr = HtmlAttr::constant("class"); - pub const closedby: HtmlAttr = HtmlAttr::constant("closedby"); - pub const color: HtmlAttr = HtmlAttr::constant("color"); - pub const colorspace: HtmlAttr = HtmlAttr::constant("colorspace"); - pub const cols: HtmlAttr = HtmlAttr::constant("cols"); - pub const colspan: HtmlAttr = HtmlAttr::constant("colspan"); - pub const command: HtmlAttr = HtmlAttr::constant("command"); - pub const commandfor: HtmlAttr = HtmlAttr::constant("commandfor"); - pub const content: HtmlAttr = HtmlAttr::constant("content"); - pub const contenteditable: HtmlAttr = HtmlAttr::constant("contenteditable"); - pub const controls: HtmlAttr = HtmlAttr::constant("controls"); - pub const coords: HtmlAttr = HtmlAttr::constant("coords"); - pub const crossorigin: HtmlAttr = HtmlAttr::constant("crossorigin"); - pub const data: HtmlAttr = HtmlAttr::constant("data"); - pub const datetime: HtmlAttr = HtmlAttr::constant("datetime"); - pub const decoding: HtmlAttr = HtmlAttr::constant("decoding"); - pub const default: HtmlAttr = HtmlAttr::constant("default"); - pub const defer: HtmlAttr = HtmlAttr::constant("defer"); - pub const dir: HtmlAttr = HtmlAttr::constant("dir"); - pub const dirname: HtmlAttr = HtmlAttr::constant("dirname"); - pub const disabled: HtmlAttr = HtmlAttr::constant("disabled"); - pub const download: HtmlAttr = HtmlAttr::constant("download"); - pub const draggable: HtmlAttr = HtmlAttr::constant("draggable"); - pub const enctype: HtmlAttr = HtmlAttr::constant("enctype"); - pub const enterkeyhint: HtmlAttr = HtmlAttr::constant("enterkeyhint"); - pub const fetchpriority: HtmlAttr = HtmlAttr::constant("fetchpriority"); - pub const r#for: HtmlAttr = HtmlAttr::constant("for"); - pub const form: HtmlAttr = HtmlAttr::constant("form"); - pub const formaction: HtmlAttr = HtmlAttr::constant("formaction"); - pub const formenctype: HtmlAttr = HtmlAttr::constant("formenctype"); - pub const formmethod: HtmlAttr = HtmlAttr::constant("formmethod"); - pub const formnovalidate: HtmlAttr = HtmlAttr::constant("formnovalidate"); - pub const formtarget: HtmlAttr = HtmlAttr::constant("formtarget"); - pub const headers: HtmlAttr = HtmlAttr::constant("headers"); - pub const height: HtmlAttr = HtmlAttr::constant("height"); - pub const hidden: HtmlAttr = HtmlAttr::constant("hidden"); - pub const high: HtmlAttr = HtmlAttr::constant("high"); - pub const href: HtmlAttr = HtmlAttr::constant("href"); - pub const hreflang: HtmlAttr = HtmlAttr::constant("hreflang"); - pub const http_equiv: HtmlAttr = HtmlAttr::constant("http-equiv"); - pub const id: HtmlAttr = HtmlAttr::constant("id"); - pub const imagesizes: HtmlAttr = HtmlAttr::constant("imagesizes"); - pub const imagesrcset: HtmlAttr = HtmlAttr::constant("imagesrcset"); - pub const inert: HtmlAttr = HtmlAttr::constant("inert"); - pub const inputmode: HtmlAttr = HtmlAttr::constant("inputmode"); - pub const integrity: HtmlAttr = HtmlAttr::constant("integrity"); - pub const is: HtmlAttr = HtmlAttr::constant("is"); - pub const ismap: HtmlAttr = HtmlAttr::constant("ismap"); - pub const itemid: HtmlAttr = HtmlAttr::constant("itemid"); - pub const itemprop: HtmlAttr = HtmlAttr::constant("itemprop"); - pub const itemref: HtmlAttr = HtmlAttr::constant("itemref"); - pub const itemscope: HtmlAttr = HtmlAttr::constant("itemscope"); - pub const itemtype: HtmlAttr = HtmlAttr::constant("itemtype"); - pub const kind: HtmlAttr = HtmlAttr::constant("kind"); - pub const label: HtmlAttr = HtmlAttr::constant("label"); - pub const lang: HtmlAttr = HtmlAttr::constant("lang"); - pub const list: HtmlAttr = HtmlAttr::constant("list"); - pub const loading: HtmlAttr = HtmlAttr::constant("loading"); - pub const r#loop: HtmlAttr = HtmlAttr::constant("loop"); - pub const low: HtmlAttr = HtmlAttr::constant("low"); - pub const max: HtmlAttr = HtmlAttr::constant("max"); - pub const maxlength: HtmlAttr = HtmlAttr::constant("maxlength"); - pub const media: HtmlAttr = HtmlAttr::constant("media"); - pub const method: HtmlAttr = HtmlAttr::constant("method"); - pub const min: HtmlAttr = HtmlAttr::constant("min"); - pub const minlength: HtmlAttr = HtmlAttr::constant("minlength"); - pub const multiple: HtmlAttr = HtmlAttr::constant("multiple"); - pub const muted: HtmlAttr = HtmlAttr::constant("muted"); - pub const name: HtmlAttr = HtmlAttr::constant("name"); - pub const nomodule: HtmlAttr = HtmlAttr::constant("nomodule"); - pub const nonce: HtmlAttr = HtmlAttr::constant("nonce"); - pub const novalidate: HtmlAttr = HtmlAttr::constant("novalidate"); - pub const open: HtmlAttr = HtmlAttr::constant("open"); - pub const optimum: HtmlAttr = HtmlAttr::constant("optimum"); - pub const pattern: HtmlAttr = HtmlAttr::constant("pattern"); - pub const ping: HtmlAttr = HtmlAttr::constant("ping"); - pub const placeholder: HtmlAttr = HtmlAttr::constant("placeholder"); - pub const playsinline: HtmlAttr = HtmlAttr::constant("playsinline"); - pub const popover: HtmlAttr = HtmlAttr::constant("popover"); - pub const popovertarget: HtmlAttr = HtmlAttr::constant("popovertarget"); - pub const popovertargetaction: HtmlAttr = HtmlAttr::constant("popovertargetaction"); - pub const poster: HtmlAttr = HtmlAttr::constant("poster"); - pub const preload: HtmlAttr = HtmlAttr::constant("preload"); - pub const readonly: HtmlAttr = HtmlAttr::constant("readonly"); - pub const referrerpolicy: HtmlAttr = HtmlAttr::constant("referrerpolicy"); - pub const rel: HtmlAttr = HtmlAttr::constant("rel"); - pub const required: HtmlAttr = HtmlAttr::constant("required"); - pub const reversed: HtmlAttr = HtmlAttr::constant("reversed"); - pub const role: HtmlAttr = HtmlAttr::constant("role"); - pub const rows: HtmlAttr = HtmlAttr::constant("rows"); - pub const rowspan: HtmlAttr = HtmlAttr::constant("rowspan"); - pub const sandbox: HtmlAttr = HtmlAttr::constant("sandbox"); - pub const scope: HtmlAttr = HtmlAttr::constant("scope"); - pub const selected: HtmlAttr = HtmlAttr::constant("selected"); - pub const shadowrootclonable: HtmlAttr = HtmlAttr::constant("shadowrootclonable"); - pub const shadowrootcustomelementregistry: HtmlAttr = HtmlAttr::constant("shadowrootcustomelementregistry"); - pub const shadowrootdelegatesfocus: HtmlAttr = HtmlAttr::constant("shadowrootdelegatesfocus"); - pub const shadowrootmode: HtmlAttr = HtmlAttr::constant("shadowrootmode"); - pub const shadowrootserializable: HtmlAttr = HtmlAttr::constant("shadowrootserializable"); - pub const shape: HtmlAttr = HtmlAttr::constant("shape"); - pub const size: HtmlAttr = HtmlAttr::constant("size"); - pub const sizes: HtmlAttr = HtmlAttr::constant("sizes"); - pub const slot: HtmlAttr = HtmlAttr::constant("slot"); - pub const span: HtmlAttr = HtmlAttr::constant("span"); - pub const spellcheck: HtmlAttr = HtmlAttr::constant("spellcheck"); - pub const src: HtmlAttr = HtmlAttr::constant("src"); - pub const srcdoc: HtmlAttr = HtmlAttr::constant("srcdoc"); - pub const srclang: HtmlAttr = HtmlAttr::constant("srclang"); - pub const srcset: HtmlAttr = HtmlAttr::constant("srcset"); - pub const start: HtmlAttr = HtmlAttr::constant("start"); - pub const step: HtmlAttr = HtmlAttr::constant("step"); - pub const style: HtmlAttr = HtmlAttr::constant("style"); - pub const tabindex: HtmlAttr = HtmlAttr::constant("tabindex"); - pub const target: HtmlAttr = HtmlAttr::constant("target"); - pub const title: HtmlAttr = HtmlAttr::constant("title"); - pub const translate: HtmlAttr = HtmlAttr::constant("translate"); - pub const r#type: HtmlAttr = HtmlAttr::constant("type"); - pub const usemap: HtmlAttr = HtmlAttr::constant("usemap"); - pub const value: HtmlAttr = HtmlAttr::constant("value"); - pub const width: HtmlAttr = HtmlAttr::constant("width"); - pub const wrap: HtmlAttr = HtmlAttr::constant("wrap"); - pub const writingsuggestions: HtmlAttr = HtmlAttr::constant("writingsuggestions"); -} diff --git a/crates/typst-library/src/introspection/counter.rs b/crates/typst-library/src/introspection/counter.rs index a7925e13a..b3c52de4e 100644 --- a/crates/typst-library/src/introspection/counter.rs +++ b/crates/typst-library/src/introspection/counter.rs @@ -12,7 +12,7 @@ use crate::engine::{Engine, Route, Sink, Traced}; use crate::foundations::{ cast, elem, func, scope, select_where, ty, Args, Array, Construct, Content, Context, Element, Func, IntoValue, Label, LocatableSelector, NativeElement, Packed, Repr, - Selector, Show, Smart, Str, StyleChain, Value, + Selector, ShowFn, Smart, Str, StyleChain, Value, }; use crate::introspection::{Introspector, Locatable, Location, Tag}; use crate::layout::{Frame, FrameItem, PageElem}; @@ -683,8 +683,8 @@ cast! { } /// Executes an update of a counter. -#[elem(Construct, Locatable, Show, Count)] -struct CounterUpdateElem { +#[elem(Construct, Locatable, Count)] +pub struct CounterUpdateElem { /// The key that identifies the counter. #[required] key: CounterKey, @@ -701,12 +701,6 @@ impl Construct for CounterUpdateElem { } } -impl Show for Packed { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(Content::empty()) - } -} - impl Count for Packed { fn update(&self) -> Option { Some(self.update.clone()) @@ -714,7 +708,7 @@ impl Count for Packed { } /// Executes a display of a counter. -#[elem(Construct, Locatable, Show)] +#[elem(Construct, Locatable)] pub struct CounterDisplayElem { /// The counter. #[required] @@ -738,20 +732,18 @@ impl Construct for CounterDisplayElem { } } -impl Show for Packed { - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(self - .counter - .display_impl( - engine, - self.location().unwrap(), - self.numbering.clone(), - self.both, - Some(styles), - )? - .display()) - } -} +pub const COUNTER_DISPLAY_RULE: ShowFn = |elem, engine, styles| { + Ok(elem + .counter + .display_impl( + engine, + elem.location().unwrap(), + elem.numbering.clone(), + elem.both, + Some(styles), + )? + .display()) +}; /// An specialized handler of the page counter that tracks both the physical /// and the logical page counter. diff --git a/crates/typst-library/src/introspection/introspector.rs b/crates/typst-library/src/introspection/introspector.rs index d2ad0525b..de74c55f5 100644 --- a/crates/typst-library/src/introspection/introspector.rs +++ b/crates/typst-library/src/introspection/introspector.rs @@ -10,9 +10,8 @@ use typst_utils::NonZeroExt; use crate::diag::{bail, StrResult}; use crate::foundations::{Content, Label, Repr, Selector}; -use crate::html::HtmlNode; use crate::introspection::{Location, Tag}; -use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform}; +use crate::layout::{Frame, FrameItem, Point, Position, Transform}; use crate::model::Numbering; /// Can be queried for elements and their positions. @@ -47,18 +46,6 @@ pub struct Introspector { type Pair = (Content, Position); impl Introspector { - /// Creates an introspector for a page list. - #[typst_macros::time(name = "introspect pages")] - pub fn paged(pages: &[Page]) -> Self { - IntrospectorBuilder::new().build_paged(pages) - } - - /// Creates an introspector for HTML. - #[typst_macros::time(name = "introspect html")] - pub fn html(output: &[HtmlNode]) -> Self { - IntrospectorBuilder::new().build_html(output) - } - /// Iterates over all locatable elements. pub fn all(&self) -> impl Iterator + '_ { self.elems.iter().map(|(c, _)| c) @@ -352,10 +339,10 @@ impl Clone for QueryCache { /// Builds the introspector. #[derive(Default)] -struct IntrospectorBuilder { - pages: usize, - page_numberings: Vec>, - page_supplements: Vec, +pub struct IntrospectorBuilder { + pub pages: usize, + pub page_numberings: Vec>, + pub page_supplements: Vec, seen: HashSet, insertions: MultiMap>, keys: MultiMap, @@ -365,41 +352,12 @@ struct IntrospectorBuilder { impl IntrospectorBuilder { /// Create an empty builder. - fn new() -> Self { + pub fn new() -> Self { Self::default() } - /// Build an introspector for a page list. - fn build_paged(mut self, pages: &[Page]) -> Introspector { - self.pages = pages.len(); - self.page_numberings.reserve(pages.len()); - self.page_supplements.reserve(pages.len()); - - // Discover all elements. - let mut elems = Vec::new(); - for (i, page) in pages.iter().enumerate() { - self.page_numberings.push(page.numbering.clone()); - self.page_supplements.push(page.supplement.clone()); - self.discover_in_frame( - &mut elems, - &page.frame, - NonZeroUsize::new(1 + i).unwrap(), - Transform::identity(), - ); - } - - self.finalize(elems) - } - - /// Build an introspector for an HTML document. - fn build_html(mut self, output: &[HtmlNode]) -> Introspector { - let mut elems = Vec::new(); - self.discover_in_html(&mut elems, output); - self.finalize(elems) - } - /// Processes the tags in the frame. - fn discover_in_frame( + pub fn discover_in_frame( &mut self, sink: &mut Vec, frame: &Frame, @@ -433,29 +391,13 @@ impl IntrospectorBuilder { } } - /// Processes the tags in the HTML element. - fn discover_in_html(&mut self, sink: &mut Vec, nodes: &[HtmlNode]) { - for node in nodes { - match node { - HtmlNode::Tag(tag) => self.discover_in_tag( - sink, - tag, - Position { page: NonZeroUsize::ONE, point: Point::zero() }, - ), - HtmlNode::Text(_, _) => {} - HtmlNode::Element(elem) => self.discover_in_html(sink, &elem.children), - HtmlNode::Frame(frame) => self.discover_in_frame( - sink, - &frame.inner, - NonZeroUsize::ONE, - Transform::identity(), - ), - } - } - } - /// Handle a tag. - fn discover_in_tag(&mut self, sink: &mut Vec, tag: &Tag, position: Position) { + pub fn discover_in_tag( + &mut self, + sink: &mut Vec, + tag: &Tag, + position: Position, + ) { match tag { Tag::Start(elem) => { let loc = elem.location().unwrap(); @@ -471,7 +413,7 @@ impl IntrospectorBuilder { /// Build a complete introspector with all acceleration structures from a /// list of top-level pairs. - fn finalize(mut self, root: Vec) -> Introspector { + pub fn finalize(mut self, root: Vec) -> Introspector { self.locations.reserve(self.seen.len()); // Save all pairs and their descendants in the correct order. diff --git a/crates/typst-library/src/introspection/metadata.rs b/crates/typst-library/src/introspection/metadata.rs index 06000174f..8ad74b96b 100644 --- a/crates/typst-library/src/introspection/metadata.rs +++ b/crates/typst-library/src/introspection/metadata.rs @@ -1,6 +1,4 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{elem, Content, Packed, Show, StyleChain, Value}; +use crate::foundations::{elem, Value}; use crate::introspection::Locatable; /// Exposes a value to the query system without producing visible content. @@ -24,15 +22,9 @@ use crate::introspection::Locatable; /// query().first().value /// } /// ``` -#[elem(Show, Locatable)] +#[elem(Locatable)] pub struct MetadataElem { /// The value to embed into the document. #[required] pub value: Value, } - -impl Show for Packed { - fn show(&self, _: &mut Engine, _styles: StyleChain) -> SourceResult { - Ok(Content::empty()) - } -} diff --git a/crates/typst-library/src/introspection/state.rs b/crates/typst-library/src/introspection/state.rs index 784f2acb2..2d15a5de0 100644 --- a/crates/typst-library/src/introspection/state.rs +++ b/crates/typst-library/src/introspection/state.rs @@ -6,8 +6,7 @@ use crate::diag::{bail, At, SourceResult}; use crate::engine::{Engine, Route, Sink, Traced}; use crate::foundations::{ cast, elem, func, scope, select_where, ty, Args, Construct, Content, Context, Func, - LocatableSelector, NativeElement, Packed, Repr, Selector, Show, Str, StyleChain, - Value, + LocatableSelector, NativeElement, Repr, Selector, Str, Value, }; use crate::introspection::{Introspector, Locatable, Location}; use crate::routines::Routines; @@ -372,8 +371,8 @@ cast! { } /// Executes a display of a state. -#[elem(Construct, Locatable, Show)] -struct StateUpdateElem { +#[elem(Construct, Locatable)] +pub struct StateUpdateElem { /// The key that identifies the state. #[required] key: Str, @@ -389,9 +388,3 @@ impl Construct for StateUpdateElem { bail!(args.span, "cannot be constructed manually"); } } - -impl Show for Packed { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(Content::empty()) - } -} diff --git a/crates/typst-library/src/layout/align.rs b/crates/typst-library/src/layout/align.rs index e5ceddf65..447648f01 100644 --- a/crates/typst-library/src/layout/align.rs +++ b/crates/typst-library/src/layout/align.rs @@ -2,11 +2,10 @@ use std::ops::Add; use ecow::{eco_format, EcoString}; -use crate::diag::{bail, HintedStrResult, SourceResult, StrResult}; -use crate::engine::Engine; +use crate::diag::{bail, HintedStrResult, StrResult}; use crate::foundations::{ - cast, elem, func, scope, ty, CastInfo, Content, Fold, FromValue, IntoValue, Packed, - Reflect, Repr, Resolve, Show, StyleChain, Value, + cast, elem, func, scope, ty, CastInfo, Content, Fold, FromValue, IntoValue, Reflect, + Repr, Resolve, StyleChain, Value, }; use crate::layout::{Abs, Axes, Axis, Dir, Side}; use crate::text::TextElem; @@ -73,7 +72,7 @@ use crate::text::TextElem; /// ```example /// Start #h(1fr) End /// ``` -#[elem(Show)] +#[elem] pub struct AlignElem { /// The [alignment] along both axes. /// @@ -97,13 +96,6 @@ pub struct AlignElem { pub body: Content, } -impl Show for Packed { - #[typst_macros::time(name = "align", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(self.body.clone().aligned(self.alignment.get(styles))) - } -} - /// Where to align something along an axis. /// /// Possible values are: diff --git a/crates/typst-library/src/layout/columns.rs b/crates/typst-library/src/layout/columns.rs index 1cea52759..e7bce393b 100644 --- a/crates/typst-library/src/layout/columns.rs +++ b/crates/typst-library/src/layout/columns.rs @@ -1,9 +1,7 @@ use std::num::NonZeroUsize; -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain}; -use crate::layout::{BlockElem, Length, Ratio, Rel}; +use crate::foundations::{elem, Content}; +use crate::layout::{Length, Ratio, Rel}; /// Separates a region into multiple equally sized columns. /// @@ -41,7 +39,7 @@ use crate::layout::{BlockElem, Length, Ratio, Rel}; /// /// #lorem(40) /// ``` -#[elem(Show)] +#[elem] pub struct ColumnsElem { /// The number of columns. #[positional] @@ -57,14 +55,6 @@ pub struct ColumnsElem { pub body: Content, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_columns) - .pack() - .spanned(self.span())) - } -} - /// Forces a column break. /// /// The function will behave like a [page break]($pagebreak) when used in a diff --git a/crates/typst-library/src/layout/grid/mod.rs b/crates/typst-library/src/layout/grid/mod.rs index f66d114a3..bf03d9387 100644 --- a/crates/typst-library/src/layout/grid/mod.rs +++ b/crates/typst-library/src/layout/grid/mod.rs @@ -11,11 +11,11 @@ use crate::diag::{bail, At, HintedStrResult, HintedString, SourceResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, scope, Array, CastInfo, Content, Context, Fold, FromValue, Func, - IntoValue, NativeElement, Packed, Reflect, Resolve, Show, Smart, StyleChain, Value, + IntoValue, Packed, Reflect, Resolve, Smart, StyleChain, Value, }; use crate::introspection::Locatable; use crate::layout::{ - Alignment, BlockElem, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, Sizing, + Alignment, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, Sizing, }; use crate::model::{TableCell, TableFooter, TableHLine, TableHeader, TableVLine}; use crate::visualize::{Paint, Stroke}; @@ -137,7 +137,7 @@ use crate::visualize::{Paint, Stroke}; /// /// Furthermore, strokes of a repeated grid header or footer will take /// precedence over regular cell strokes. -#[elem(scope, Locatable, Show)] +#[elem(scope, Locatable)] pub struct GridElem { /// The column sizes. /// @@ -321,14 +321,6 @@ impl GridElem { type GridFooter; } -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_grid) - .pack() - .spanned(self.span())) - } -} - /// Track sizing definitions. #[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] pub struct TrackSizings(pub SmallVec<[Sizing; 4]>); @@ -649,7 +641,7 @@ pub struct GridVLine { /// which allows you, for example, to apply styles based on a cell's position. /// Refer to the examples of the [`table.cell`]($table.cell) element to learn /// more about this. -#[elem(name = "cell", title = "Grid Cell", Locatable, Show)] +#[elem(name = "cell", title = "Grid Cell", Locatable)] pub struct GridCell { /// The cell's body. #[required] @@ -749,12 +741,6 @@ cast! { v: Content => v.into(), } -impl Show for Packed { - fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult { - show_grid_cell(self.body.clone(), self.inset.get(styles), self.align.get(styles)) - } -} - impl Default for Packed { fn default() -> Self { Packed::new( @@ -775,28 +761,6 @@ impl From for GridCell { } } -/// Function with common code to display a grid cell or table cell. -pub(crate) fn show_grid_cell( - mut body: Content, - inset: Smart>>>, - align: Smart, -) -> SourceResult { - let inset = inset.unwrap_or_default().map(Option::unwrap_or_default); - - if inset != Sides::default() { - // Only pad if some inset is not 0pt. - // Avoids a bug where using .padded() in any way inside Show causes - // alignment in align(...) to break. - body = body.padded(inset); - } - - if let Smart::Custom(alignment) = align { - body = body.aligned(alignment); - } - - Ok(body) -} - /// A value that can be configured per cell. #[derive(Debug, Clone, PartialEq, Hash)] pub enum Celled { diff --git a/crates/typst-library/src/layout/hide.rs b/crates/typst-library/src/layout/hide.rs index 377b08a7c..c667ef979 100644 --- a/crates/typst-library/src/layout/hide.rs +++ b/crates/typst-library/src/layout/hide.rs @@ -1,6 +1,4 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{elem, Content, Packed, Show, StyleChain}; +use crate::foundations::{elem, Content}; use crate::introspection::Locatable; /// Hides content without affecting layout. @@ -15,7 +13,7 @@ use crate::introspection::Locatable; /// Hello Jane \ /// #hide[Hello] Joe /// ``` -#[elem(Locatable, Show)] +#[elem(Locatable)] pub struct HideElem { /// The content to hide. #[required] @@ -26,10 +24,3 @@ pub struct HideElem { #[ghost] pub hidden: bool, } - -impl Show for Packed { - #[typst_macros::time(name = "hide", span = self.span())] - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(self.body.clone().set(HideElem::hidden, true)) - } -} diff --git a/crates/typst-library/src/layout/layout.rs b/crates/typst-library/src/layout/layout.rs index 46271ff22..00897bcfe 100644 --- a/crates/typst-library/src/layout/layout.rs +++ b/crates/typst-library/src/layout/layout.rs @@ -1,13 +1,7 @@ -use comemo::Track; use typst_syntax::Span; -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{ - dict, elem, func, Content, Context, Func, NativeElement, Packed, Show, StyleChain, -}; +use crate::foundations::{elem, func, Content, Func, NativeElement}; use crate::introspection::Locatable; -use crate::layout::{BlockElem, Size}; /// Provides access to the current outer container's (or page's, if none) /// dimensions (width and height). @@ -86,37 +80,9 @@ pub fn layout( } /// Executes a `layout` call. -#[elem(Locatable, Show)] -struct LayoutElem { +#[elem(Locatable)] +pub struct LayoutElem { /// The function to call with the outer container's (or page's) size. #[required] - func: Func, -} - -impl Show for Packed { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::multi_layouter( - self.clone(), - |elem, engine, locator, styles, regions| { - // Gets the current region's base size, which will be the size of the - // outer container, or of the page if there is no such container. - let Size { x, y } = regions.base(); - let loc = elem.location().unwrap(); - let context = Context::new(Some(loc), Some(styles)); - let result = elem - .func - .call( - engine, - context.track(), - [dict! { "width" => x, "height" => y }], - )? - .display(); - (engine.routines.layout_fragment)( - engine, &result, locator, styles, regions, - ) - }, - ) - .pack() - .spanned(self.span())) - } + pub func: Func, } diff --git a/crates/typst-library/src/layout/pad.rs b/crates/typst-library/src/layout/pad.rs index 1dc6d1316..d533df35b 100644 --- a/crates/typst-library/src/layout/pad.rs +++ b/crates/typst-library/src/layout/pad.rs @@ -1,7 +1,5 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain}; -use crate::layout::{BlockElem, Length, Rel}; +use crate::foundations::{elem, Content}; +use crate::layout::{Length, Rel}; /// Adds spacing around content. /// @@ -16,7 +14,7 @@ use crate::layout::{BlockElem, Length, Rel}; /// _Typing speeds can be /// measured in words per minute._ /// ``` -#[elem(title = "Padding", Show)] +#[elem(title = "Padding")] pub struct PadElem { /// The padding at the left side. #[parse( @@ -55,11 +53,3 @@ pub struct PadElem { #[required] pub body: Content, } - -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_pad) - .pack() - .spanned(self.span())) - } -} diff --git a/crates/typst-library/src/layout/repeat.rs b/crates/typst-library/src/layout/repeat.rs index ab042ceb1..c3cff4deb 100644 --- a/crates/typst-library/src/layout/repeat.rs +++ b/crates/typst-library/src/layout/repeat.rs @@ -1,8 +1,6 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain}; +use crate::foundations::{elem, Content}; use crate::introspection::Locatable; -use crate::layout::{BlockElem, Length}; +use crate::layout::Length; /// Repeats content to the available space. /// @@ -25,7 +23,7 @@ use crate::layout::{BlockElem, Length}; /// Berlin, the 22nd of December, 2022 /// ] /// ``` -#[elem(Locatable, Show)] +#[elem(Locatable)] pub struct RepeatElem { /// The content to repeat. #[required] @@ -40,11 +38,3 @@ pub struct RepeatElem { #[default(true)] pub justify: bool, } - -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_repeat) - .pack() - .spanned(self.span())) - } -} diff --git a/crates/typst-library/src/layout/stack.rs b/crates/typst-library/src/layout/stack.rs index 5fc78480e..fca1ecb86 100644 --- a/crates/typst-library/src/layout/stack.rs +++ b/crates/typst-library/src/layout/stack.rs @@ -1,9 +1,7 @@ use std::fmt::{self, Debug, Formatter}; -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{cast, elem, Content, NativeElement, Packed, Show, StyleChain}; -use crate::layout::{BlockElem, Dir, Spacing}; +use crate::foundations::{cast, elem, Content}; +use crate::layout::{Dir, Spacing}; /// Arranges content and spacing horizontally or vertically. /// @@ -19,7 +17,7 @@ use crate::layout::{BlockElem, Dir, Spacing}; /// rect(width: 90pt), /// ) /// ``` -#[elem(Show)] +#[elem] pub struct StackElem { /// The direction along which the items are stacked. Possible values are: /// @@ -47,14 +45,6 @@ pub struct StackElem { pub children: Vec, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_stack) - .pack() - .spanned(self.span())) - } -} - /// A child of a stack element. #[derive(Clone, PartialEq, Hash)] pub enum StackChild { diff --git a/crates/typst-library/src/layout/transform.rs b/crates/typst-library/src/layout/transform.rs index d153d97db..c2d9a21c7 100644 --- a/crates/typst-library/src/layout/transform.rs +++ b/crates/typst-library/src/layout/transform.rs @@ -1,11 +1,5 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{ - cast, elem, Content, NativeElement, Packed, Show, Smart, StyleChain, -}; -use crate::layout::{ - Abs, Alignment, Angle, BlockElem, HAlignment, Length, Ratio, Rel, VAlignment, -}; +use crate::foundations::{cast, elem, Content, Smart}; +use crate::layout::{Abs, Alignment, Angle, HAlignment, Length, Ratio, Rel, VAlignment}; /// Moves content without affecting layout. /// @@ -25,7 +19,7 @@ use crate::layout::{ /// ) /// )) /// ``` -#[elem(Show)] +#[elem] pub struct MoveElem { /// The horizontal displacement of the content. pub dx: Rel, @@ -38,14 +32,6 @@ pub struct MoveElem { pub body: Content, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_move) - .pack() - .spanned(self.span())) - } -} - /// Rotates content without affecting layout. /// /// Rotates an element by a given angle. The layout will act as if the element @@ -60,7 +46,7 @@ impl Show for Packed { /// .map(i => rotate(24deg * i)[X]), /// ) /// ``` -#[elem(Show)] +#[elem] pub struct RotateElem { /// The amount of rotation. /// @@ -107,14 +93,6 @@ pub struct RotateElem { pub body: Content, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_rotate) - .pack() - .spanned(self.span())) - } -} - /// Scales content without affecting layout. /// /// Lets you mirror content by specifying a negative scale on a single axis. @@ -125,7 +103,7 @@ impl Show for Packed { /// #scale(x: -100%)[This is mirrored.] /// #scale(x: -100%, reflow: true)[This is mirrored.] /// ``` -#[elem(Show)] +#[elem] pub struct ScaleElem { /// The scaling factor for both axes, as a positional argument. This is just /// an optional shorthand notation for setting `x` and `y` to the same @@ -179,14 +157,6 @@ pub struct ScaleElem { pub body: Content, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_scale) - .pack() - .spanned(self.span())) - } -} - /// To what size something shall be scaled. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum ScaleAmount { @@ -215,7 +185,7 @@ cast! { /// This is some fake italic text. /// ] /// ``` -#[elem(Show)] +#[elem] pub struct SkewElem { /// The horizontal skewing angle. /// @@ -265,14 +235,6 @@ pub struct SkewElem { pub body: Content, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_skew) - .pack() - .spanned(self.span())) - } -} - /// A scale-skew-translate transformation. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct Transform { diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs index fa7977888..025e997c6 100644 --- a/crates/typst-library/src/lib.rs +++ b/crates/typst-library/src/lib.rs @@ -15,7 +15,6 @@ extern crate self as typst_library; pub mod diag; pub mod engine; pub mod foundations; -pub mod html; pub mod introspection; pub mod layout; pub mod loading; @@ -36,6 +35,7 @@ use typst_utils::{LazyHash, SmallBitSet}; use crate::diag::FileResult; use crate::foundations::{Array, Binding, Bytes, Datetime, Dict, Module, Scope, Styles}; use crate::layout::{Alignment, Dir}; +use crate::routines::Routines; use crate::text::{Font, FontBook}; use crate::visualize::Color; @@ -139,6 +139,11 @@ impl WorldExt for T { } /// Definition of Typst's standard library. +/// +/// To create and configure the standard library, use the `LibraryExt` trait +/// and call +/// - `Library::default()` for a standard configuration +/// - `Library::builder().build()` if you want to customize the library #[derive(Debug, Clone, Hash)] pub struct Library { /// The module that contains the definitions that are available everywhere. @@ -154,30 +159,27 @@ pub struct Library { pub features: Features, } -impl Library { - /// Create a new builder for a library. - pub fn builder() -> LibraryBuilder { - LibraryBuilder::default() - } -} - -impl Default for Library { - /// Constructs the standard library with the default configuration. - fn default() -> Self { - Self::builder().build() - } -} - /// Configurable builder for the standard library. /// -/// This struct is created by [`Library::builder`]. -#[derive(Debug, Clone, Default)] +/// Constructed via the `LibraryExt` trait. +#[derive(Debug, Clone)] pub struct LibraryBuilder { + routines: &'static Routines, inputs: Option, features: Features, } impl LibraryBuilder { + /// Creates a new builder. + #[doc(hidden)] + pub fn from_routines(routines: &'static Routines) -> Self { + Self { + routines, + inputs: None, + features: Features::default(), + } + } + /// Configure the inputs visible through `sys.inputs`. pub fn with_inputs(mut self, inputs: Dict) -> Self { self.inputs = Some(inputs); @@ -196,7 +198,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, @@ -278,7 +280,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); @@ -293,7 +300,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/math/equation.rs b/crates/typst-library/src/math/equation.rs index b97bb18da..a2ae54471 100644 --- a/crates/typst-library/src/math/equation.rs +++ b/crates/typst-library/src/math/equation.rs @@ -1,20 +1,19 @@ use std::num::NonZeroUsize; +use codex::styling::MathVariant; use typst_utils::NonZeroExt; use unicode_math_class::MathClass; use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{ - elem, Content, NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles, - Synthesize, + elem, Content, NativeElement, Packed, ShowSet, Smart, StyleChain, Styles, Synthesize, }; use crate::introspection::{Count, Counter, CounterUpdate, Locatable}; use crate::layout::{ - AlignElem, Alignment, BlockElem, InlineElem, OuterHAlignment, SpecificAlignment, - VAlignment, + AlignElem, Alignment, BlockElem, OuterHAlignment, SpecificAlignment, VAlignment, }; -use crate::math::{MathSize, MathVariant}; +use crate::math::MathSize; use crate::model::{Numbering, Outlinable, ParLine, Refable, Supplement}; use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem}; @@ -46,7 +45,7 @@ use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem}; /// least one space lifts it into a separate block that is centered /// horizontally. For more details about math syntax, see the /// [main math page]($category/math). -#[elem(Locatable, Synthesize, Show, ShowSet, Count, LocalName, Refable, Outlinable)] +#[elem(Locatable, Synthesize, ShowSet, Count, LocalName, Refable, Outlinable)] pub struct EquationElem { /// Whether the equation is displayed as a separate block. #[default(false)] @@ -113,7 +112,7 @@ pub struct EquationElem { /// The style variant to select. #[internal] #[ghost] - pub variant: MathVariant, + pub variant: Option, /// Affects the height of exponents. #[internal] @@ -130,7 +129,7 @@ pub struct EquationElem { /// Whether to use italic glyphs. #[internal] #[ghost] - pub italic: Smart, + pub italic: Option, /// A forced class to use for all fragment. #[internal] @@ -165,23 +164,6 @@ impl Synthesize for Packed { } } -impl Show for Packed { - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - if self.block.get(styles) { - Ok(BlockElem::multi_layouter( - self.clone(), - engine.routines.layout_equation_block, - ) - .pack() - .spanned(self.span())) - } else { - Ok(InlineElem::layouter(self.clone(), engine.routines.layout_equation_inline) - .pack() - .spanned(self.span())) - } - } -} - impl ShowSet for Packed { fn show_set(&self, styles: StyleChain) -> Styles { let mut out = Styles::new(); diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs index 5daa3e358..79e047cdd 100644 --- a/crates/typst-library/src/math/mod.rs +++ b/crates/typst-library/src/math/mod.rs @@ -81,6 +81,7 @@ pub fn module() -> Module { math.define_func::(); math.define_func::(); math.define_func::(); + math.define_func::(); math.define_func::(); math.define_func::(); math.define_func::(); diff --git a/crates/typst-library/src/math/style.rs b/crates/typst-library/src/math/style.rs index 53242e6e0..6a85fd123 100644 --- a/crates/typst-library/src/math/style.rs +++ b/crates/typst-library/src/math/style.rs @@ -1,4 +1,6 @@ -use crate::foundations::{func, Cast, Content, Smart}; +use codex::styling::MathVariant; + +use crate::foundations::{func, Cast, Content}; use crate::math::EquationElem; /// Bold font style in math. @@ -24,7 +26,7 @@ pub fn upright( /// The content to style. body: Content, ) -> Content { - body.set(EquationElem::italic, Smart::Custom(false)) + body.set(EquationElem::italic, Some(false)) } /// Italic font style in math. @@ -35,7 +37,7 @@ pub fn italic( /// The content to style. body: Content, ) -> Content { - body.set(EquationElem::italic, Smart::Custom(true)) + body.set(EquationElem::italic, Some(true)) } /// Serif (roman) font style in math. @@ -46,7 +48,7 @@ pub fn serif( /// The content to style. body: Content, ) -> Content { - body.set(EquationElem::variant, MathVariant::Serif) + body.set(EquationElem::variant, Some(MathVariant::Plain)) } /// Sans-serif font style in math. @@ -59,23 +61,39 @@ pub fn sans( /// The content to style. body: Content, ) -> Content { - body.set(EquationElem::variant, MathVariant::Sans) + body.set(EquationElem::variant, Some(MathVariant::SansSerif)) } -/// Calligraphic font style in math. +/// Calligraphic (chancery) font style in math. /// /// ```example /// Let $cal(P)$ be the set of ... /// ``` /// -/// This corresponds both to LaTeX's `\mathcal` and `\mathscr` as both of these -/// styles share the same Unicode codepoints. Switching between the styles is -/// thus only possible if supported by the font via -/// [font features]($text.features). +/// This is the default calligraphic/script style for most math fonts. See +/// [`scr`]($math.scr) for more on how to get the other style (roundhand). +#[func(title = "Calligraphic", keywords = ["mathcal", "chancery"])] +pub fn cal( + /// The content to style. + body: Content, +) -> Content { + body.set(EquationElem::variant, Some(MathVariant::Chancery)) +} + +/// Script (roundhand) font style in math. /// -/// For the default math font, the roundhand style is available through the -/// `ss01` feature. Therefore, you could define your own version of `\mathscr` -/// like this: +/// ```example +/// $ scr(S) $ +/// ``` +/// +/// There are two ways that fonts can support differentiating `cal` and `scr`. +/// The first is using Unicode variation sequences. This works out of the box +/// in Typst, however only a few math fonts currently support this. +/// +/// The other way is using [font features]($text.features). For example, the +/// roundhand style might be available in a font through the `ss01` feature. +/// To use it in Typst, you could then define your own version of `scr` like +/// this: /// /// ```example /// #let scr(it) = text( @@ -88,12 +106,12 @@ pub fn sans( /// /// (The box is not conceptually necessary, but unfortunately currently needed /// due to limitations in Typst's text style handling in math.) -#[func(title = "Calligraphic", keywords = ["mathcal", "mathscr"])] -pub fn cal( +#[func(title = "Script Style", keywords = ["mathscr", "roundhand"])] +pub fn scr( /// The content to style. body: Content, ) -> Content { - body.set(EquationElem::variant, MathVariant::Cal) + body.set(EquationElem::variant, Some(MathVariant::Roundhand)) } /// Fraktur font style in math. @@ -106,7 +124,7 @@ pub fn frak( /// The content to style. body: Content, ) -> Content { - body.set(EquationElem::variant, MathVariant::Frak) + body.set(EquationElem::variant, Some(MathVariant::Fraktur)) } /// Monospace font style in math. @@ -119,7 +137,7 @@ pub fn mono( /// The content to style. body: Content, ) -> Content { - body.set(EquationElem::variant, MathVariant::Mono) + body.set(EquationElem::variant, Some(MathVariant::Monospace)) } /// Blackboard bold (double-struck) font style in math. @@ -137,7 +155,7 @@ pub fn bb( /// The content to style. body: Content, ) -> Content { - body.set(EquationElem::variant, MathVariant::Bb) + body.set(EquationElem::variant, Some(MathVariant::DoubleStruck)) } /// Forced display style in math. @@ -240,15 +258,3 @@ pub enum MathSize { /// Math on its own line. Display, } - -/// A mathematical style variant, as defined by Unicode. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Cast, Hash)] -pub enum MathVariant { - #[default] - Serif, - Sans, - Cal, - Frak, - Mono, - Bb, -} diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index 6cbe4377f..4cc252f5b 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -2,7 +2,6 @@ use std::any::TypeId; use std::collections::HashMap; use std::ffi::OsStr; use std::fmt::{self, Debug, Formatter}; -use std::num::NonZeroUsize; use std::path::Path; use std::sync::{Arc, LazyLock}; @@ -17,7 +16,7 @@ use hayagriva::{ use indexmap::IndexMap; use smallvec::{smallvec, SmallVec}; use typst_syntax::{Span, Spanned, SyntaxMode}; -use typst_utils::{Get, ManuallyHash, NonZeroExt, PicoStr}; +use typst_utils::{ManuallyHash, PicoStr}; use crate::diag::{ bail, error, At, HintedStrResult, LoadError, LoadResult, LoadedWithin, ReportPos, @@ -26,18 +25,17 @@ use crate::diag::{ use crate::engine::{Engine, Sink}; use crate::foundations::{ elem, Bytes, CastInfo, Content, Derived, FromValue, IntoValue, Label, NativeElement, - OneOrMultiple, Packed, Reflect, Scope, Show, ShowSet, Smart, StyleChain, Styles, + OneOrMultiple, Packed, Reflect, Scope, ShowSet, Smart, StyleChain, Styles, Synthesize, Value, }; use crate::introspection::{Introspector, Locatable, Location}; use crate::layout::{ BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem, - Sides, Sizing, TrackSizings, + Sizing, TrackSizings, }; use crate::loading::{format_yaml_error, DataSource, Load, LoadSource, Loaded}; use crate::model::{ - CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem, - Url, + CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, Url, }; use crate::routines::Routines; use crate::text::{ @@ -88,7 +86,7 @@ use crate::World; /// /// #bibliography("works.bib") /// ``` -#[elem(Locatable, Synthesize, Show, ShowSet, LocalName)] +#[elem(Locatable, Synthesize, ShowSet, LocalName)] pub struct BibliographyElem { /// One or multiple paths to or raw bytes for Hayagriva `.yaml` and/or /// BibLaTeX `.bib` files. @@ -203,84 +201,6 @@ impl Synthesize for Packed { } } -impl Show for Packed { - #[typst_macros::time(name = "bibliography", span = self.span())] - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - const COLUMN_GUTTER: Em = Em::new(0.65); - const INDENT: Em = Em::new(1.5); - - let span = self.span(); - - let mut seq = vec![]; - if let Some(title) = self.title.get_ref(styles).clone().unwrap_or_else(|| { - Some(TextElem::packed(Self::local_name_in(styles)).spanned(span)) - }) { - seq.push( - HeadingElem::new(title) - .with_depth(NonZeroUsize::ONE) - .pack() - .spanned(span), - ); - } - - let works = Works::generate(engine).at(span)?; - let references = works - .references - .as_ref() - .ok_or_else(|| match self.style.get_ref(styles).source { - CslSource::Named(style) => eco_format!( - "CSL style \"{}\" is not suitable for bibliographies", - style.display_name() - ), - CslSource::Normal(..) => { - "CSL style is not suitable for bibliographies".into() - } - }) - .at(span)?; - - if references.iter().any(|(prefix, _)| prefix.is_some()) { - let row_gutter = styles.get(ParElem::spacing); - - let mut cells = vec![]; - for (prefix, reference) in references { - cells.push(GridChild::Item(GridItem::Cell( - Packed::new(GridCell::new(prefix.clone().unwrap_or_default())) - .spanned(span), - ))); - cells.push(GridChild::Item(GridItem::Cell( - Packed::new(GridCell::new(reference.clone())).spanned(span), - ))); - } - seq.push( - GridElem::new(cells) - .with_columns(TrackSizings(smallvec![Sizing::Auto; 2])) - .with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()])) - .with_row_gutter(TrackSizings(smallvec![row_gutter.into()])) - .pack() - .spanned(span), - ); - } else { - for (_, reference) in references { - let realized = reference.clone(); - let block = if works.hanging_indent { - let body = HElem::new((-INDENT).into()).pack() + realized; - let inset = Sides::default() - .with(styles.resolve(TextElem::dir).start(), Some(INDENT.into())); - BlockElem::new() - .with_body(Some(BlockBody::Content(body))) - .with_inset(inset) - } else { - BlockElem::new().with_body(Some(BlockBody::Content(realized))) - }; - - seq.push(block.pack().spanned(span)); - } - } - - Ok(Content::sequence(seq)) - } -} - impl ShowSet for Packed { fn show_set(&self, _: StyleChain) -> Styles { const INDENT: Em = Em::new(1.0); @@ -564,7 +484,7 @@ impl IntoValue for CslSource { /// memoization) for the whole document. This setup is necessary because /// citation formatting is inherently stateful and we need access to all /// citations to do it. -pub(super) struct Works { +pub struct Works { /// Maps from the location of a citation group to its rendered content. pub citations: HashMap>, /// Lists all references in the bibliography, with optional prefix, or diff --git a/crates/typst-library/src/model/cite.rs b/crates/typst-library/src/model/cite.rs index 1ad1b832d..a1e09dccb 100644 --- a/crates/typst-library/src/model/cite.rs +++ b/crates/typst-library/src/model/cite.rs @@ -3,8 +3,7 @@ use typst_syntax::Spanned; use crate::diag::{error, At, HintedString, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, Cast, Content, Derived, Label, Packed, Show, Smart, StyleChain, - Synthesize, + cast, elem, Cast, Content, Derived, Label, Packed, Smart, StyleChain, Synthesize, }; use crate::introspection::Locatable; use crate::model::bibliography::Works; @@ -153,16 +152,15 @@ pub enum CitationForm { /// /// This is automatically created from adjacent citations during show rule /// application. -#[elem(Locatable, Show)] +#[elem(Locatable)] pub struct CiteGroup { /// The citations. #[required] pub children: Vec>, } -impl Show for Packed { - #[typst_macros::time(name = "cite", span = self.span())] - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { +impl Packed { + pub fn realize(&self, engine: &mut Engine) -> SourceResult { let location = self.location().unwrap(); let span = self.span(); Works::generate(engine) diff --git a/crates/typst-library/src/model/emph.rs b/crates/typst-library/src/model/emph.rs index ae47e0d45..6b18542a4 100644 --- a/crates/typst-library/src/model/emph.rs +++ b/crates/typst-library/src/model/emph.rs @@ -1,11 +1,5 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{ - elem, Content, NativeElement, Packed, Show, StyleChain, TargetElem, -}; -use crate::html::{tag, HtmlElem}; +use crate::foundations::{elem, Content}; use crate::introspection::Locatable; -use crate::text::{ItalicToggle, TextElem}; /// Emphasizes content by toggling italics. /// @@ -30,24 +24,9 @@ use crate::text::{ItalicToggle, TextElem}; /// This function also has dedicated syntax: To emphasize content, simply /// enclose it in underscores (`_`). Note that this only works at word /// boundaries. To emphasize part of a word, you have to use the function. -#[elem(title = "Emphasis", keywords = ["italic"], Locatable, Show)] +#[elem(title = "Emphasis", keywords = ["italic"], Locatable)] pub struct EmphElem { /// The content to emphasize. #[required] pub body: Content, } - -impl Show for Packed { - #[typst_macros::time(name = "emph", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let body = self.body.clone(); - Ok(if styles.get(TargetElem::target).is_html() { - HtmlElem::new(tag::em) - .with_body(Some(body)) - .pack() - .spanned(self.span()) - } else { - body.set(TextElem::emph, ItalicToggle(true)) - }) - } -} diff --git a/crates/typst-library/src/model/enum.rs b/crates/typst-library/src/model/enum.rs index d78dad2a0..a93546c37 100644 --- a/crates/typst-library/src/model/enum.rs +++ b/crates/typst-library/src/model/enum.rs @@ -1,20 +1,12 @@ use std::str::FromStr; -use ecow::eco_format; use smallvec::SmallVec; -use crate::diag::{bail, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ - cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain, - Styles, TargetElem, -}; -use crate::html::{attr, tag, HtmlElem}; +use crate::diag::bail; +use crate::foundations::{cast, elem, scope, Array, Content, Packed, Smart, Styles}; use crate::introspection::Locatable; -use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem}; -use crate::model::{ - ListItemLike, ListLike, Numbering, NumberingPattern, ParElem, ParbreakElem, -}; +use crate::layout::{Alignment, Em, HAlignment, Length, VAlignment}; +use crate::model::{ListItemLike, ListLike, Numbering, NumberingPattern}; /// A numbered list. /// @@ -72,7 +64,7 @@ use crate::model::{ /// Enumeration items can contain multiple paragraphs and other block-level /// content. All content that is indented more than an item's marker becomes /// part of that item. -#[elem(scope, title = "Numbered List", Locatable, Show)] +#[elem(scope, title = "Numbered List", Locatable)] pub struct EnumElem { /// Defines the default [spacing]($enum.spacing) of the enumeration. If it /// is `{false}`, the items are spaced apart with @@ -224,51 +216,6 @@ impl EnumElem { type EnumItem; } -impl Show for Packed { - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let tight = self.tight.get(styles); - - if styles.get(TargetElem::target).is_html() { - let mut elem = HtmlElem::new(tag::ol); - if self.reversed.get(styles) { - elem = elem.with_attr(attr::reversed, "reversed"); - } - if let Some(n) = self.start.get(styles).custom() { - elem = elem.with_attr(attr::start, eco_format!("{n}")); - } - let body = Content::sequence(self.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 !tight { - body += ParbreakElem::shared(); - } - li.with_body(Some(body)).pack().spanned(item.span()) - })); - return Ok(elem.with_body(Some(body)).pack().spanned(self.span())); - } - - let mut realized = - BlockElem::multi_layouter(self.clone(), engine.routines.layout_enum) - .pack() - .spanned(self.span()); - - if tight { - let spacing = self - .spacing - .get(styles) - .unwrap_or_else(|| styles.get(ParElem::leading)); - let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack(); - realized = v + realized; - } - - Ok(realized) - } -} - /// An enumeration item. #[elem(name = "item", title = "Numbered List Item", Locatable)] pub struct EnumItem { diff --git a/crates/typst-library/src/model/figure.rs b/crates/typst-library/src/model/figure.rs index 8d0a306b6..a1859cc20 100644 --- a/crates/typst-library/src/model/figure.rs +++ b/crates/typst-library/src/model/figure.rs @@ -9,19 +9,16 @@ use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, scope, select_where, Content, Element, NativeElement, Packed, Selector, - Show, ShowSet, Smart, StyleChain, Styles, Synthesize, TargetElem, + ShowSet, Smart, StyleChain, Styles, Synthesize, }; -use crate::html::{tag, HtmlElem}; use crate::introspection::{ Count, Counter, CounterKey, CounterUpdate, Locatable, Location, }; use crate::layout::{ - AlignElem, Alignment, BlockBody, BlockElem, Em, HAlignment, Length, OuterVAlignment, - PlaceElem, PlacementScope, VAlignment, VElem, -}; -use crate::model::{ - Numbering, NumberingPattern, Outlinable, ParbreakElem, Refable, Supplement, + AlignElem, Alignment, BlockElem, Em, Length, OuterVAlignment, PlacementScope, + VAlignment, }; +use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement}; use crate::text::{Lang, Region, TextElem}; use crate::visualize::ImageElem; @@ -104,7 +101,7 @@ use crate::visualize::ImageElem; /// caption: [I'm up here], /// ) /// ``` -#[elem(scope, Locatable, Synthesize, Count, Show, ShowSet, Refable, Outlinable)] +#[elem(scope, Locatable, Synthesize, Count, ShowSet, Refable, Outlinable)] pub struct FigureElem { /// The content of the figure. Often, an [image]. #[required] @@ -328,65 +325,6 @@ impl Synthesize for Packed { } } -impl Show for Packed { - #[typst_macros::time(name = "figure", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let span = self.span(); - let target = styles.get(TargetElem::target); - let mut realized = self.body.clone(); - - // Build the caption, if any. - if let Some(caption) = self.caption.get_cloned(styles) { - let (first, second) = match caption.position.get(styles) { - OuterVAlignment::Top => (caption.pack(), realized), - OuterVAlignment::Bottom => (realized, caption.pack()), - }; - let mut seq = Vec::with_capacity(3); - seq.push(first); - if !target.is_html() { - let v = VElem::new(self.gap.get(styles).into()).with_weak(true); - seq.push(v.pack().spanned(span)) - } - seq.push(second); - realized = Content::sequence(seq) - } - - // Ensure that the body is considered a paragraph. - realized += ParbreakElem::shared().clone().spanned(span); - - if target.is_html() { - return Ok(HtmlElem::new(tag::figure) - .with_body(Some(realized)) - .pack() - .spanned(span)); - } - - // Wrap the contents in a block. - realized = BlockElem::new() - .with_body(Some(BlockBody::Content(realized))) - .pack() - .spanned(span); - - // Wrap in a float. - if let Some(align) = self.placement.get(styles) { - realized = PlaceElem::new(realized) - .with_alignment(align.map(|align| HAlignment::Center + align)) - .with_scope(self.scope.get(styles)) - .with_float(true) - .pack() - .spanned(span); - } else if self.scope.get(styles) == PlacementScope::Parent { - bail!( - span, - "parent-scoped placement is only available for floating figures"; - hint: "you can enable floating placement with `figure(placement: auto, ..)`" - ); - } - - Ok(realized) - } -} - impl ShowSet for Packed { fn show_set(&self, _: StyleChain) -> Styles { // Still allows breakable figures with @@ -471,7 +409,7 @@ impl Outlinable for Packed { /// caption: [A rectangle], /// ) /// ``` -#[elem(name = "caption", Locatable, Synthesize, Show)] +#[elem(name = "caption", Locatable, Synthesize)] pub struct FigureCaption { /// The caption's position in the figure. Either `{top}` or `{bottom}`. /// @@ -559,6 +497,35 @@ pub struct FigureCaption { } impl FigureCaption { + /// Realizes the textual caption content. + pub fn realize( + &self, + engine: &mut Engine, + styles: StyleChain, + ) -> SourceResult { + let mut realized = self.body.clone(); + + if let ( + Some(Some(mut supplement)), + Some(Some(numbering)), + Some(Some(counter)), + Some(Some(location)), + ) = ( + self.supplement.clone(), + &self.numbering, + &self.counter, + &self.figure_location, + ) { + let numbers = counter.display_at_loc(engine, *location, styles, numbering)?; + if !supplement.is_empty() { + supplement += TextElem::packed('\u{a0}'); + } + realized = supplement + numbers + self.get_separator(styles) + realized; + } + + Ok(realized) + } + /// Gets the default separator in the given language and (optionally) /// region. fn local_separator(lang: Lang, _: Option) -> &'static str { @@ -588,43 +555,6 @@ impl Synthesize for Packed { } } -impl Show for Packed { - #[typst_macros::time(name = "figure.caption", span = self.span())] - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let mut realized = self.body.clone(); - - if let ( - Some(Some(mut supplement)), - Some(Some(numbering)), - Some(Some(counter)), - Some(Some(location)), - ) = ( - self.supplement.clone(), - &self.numbering, - &self.counter, - &self.figure_location, - ) { - let numbers = counter.display_at_loc(engine, *location, styles, numbering)?; - if !supplement.is_empty() { - supplement += TextElem::packed('\u{a0}'); - } - realized = supplement + numbers + self.get_separator(styles) + realized; - } - - Ok(if styles.get(TargetElem::target).is_html() { - HtmlElem::new(tag::figcaption) - .with_body(Some(realized)) - .pack() - .spanned(self.span()) - } else { - BlockElem::new() - .with_body(Some(BlockBody::Content(realized))) - .pack() - .spanned(self.span()) - }) - } -} - cast! { FigureCaption, v: Content => v.unpack::().unwrap_or_else(Self::new), diff --git a/crates/typst-library/src/model/footnote.rs b/crates/typst-library/src/model/footnote.rs index f284f70ca..147a3f009 100644 --- a/crates/typst-library/src/model/footnote.rs +++ b/crates/typst-library/src/model/footnote.rs @@ -3,16 +3,16 @@ use std::str::FromStr; use typst_utils::NonZeroExt; -use crate::diag::{bail, At, SourceResult, StrResult}; +use crate::diag::{bail, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, Content, Label, NativeElement, Packed, Show, ShowSet, Smart, - StyleChain, Styles, + cast, elem, scope, Content, Label, NativeElement, Packed, ShowSet, Smart, StyleChain, + Styles, }; -use crate::introspection::{Count, Counter, CounterUpdate, Locatable, Location}; -use crate::layout::{Abs, Em, HElem, Length, Ratio}; -use crate::model::{Destination, Numbering, NumberingPattern, ParElem}; -use crate::text::{SuperElem, TextElem, TextSize}; +use crate::introspection::{Count, CounterUpdate, Locatable, Location}; +use crate::layout::{Abs, Em, Length, Ratio}; +use crate::model::{Numbering, NumberingPattern, ParElem}; +use crate::text::{TextElem, TextSize}; use crate::visualize::{LineElem, Stroke}; /// A footnote. @@ -51,7 +51,7 @@ use crate::visualize::{LineElem, Stroke}; /// apply to the footnote's content. See [here][issue] for more information. /// /// [issue]: https://github.com/typst/typst/issues/1467#issuecomment-1588799440 -#[elem(scope, Locatable, Show, Count)] +#[elem(scope, Locatable, Count)] pub struct FootnoteElem { /// How to number footnotes. /// @@ -135,22 +135,6 @@ impl Packed { } } -impl Show for Packed { - #[typst_macros::time(name = "footnote", span = self.span())] - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let span = self.span(); - let loc = self.declaration_location(engine).at(span)?; - let numbering = self.numbering.get_ref(styles); - let counter = Counter::of(FootnoteElem::ELEM); - let num = counter.display_at_loc(engine, loc, styles, numbering)?; - let sup = SuperElem::new(num).pack().spanned(span); - let loc = loc.variant(1); - // Add zero-width weak spacing to make the footnote "sticky". - // TODO(accessibility): generate alt text - Ok(HElem::hole().pack() + sup.linked(Destination::Location(loc), None)) - } -} - impl Count for Packed { fn update(&self) -> Option { (!self.is_ref()).then(|| CounterUpdate::Step(NonZeroUsize::ONE)) @@ -192,7 +176,7 @@ cast! { /// page run is a sequence of pages without an explicit pagebreak in between). /// For this reason, set and show rules for footnote entries should be defined /// before any page content, typically at the very start of the document. -#[elem(name = "entry", title = "Footnote Entry", Locatable, Show, ShowSet)] +#[elem(name = "entry", title = "Footnote Entry", Locatable, ShowSet)] pub struct FootnoteEntry { /// The footnote for this entry. Its location can be used to determine /// the footnote counter state. @@ -275,38 +259,6 @@ pub struct FootnoteEntry { pub indent: Length, } -impl Show for Packed { - #[typst_macros::time(name = "footnote.entry", span = self.span())] - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let span = self.span(); - let number_gap = Em::new(0.05); - let default = StyleChain::default(); - let numbering = self.note.numbering.get_ref(default); - let counter = Counter::of(FootnoteElem::ELEM); - let Some(loc) = self.note.location() else { - bail!( - span, "footnote entry must have a location"; - hint: "try using a query or a show rule to customize the footnote instead" - ); - }; - - let num = counter.display_at_loc(engine, loc, styles, numbering)?; - let sup = SuperElem::new(num) - .pack() - .spanned(span) - // TODO(accessibility): generate alt text - .linked(Destination::Location(loc), None) - .located(loc.variant(1)); - - Ok(Content::sequence([ - HElem::new(self.indent.get(styles).into()).pack(), - sup, - HElem::new(number_gap.into()).with_weak(true).pack(), - self.note.body_content().unwrap().clone(), - ])) - } -} - impl ShowSet for Packed { fn show_set(&self, _: StyleChain) -> Styles { let mut out = Styles::new(); diff --git a/crates/typst-library/src/model/heading.rs b/crates/typst-library/src/model/heading.rs index d6f6d01f9..0f2a1d338 100644 --- a/crates/typst-library/src/model/heading.rs +++ b/crates/typst-library/src/model/heading.rs @@ -1,21 +1,16 @@ use std::num::NonZeroUsize; -use ecow::eco_format; -use typst_utils::{Get, NonZeroExt}; +use typst_utils::NonZeroExt; -use crate::diag::{warning, SourceResult}; +use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{ - elem, Content, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain, - Styles, Synthesize, TargetElem, + elem, Content, NativeElement, Packed, ShowSet, Smart, StyleChain, Styles, Synthesize, }; -use crate::html::{attr, tag, HtmlElem}; -use crate::introspection::{ - Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink, -}; -use crate::layout::{Abs, Axes, BlockBody, BlockElem, Em, HElem, Length, Region, Sides}; +use crate::introspection::{Count, Counter, CounterUpdate, Locatable}; +use crate::layout::{BlockElem, Em, Length}; use crate::model::{Numbering, Outlinable, Refable, Supplement}; -use crate::text::{FontWeight, LocalName, SpaceElem, TextElem, TextSize}; +use crate::text::{FontWeight, LocalName, TextElem, TextSize}; /// A section heading. /// @@ -49,7 +44,7 @@ use crate::text::{FontWeight, LocalName, SpaceElem, TextElem, TextSize}; /// one or multiple equals signs, followed by a space. The number of equals /// signs determines the heading's logical nesting depth. The `{offset}` field /// can be set to configure the starting depth. -#[elem(Locatable, Synthesize, Count, Show, ShowSet, LocalName, Refable, Outlinable)] +#[elem(Locatable, Synthesize, Count, ShowSet, LocalName, Refable, Outlinable)] pub struct HeadingElem { /// The absolute nesting depth of the heading, starting from one. If set /// to `{auto}`, it is computed from `{offset + depth}`. @@ -215,96 +210,6 @@ impl Synthesize for Packed { } } -impl Show for Packed { - #[typst_macros::time(name = "heading", span = self.span())] - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let html = styles.get(TargetElem::target).is_html(); - - const SPACING_TO_NUMBERING: Em = Em::new(0.3); - - let span = self.span(); - let mut realized = self.body.clone(); - - let hanging_indent = self.hanging_indent.get(styles); - let mut indent = match hanging_indent { - Smart::Custom(length) => length.resolve(styles), - Smart::Auto => Abs::zero(), - }; - - if let Some(numbering) = self.numbering.get_ref(styles).as_ref() { - let location = self.location().unwrap(); - let numbering = Counter::of(HeadingElem::ELEM) - .display_at_loc(engine, location, styles, numbering)? - .spanned(span); - - if hanging_indent.is_auto() && !html { - let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false)); - - // We don't have a locator for the numbering here, so we just - // use the measurement infrastructure for now. - let link = LocatorLink::measure(location); - let size = (engine.routines.layout_frame)( - engine, - &numbering, - Locator::link(&link), - styles, - pod, - )? - .size(); - - indent = size.x + SPACING_TO_NUMBERING.resolve(styles); - } - - let spacing = if html { - SpaceElem::shared().clone() - } else { - HElem::new(SPACING_TO_NUMBERING.into()).with_weak(true).pack() - }; - - realized = numbering + spacing + realized; - } - - Ok(if html { - // 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 = self.resolve_level(styles).get(); - 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) - } - } else { - let block = if indent != Abs::zero() { - let body = HElem::new((-indent).into()).pack() + realized; - let inset = Sides::default() - .with(styles.resolve(TextElem::dir).start(), Some(indent.into())); - BlockElem::new() - .with_body(Some(BlockBody::Content(body))) - .with_inset(inset) - } else { - BlockElem::new().with_body(Some(BlockBody::Content(realized))) - }; - block.pack().spanned(span) - }) - } -} - impl ShowSet for Packed { fn show_set(&self, styles: StyleChain) -> Styles { let level = self.resolve_level(styles).get(); diff --git a/crates/typst-library/src/model/link.rs b/crates/typst-library/src/model/link.rs index 6203436e6..e8a797370 100644 --- a/crates/typst-library/src/model/link.rs +++ b/crates/typst-library/src/model/link.rs @@ -2,13 +2,10 @@ use std::ops::Deref; use ecow::{eco_format, EcoString}; -use crate::diag::{bail, warning, At, SourceResult, StrResult}; -use crate::engine::Engine; +use crate::diag::{bail, StrResult}; use crate::foundations::{ - cast, elem, Content, Label, NativeElement, Packed, Repr, Show, ShowSet, Smart, - StyleChain, Styles, TargetElem, + cast, elem, Content, Label, Packed, Repr, ShowSet, Smart, StyleChain, Styles, }; -use crate::html::{attr, tag, HtmlElem}; use crate::introspection::{Locatable, Location}; use crate::layout::Position; use crate::text::TextElem; @@ -38,7 +35,7 @@ use crate::text::TextElem; /// # Syntax /// This function also has dedicated syntax: Text that starts with `http://` or /// `https://` is automatically turned into a link. -#[elem(Locatable, Show)] +#[elem(Locatable)] pub struct LinkElem { /// A text describing the link. pub alt: Option, @@ -106,42 +103,6 @@ impl LinkElem { } } -impl Show for Packed { - #[typst_macros::time(name = "link", span = self.span())] - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let body = self.body.clone(); - - Ok(if styles.get(TargetElem::target).is_html() { - if let LinkTarget::Dest(Destination::Url(url)) = &self.dest { - HtmlElem::new(tag::a) - .with_attr(attr::href, url.clone().into_inner()) - .with_body(Some(body)) - .pack() - .spanned(self.span()) - } else { - engine.sink.warn(warning!( - self.span(), - "non-URL links are not yet supported by HTML export" - )); - body - } - } else { - let alt = self.alt.get_cloned(styles); - match &self.dest { - LinkTarget::Dest(dest) => { - let url = || dest.as_url().map(|url| url.clone().into_inner()); - body.linked(dest.clone(), alt.or_else(url)) - } - LinkTarget::Label(label) => { - let elem = engine.introspector.query_label(*label).at(self.span())?; - let dest = Destination::Location(elem.location().unwrap()); - body.linked(dest, alt) - } - } - }) - } -} - impl ShowSet for Packed { fn show_set(&self, _: StyleChain) -> Styles { let mut out = Styles::new(); diff --git a/crates/typst-library/src/model/list.rs b/crates/typst-library/src/model/list.rs index 4fdd0d492..15636be3f 100644 --- a/crates/typst-library/src/model/list.rs +++ b/crates/typst-library/src/model/list.rs @@ -3,13 +3,11 @@ use comemo::Track; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, Array, Content, Context, Depth, Func, NativeElement, Packed, Show, - Smart, StyleChain, Styles, TargetElem, Value, + cast, elem, scope, Array, Content, Context, Depth, Func, NativeElement, Packed, + Smart, StyleChain, Styles, Value, }; -use crate::html::{tag, HtmlElem}; use crate::introspection::Locatable; -use crate::layout::{BlockElem, Em, Length, VElem}; -use crate::model::{ParElem, ParbreakElem}; +use crate::layout::{Em, Length}; use crate::text::TextElem; /// A bullet list. @@ -43,7 +41,7 @@ use crate::text::TextElem; /// followed by a space to create a list item. A list item can contain multiple /// paragraphs and other block-level content. All content that is indented /// more than an item's marker becomes part of that item. -#[elem(scope, title = "Bullet List", Locatable, Show)] +#[elem(scope, title = "Bullet List", Locatable)] pub struct ListElem { /// Defines the default [spacing]($list.spacing) of the list. If it is /// `{false}`, the items are spaced apart with @@ -137,45 +135,6 @@ impl ListElem { type ListItem; } -impl Show for Packed { - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let tight = self.tight.get(styles); - - if styles.get(TargetElem::target).is_html() { - return Ok(HtmlElem::new(tag::ul) - .with_body(Some(Content::sequence(self.children.iter().map(|item| { - // Text in wide lists shall always turn into paragraphs. - let mut body = item.body.clone(); - if !tight { - body += ParbreakElem::shared(); - } - HtmlElem::new(tag::li) - .with_body(Some(body)) - .pack() - .spanned(item.span()) - })))) - .pack() - .spanned(self.span())); - } - - let mut realized = - BlockElem::multi_layouter(self.clone(), engine.routines.layout_list) - .pack() - .spanned(self.span()); - - if tight { - let spacing = self - .spacing - .get(styles) - .unwrap_or_else(|| styles.get(ParElem::leading)); - let v = VElem::new(spacing.into()).with_weak(true).with_attach(true).pack(); - realized = v + realized; - } - - Ok(realized) - } -} - /// A bullet list item. #[elem(name = "item", title = "Bullet List Item", Locatable)] pub struct ListItem { diff --git a/crates/typst-library/src/model/mod.rs b/crates/typst-library/src/model/mod.rs index 9bdbf0013..a0f7e11af 100644 --- a/crates/typst-library/src/model/mod.rs +++ b/crates/typst-library/src/model/mod.rs @@ -46,23 +46,23 @@ use crate::foundations::Scope; pub fn define(global: &mut Scope) { global.start_category(crate::Category::Model); global.define_elem::(); - global.define_elem::(); + global.define_elem::(); + global.define_elem::(); + global.define_elem::(); + global.define_elem::(); + global.define_elem::(); + global.define_elem::(); + global.define_elem::(); global.define_elem::(); - global.define_elem::(); global.define_elem::(); global.define_elem::(); - global.define_elem::(); global.define_elem::(); + global.define_elem::(); + global.define_elem::(); + global.define_elem::(); global.define_elem::(); global.define_elem::(); - global.define_elem::(); - global.define_elem::(); - global.define_elem::(); - global.define_elem::(); global.define_elem::(); - global.define_elem::(); - global.define_elem::(); - global.define_elem::(); global.define_func::(); global.reset_category(); } diff --git a/crates/typst-library/src/model/numbering.rs b/crates/typst-library/src/model/numbering.rs index 236ced361..449dfdb33 100644 --- a/crates/typst-library/src/model/numbering.rs +++ b/crates/typst-library/src/model/numbering.rs @@ -18,7 +18,7 @@ use crate::foundations::{cast, func, Context, Func, Str, Value}; /// /// A numbering pattern consists of counting symbols, for which the actual /// number is substituted, their prefixes, and one suffix. The prefixes and the -/// suffix are repeated as-is. +/// suffix are displayed as-is. /// /// # Example /// ```example @@ -66,10 +66,10 @@ pub fn numbering( /// items, the number is represented using repeated symbols. /// /// **Suffixes** are all characters after the last counting symbol. They are - /// repeated as-is at the end of any rendered number. + /// displayed as-is at the end of any rendered number. /// /// **Prefixes** are all characters that are neither counting symbols nor - /// suffixes. They are repeated as-is at in front of their rendered + /// suffixes. They are displayed as-is at in front of their rendered /// equivalent of their counting symbol. /// /// This parameter can also be an arbitrary function that gets each number diff --git a/crates/typst-library/src/model/outline.rs b/crates/typst-library/src/model/outline.rs index 934a889cb..2dbd33cc2 100644 --- a/crates/typst-library/src/model/outline.rs +++ b/crates/typst-library/src/model/outline.rs @@ -1,8 +1,7 @@ use std::num::NonZeroUsize; use std::str::FromStr; -use comemo::{Track, Tracked}; -use ecow::{eco_format, EcoString}; +use comemo::Tracked; use smallvec::SmallVec; use typst_syntax::Span; use typst_utils::{Get, NonZeroExt}; @@ -11,19 +10,18 @@ use crate::diag::{bail, error, At, HintedStrResult, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, func, scope, select_where, Args, Construct, Content, Context, Func, - LocatableSelector, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain, + LocatableSelector, NativeElement, Packed, Resolve, ShowSet, Smart, StyleChain, Styles, }; use crate::introspection::{ Counter, CounterKey, Introspector, Locatable, Location, Locator, LocatorLink, }; use crate::layout::{ - Abs, Axes, BlockBody, BlockElem, BoxElem, Dir, Em, Fr, HElem, Length, PageElem, - Region, Rel, RepeatElem, Sides, + Abs, Axes, BlockBody, BlockElem, BoxElem, Dir, Em, Fr, HElem, Length, Region, Rel, + RepeatElem, Sides, }; -use crate::math::EquationElem; -use crate::model::{Destination, HeadingElem, NumberingPattern, ParElem, Refable}; -use crate::text::{LocalName, SmartQuoteElem, SmartQuotes, SpaceElem, TextElem}; +use crate::model::{HeadingElem, NumberingPattern, ParElem, Refable}; +use crate::text::{LocalName, SpaceElem, TextElem}; /// A table of contents, figures, or other elements. /// @@ -148,7 +146,7 @@ use crate::text::{LocalName, SmartQuoteElem, SmartQuotes, SpaceElem, TextElem}; /// /// [^1]: The outline of equations is the exception to this rule as it does not /// have a body and thus does not use indented layout. -#[elem(scope, keywords = ["Table of Contents", "toc"], Show, ShowSet, LocalName, Locatable)] +#[elem(scope, keywords = ["Table of Contents", "toc"], ShowSet, LocalName, Locatable)] pub struct OutlineElem { /// The title of the outline. /// @@ -250,48 +248,6 @@ impl OutlineElem { type OutlineEntry; } -impl Show for Packed { - #[typst_macros::time(name = "outline", span = self.span())] - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let span = self.span(); - - // Build the outline title. - let mut seq = vec![]; - if let Some(title) = self.title.get_cloned(styles).unwrap_or_else(|| { - Some(TextElem::packed(Self::local_name_in(styles)).spanned(span)) - }) { - seq.push( - HeadingElem::new(title) - .with_depth(NonZeroUsize::ONE) - .pack() - .spanned(span), - ); - } - - let elems = engine.introspector.query(&self.target.get_ref(styles).0); - let depth = self.depth.get(styles).unwrap_or(NonZeroUsize::MAX); - - // Build the outline entries. - let mut entries = vec![]; - for elem in elems { - let Some(outlinable) = elem.with::() else { - bail!(span, "cannot outline {}", elem.func().name()); - }; - - let level = outlinable.level(); - if outlinable.outlined() && level <= depth { - let entry = OutlineEntry::new(level, elem); - entries.push(entry.pack().spanned(span)); - } - } - - // Wrap the entries into a marker for pdf tagging. - seq.push(OutlineBody::new(Content::sequence(entries)).pack()); - - Ok(Content::sequence(seq)) - } -} - impl ShowSet for Packed { fn show_set(&self, styles: StyleChain) -> Styles { let mut out = Styles::new(); @@ -311,16 +267,10 @@ impl LocalName for Packed { } /// Only used to delimit the outline in tagged PDF. -#[elem(Locatable, Show)] +#[elem(Locatable)] pub struct OutlineBody { #[required] - body: Content, -} - -impl Show for Packed { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(self.body.clone()) - } + pub body: Content, } /// Defines how an outline is indented. @@ -381,7 +331,7 @@ pub trait Outlinable: Refable { /// With show-set and show rules on outline entries, you can richly customize /// the outline's appearance. See the /// [section on styling the outline]($outline/#styling-the-outline) for details. -#[elem(scope, name = "entry", title = "Outline Entry", Locatable, Show)] +#[elem(scope, name = "entry", title = "Outline Entry", Locatable)] pub struct OutlineEntry { /// The nesting level of this outline entry. Starts at `{1}` for top-level /// entries. @@ -426,34 +376,6 @@ pub struct OutlineEntry { pub parent: Option>, } -impl Show for Packed { - #[typst_macros::time(name = "outline.entry", span = self.span())] - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let span = self.span(); - let context = Context::new(None, Some(styles)); - let context = context.track(); - - // TODO(accessibility): prefix should be wrapped in a `Lbl` structure element - let prefix = self.prefix(engine, context, span)?; - let body = self.body().at(span)?; - let page = self.page(engine, context, span)?; - let alt = alt_text(styles, &prefix, &body, &page); - let inner = self.inner(context, span, body, page)?; - let block = if self.element.is::() { - let body = prefix.unwrap_or_default() + inner; - BlockElem::new() - .with_body(Some(BlockBody::Content(body))) - .pack() - .spanned(span) - } else { - self.indented(engine, context, span, prefix, inner, Em::new(0.5).into())? - }; - - let loc = self.element_location().at(span)?; - Ok(block.linked(Destination::Location(loc), Some(alt))) - } -} - #[scope] impl OutlineEntry { /// A helper function for producing an indented entry layout: Lays out a @@ -677,7 +599,8 @@ impl OutlineEntry { .ok_or_else(|| error!("cannot outline {}", self.element.func().name())) } - fn element_location(&self) -> HintedStrResult { + /// Returns the location of the outlined element. + pub fn element_location(&self) -> HintedStrResult { let elem = &self.element; elem.location().ok_or_else(|| { if elem.can::() && elem.can::() { @@ -697,27 +620,6 @@ cast! { v: Content => v.unpack::().map_err(|_| "expected outline entry")? } -fn alt_text( - styles: StyleChain, - prefix: &Option, - body: &Content, - page: &Content, -) -> EcoString { - let prefix = prefix.as_ref().map(|p| p.plain_text()).unwrap_or_default(); - let body = body.plain_text(); - let page_str = PageElem::local_name_in(styles); - let page_nr = page.plain_text(); - let quotes = SmartQuotes::get( - styles.get_ref(SmartQuoteElem::quotes), - styles.get(TextElem::lang), - styles.get(TextElem::region), - styles.get(SmartQuoteElem::alternative), - ); - let open = quotes.double_open; - let close = quotes.double_close; - eco_format!("{prefix} {open}{body}{close} {page_str} {page_nr}",) -} - /// Measures the width of a prefix. fn measure_prefix( engine: &mut Engine, @@ -774,8 +676,8 @@ fn query_prefix_widths( } /// Helper type for introspection-based prefix alignment. -#[elem(Construct, Locatable, Show)] -struct PrefixInfo { +#[elem(Construct, Locatable)] +pub(crate) struct PrefixInfo { /// The location of the outline this prefix is part of. This is used to /// scope prefix computations to a specific outline. #[required] @@ -797,9 +699,3 @@ impl Construct for PrefixInfo { bail!(args.span, "cannot be constructed manually"); } } - -impl Show for Packed { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(Content::empty()) - } -} diff --git a/crates/typst-library/src/model/quote.rs b/crates/typst-library/src/model/quote.rs index a8cf3eaef..5ce4a92f5 100644 --- a/crates/typst-library/src/model/quote.rs +++ b/crates/typst-library/src/model/quote.rs @@ -1,16 +1,13 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; +use typst_syntax::Span; + use crate::foundations::{ - cast, elem, Content, Depth, Label, NativeElement, Packed, Show, ShowSet, Smart, - StyleChain, Styles, TargetElem, + cast, elem, Content, Depth, Label, NativeElement, Packed, ShowSet, Smart, StyleChain, + Styles, }; -use crate::html::{attr, tag, HtmlElem}; use crate::introspection::Locatable; -use crate::layout::{ - Alignment, BlockBody, BlockElem, Em, HElem, PadElem, Spacing, VElem, -}; -use crate::model::{CitationForm, CiteElem, Destination, LinkElem, LinkTarget}; -use crate::text::{SmartQuoteElem, SmartQuotes, SpaceElem, TextElem}; +use crate::layout::{BlockElem, Em, PadElem}; +use crate::model::{CitationForm, CiteElem}; +use crate::text::{SmartQuotes, SpaceElem, TextElem}; /// Displays a quote alongside an optional attribution. /// @@ -44,7 +41,7 @@ use crate::text::{SmartQuoteElem, SmartQuotes, SpaceElem, TextElem}; /// flame of Udûn. Go back to the Shadow! You cannot pass. /// ] /// ``` -#[elem(Locatable, ShowSet, Show)] +#[elem(Locatable, ShowSet)] pub struct QuoteElem { /// Whether this is a block quote. /// @@ -62,7 +59,7 @@ pub struct QuoteElem { /// Ich bin ein Berliner. /// ] /// ``` - block: bool, + pub block: bool, /// Whether double quotes should be added around this quote. /// @@ -88,7 +85,7 @@ pub struct QuoteElem { /// translate the quote: /// #quote[I am a Berliner.] /// ``` - quotes: Smart, + pub quotes: Smart, /// The attribution of this quote, usually the author or source. Can be a /// label pointing to a bibliography entry or any content. By default only @@ -105,7 +102,7 @@ pub struct QuoteElem { /// } /// /// #quote( - /// attribution: link("https://typst.app/home")[typst.com] + /// attribution: link("https://typst.app/home")[typst.app] /// )[ /// Compose papers faster /// ] @@ -123,17 +120,36 @@ pub struct QuoteElem { /// /// #bibliography("works.bib", style: "apa") /// ``` - attribution: Option, + pub attribution: Option, /// The quote. #[required] - body: Content, + pub body: Content, /// The nesting depth. #[internal] #[fold] #[ghost] - depth: Depth, + pub depth: Depth, +} + +impl QuoteElem { + /// Quotes the body content with the appropriate quotes based on the current + /// styles and surroundings. + pub fn quoted(body: Content, styles: StyleChain<'_>) -> Content { + let quotes = SmartQuotes::get_in(styles); + + // Alternate between single and double quotes. + let Depth(depth) = styles.get(QuoteElem::depth); + let double = depth % 2 == 0; + + Content::sequence([ + TextElem::packed(quotes.open(double)), + body, + TextElem::packed(quotes.close(double)), + ]) + .set(QuoteElem::depth, Depth(1)) + } } /// Attribution for a [quote](QuoteElem). @@ -143,6 +159,23 @@ pub enum Attribution { Label(Label), } +impl Attribution { + /// Realize as an em dash followed by text or a citation. + pub fn realize(&self, span: Span) -> Content { + Content::sequence([ + TextElem::packed('—'), + SpaceElem::shared().clone(), + match self { + Attribution::Content(content) => content.clone(), + Attribution::Label(label) => CiteElem::new(*label) + .with_form(Some(CitationForm::Prose)) + .pack() + .spanned(span), + }, + ]) + } +} + cast! { Attribution, self => match self { @@ -153,96 +186,6 @@ cast! { label: Label => Self::Label(label), } -impl Show for Packed { - #[typst_macros::time(name = "quote", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let mut realized = self.body.clone(); - let block = self.block.get(styles); - let html = styles.get(TargetElem::target).is_html(); - - if self.quotes.get(styles).unwrap_or(!block) { - let quotes = SmartQuotes::get( - styles.get_ref(SmartQuoteElem::quotes), - styles.get(TextElem::lang), - styles.get(TextElem::region), - styles.get(SmartQuoteElem::alternative), - ); - - // Alternate between single and double quotes. - let Depth(depth) = styles.get(QuoteElem::depth); - let double = depth % 2 == 0; - - if !html { - // Add zero-width weak spacing to make the quotes "sticky". - let hole = HElem::hole().pack(); - realized = Content::sequence([hole.clone(), realized, hole]); - } - realized = Content::sequence([ - TextElem::packed(quotes.open(double)), - realized, - TextElem::packed(quotes.close(double)), - ]) - .set(QuoteElem::depth, Depth(1)); - } - - let attribution = self.attribution.get_ref(styles); - - if block { - realized = if html { - let mut elem = 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 { - elem = elem.with_attr(attr::cite, url.clone().into_inner()); - } - } - } - elem.pack() - } else { - BlockElem::new().with_body(Some(BlockBody::Content(realized))).pack() - } - .spanned(self.span()); - - if let Some(attribution) = attribution { - let attribution = match attribution { - Attribution::Content(content) => content.clone(), - Attribution::Label(label) => CiteElem::new(*label) - .with_form(Some(CitationForm::Prose)) - .pack() - .spanned(self.span()), - }; - let attribution = Content::sequence([ - TextElem::packed('—'), - SpaceElem::shared().clone(), - attribution, - ]); - - if html { - realized += attribution; - } else { - // Bring the attribution a bit closer to the quote. - let gap = Spacing::Rel(Em::new(0.9).into()); - let v = VElem::new(gap).with_weak(true).pack(); - realized += v; - realized += BlockElem::new() - .with_body(Some(BlockBody::Content(attribution))) - .pack() - .aligned(Alignment::END); - } - } - - if !html { - realized = PadElem::new(realized).pack(); - } - } else if let Some(Attribution::Label(label)) = attribution { - realized += SpaceElem::shared().clone() - + CiteElem::new(*label).pack().spanned(self.span()); - } - - Ok(realized) - } -} - impl ShowSet for Packed { fn show_set(&self, styles: StyleChain) -> Styles { let mut out = Styles::new(); diff --git a/crates/typst-library/src/model/reference.rs b/crates/typst-library/src/model/reference.rs index f52690678..dfb1aa7b7 100644 --- a/crates/typst-library/src/model/reference.rs +++ b/crates/typst-library/src/model/reference.rs @@ -5,7 +5,7 @@ use crate::diag::{bail, At, Hint, SourceResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, Cast, Content, Context, Func, IntoValue, Label, NativeElement, Packed, - Repr, Show, Smart, StyleChain, Synthesize, + Repr, Smart, StyleChain, Synthesize, }; use crate::introspection::{Counter, CounterKey, Locatable}; use crate::math::EquationElem; @@ -134,7 +134,7 @@ use crate::text::TextElem; /// In @beginning we prove @pythagoras. /// $ a^2 + b^2 = c^2 $ /// ``` -#[elem(title = "Reference", Synthesize, Locatable, Show)] +#[elem(title = "Reference", Synthesize, Locatable)] pub struct RefElem { /// The target label that should be referenced. /// @@ -220,9 +220,13 @@ impl Synthesize for Packed { } } -impl Show for Packed { - #[typst_macros::time(name = "ref", span = self.span())] - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { +impl Packed { + /// Realize as a linked, textual reference. + pub fn realize( + &self, + engine: &mut Engine, + styles: StyleChain, + ) -> SourceResult { let elem = engine.introspector.query_label(self.target); let span = self.span(); @@ -242,7 +246,7 @@ impl Show for Packed { .at(span)?; let supplement = engine.introspector.page_supplement(loc); - return show_reference( + return realize_reference( self, engine, styles, @@ -306,7 +310,7 @@ impl Show for Packed { )) .at(span)?; - show_reference( + realize_reference( self, engine, styles, @@ -319,7 +323,7 @@ impl Show for Packed { } /// Show a reference. -fn show_reference( +fn realize_reference( reference: &Packed, engine: &mut Engine, styles: StyleChain, diff --git a/crates/typst-library/src/model/strong.rs b/crates/typst-library/src/model/strong.rs index dfbe37c94..399f496ae 100644 --- a/crates/typst-library/src/model/strong.rs +++ b/crates/typst-library/src/model/strong.rs @@ -1,11 +1,5 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{ - elem, Content, NativeElement, Packed, Show, StyleChain, TargetElem, -}; -use crate::html::{tag, HtmlElem}; +use crate::foundations::{elem, Content}; use crate::introspection::Locatable; -use crate::text::{TextElem, WeightDelta}; /// Strongly emphasizes content by increasing the font weight. /// @@ -25,7 +19,7 @@ use crate::text::{TextElem, WeightDelta}; /// simply enclose it in stars/asterisks (`*`). Note that this only works at /// word boundaries. To strongly emphasize part of a word, you have to use the /// function. -#[elem(title = "Strong Emphasis", keywords = ["bold", "weight"], Locatable, Show)] +#[elem(title = "Strong Emphasis", keywords = ["bold", "weight"], Locatable)] pub struct StrongElem { /// The delta to apply on the font weight. /// @@ -40,18 +34,3 @@ pub struct StrongElem { #[required] pub body: Content, } - -impl Show for Packed { - #[typst_macros::time(name = "strong", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let body = self.body.clone(); - Ok(if styles.get(TargetElem::target).is_html() { - HtmlElem::new(tag::strong) - .with_body(Some(body)) - .pack() - .spanned(self.span()) - } else { - body.set(TextElem::delta, WeightDelta(self.delta.get(styles))) - }) - } -} diff --git a/crates/typst-library/src/model/table.rs b/crates/typst-library/src/model/table.rs index 91aa9b4a3..7a638083d 100644 --- a/crates/typst-library/src/model/table.rs +++ b/crates/typst-library/src/model/table.rs @@ -4,19 +4,12 @@ use std::sync::Arc; use ecow::EcoString; use typst_utils::NonZeroExt; -use crate::diag::{bail, HintedStrResult, HintedString, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ - cast, elem, scope, Content, NativeElement, Packed, Show, Smart, StyleChain, - TargetElem, -}; -use crate::html::{attr, tag, HtmlAttrs, HtmlElem, HtmlTag}; -use crate::introspection::{Locatable, Locator}; -use crate::layout::grid::resolve::{table_to_cellgrid, Cell, CellGrid, Entry}; +use crate::diag::{bail, HintedStrResult, HintedString}; +use crate::foundations::{cast, elem, scope, Content, Packed, Smart}; +use crate::introspection::Locatable; use crate::layout::{ - show_grid_cell, Abs, Alignment, BlockElem, Celled, GridCell, GridFooter, GridHLine, - GridHeader, GridVLine, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, - TrackSizings, + Abs, Alignment, Celled, GridCell, GridFooter, GridHLine, GridHeader, GridVLine, + Length, OuterHAlignment, OuterVAlignment, Rel, Sides, TrackSizings, }; use crate::model::Figurable; use crate::pdf::TableCellKind; @@ -123,7 +116,7 @@ use crate::visualize::{Paint, Stroke}; /// [Robert], b, a, b, /// ) /// ``` -#[elem(scope, Locatable, Show, LocalName, Figurable)] +#[elem(scope, Locatable, LocalName, Figurable)] pub struct TableElem { /// The column sizes. See the [grid documentation]($grid) for more /// information on track sizing. @@ -260,113 +253,6 @@ impl TableElem { type TableFooter; } -fn show_cell_html(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::default(); - 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()) -} - -fn show_cellgrid_html(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_html(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)) -} - -impl Show for Packed { - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(if styles.get(TargetElem::target).is_html() { - // TODO: This is a hack, it is not clear whether the locator is actually used by HTML. - // How can we find out whether locator is actually used? - let locator = Locator::root(); - show_cellgrid_html(table_to_cellgrid(self, engine, locator, styles)?, styles) - } else { - BlockElem::multi_layouter(self.clone(), engine.routines.layout_table).pack() - } - .spanned(self.span())) - } -} - impl LocalName for Packed { const KEY: &'static str = "table"; } @@ -766,7 +652,7 @@ pub struct TableVLine { /// [Vikram], [49], [Perseverance], /// ) /// ``` -#[elem(name = "cell", title = "Table Cell", Locatable, Show)] +#[elem(name = "cell", title = "Table Cell", Locatable)] pub struct TableCell { /// The cell's body. #[required] @@ -817,12 +703,6 @@ cast! { v: Content => v.into(), } -impl Show for Packed { - fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult { - show_grid_cell(self.body.clone(), self.inset.get(styles), self.align.get(styles)) - } -} - impl Default for Packed { fn default() -> Self { Packed::new( diff --git a/crates/typst-library/src/model/terms.rs b/crates/typst-library/src/model/terms.rs index 3db510370..3a201987d 100644 --- a/crates/typst-library/src/model/terms.rs +++ b/crates/typst-library/src/model/terms.rs @@ -1,16 +1,10 @@ -use typst_utils::{Get, Numeric}; - -use crate::diag::{bail, SourceResult}; -use crate::engine::Engine; +use crate::diag::bail; use crate::foundations::{ - cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain, - Styles, TargetElem, + cast, elem, scope, Array, Content, NativeElement, Packed, Smart, Styles, }; -use crate::html::{tag, HtmlElem}; use crate::introspection::Locatable; -use crate::layout::{Em, HElem, Length, Sides, StackChild, StackElem, VElem}; -use crate::model::{ListItemLike, ListLike, ParElem, ParbreakElem}; -use crate::text::TextElem; +use crate::layout::{Em, HElem, Length}; +use crate::model::{ListItemLike, ListLike}; /// A list of terms and their descriptions. /// @@ -28,7 +22,7 @@ use crate::text::TextElem; /// # Syntax /// This function also has dedicated syntax: Starting a line with a slash, /// followed by a term, a colon and a description creates a term list item. -#[elem(scope, title = "Term List", Locatable, Show)] +#[elem(scope, title = "Term List", Locatable)] pub struct TermsElem { /// Defines the default [spacing]($terms.spacing) of the term list. If it is /// `{false}`, the items are spaced apart with @@ -118,94 +112,6 @@ impl TermsElem { type TermItem; } -impl Show for Packed { - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let span = self.span(); - let tight = self.tight.get(styles); - - if styles.get(TargetElem::target).is_html() { - return Ok(HtmlElem::new(tag::dl) - .with_body(Some(Content::sequence(self.children.iter().flat_map( - |item| { - // Text in wide term lists shall always turn into paragraphs. - let mut description = item.description.clone(); - if !tight { - 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()); - } - - let separator = self.separator.get_ref(styles); - let indent = self.indent.get(styles); - let hanging_indent = self.hanging_indent.get(styles); - let gutter = self.spacing.get(styles).unwrap_or_else(|| { - if tight { - styles.get(ParElem::leading) - } else { - styles.get(ParElem::spacing) - } - }); - - let pad = hanging_indent + indent; - let unpad = (!hanging_indent.is_zero()) - .then(|| HElem::new((-hanging_indent).into()).pack().spanned(span)); - - let mut children = vec![]; - for child in self.children.iter() { - let mut seq = vec![]; - seq.extend(unpad.clone()); - seq.push(child.term.clone().strong()); - seq.push((*separator).clone()); - seq.push(child.description.clone()); - - // Text in wide term lists shall always turn into paragraphs. - if !tight { - seq.push(ParbreakElem::shared().clone()); - } - - children.push(StackChild::Block(Content::sequence(seq))); - } - - let padding = - Sides::default().with(styles.resolve(TextElem::dir).start(), pad.into()); - - let mut realized = StackElem::new(children) - .with_spacing(Some(gutter.into())) - .pack() - .spanned(span) - .padded(padding) - .set(TermsElem::within, true); - - if tight { - let spacing = self - .spacing - .get(styles) - .unwrap_or_else(|| styles.get(ParElem::leading)); - let v = VElem::new(spacing.into()) - .with_weak(true) - .with_attach(true) - .pack() - .spanned(span); - realized = v + realized; - } - - Ok(realized) - } -} - /// A term list item. #[elem(name = "item", title = "Term List Item", Locatable)] pub struct TermItem { diff --git a/crates/typst-library/src/pdf/accessibility.rs b/crates/typst-library/src/pdf/accessibility.rs index 9399c1c60..16a57e537 100644 --- a/crates/typst-library/src/pdf/accessibility.rs +++ b/crates/typst-library/src/pdf/accessibility.rs @@ -4,14 +4,12 @@ use ecow::EcoString; use typst_macros::{cast, elem, func, Cast}; use typst_utils::NonZeroExt; -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{Content, NativeElement, Packed, Show, Smart, StyleChain}; +use crate::foundations::{Content, NativeElement, Smart}; use crate::introspection::Locatable; use crate::model::TableCell; // TODO: docs -#[elem(Locatable, Show)] +#[elem(Locatable)] pub struct PdfTagElem { #[default(PdfTagKind::NonStruct)] pub kind: PdfTagKind, @@ -28,13 +26,6 @@ pub struct PdfTagElem { pub body: Content, } -impl Show for Packed { - #[typst_macros::time(name = "pdf.tag", span = self.span())] - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(self.body.clone()) - } -} - // TODO: docs /// PDF structure elements #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -183,7 +174,7 @@ pub enum ListNumbering { /// Mark content as a PDF artifact. /// TODO: maybe generalize this and use it to mark html elements with `aria-hidden="true"`? -#[elem(Locatable, Show)] +#[elem(Locatable)] pub struct ArtifactElem { /// The artifact kind. #[default(ArtifactKind::Other)] @@ -207,13 +198,6 @@ pub enum ArtifactKind { Other, } -impl Show for Packed { - #[typst_macros::time(name = "pdf.artifact", span = self.span())] - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(self.body.clone()) - } -} - // TODO: feature gate /// Explicity define this cell as a header cell. #[func] diff --git a/crates/typst-library/src/pdf/embed.rs b/crates/typst-library/src/pdf/embed.rs index 0f93f95af..3aba85623 100644 --- a/crates/typst-library/src/pdf/embed.rs +++ b/crates/typst-library/src/pdf/embed.rs @@ -1,12 +1,8 @@ use ecow::EcoString; -use typst_library::foundations::Target; use typst_syntax::Spanned; -use crate::diag::{warning, At, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ - elem, Bytes, Cast, Content, Derived, Packed, Show, StyleChain, TargetElem, -}; +use crate::diag::At; +use crate::foundations::{elem, Bytes, Cast, Derived}; use crate::introspection::Locatable; use crate::World; @@ -33,7 +29,7 @@ use crate::World; /// - This element is ignored if exporting to a format other than PDF. /// - File embeddings are not currently supported for PDF/A-2, even if the /// embedded file conforms to PDF/A-1 or PDF/A-2. -#[elem(Show, Locatable)] +#[elem(Locatable)] pub struct EmbedElem { /// The [path]($syntax/#paths) of the file to be embedded. /// @@ -77,17 +73,6 @@ pub struct EmbedElem { pub description: Option, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - if styles.get(TargetElem::target) == Target::Html { - engine - .sink - .warn(warning!(self.span(), "embed was ignored during HTML export")); - } - Ok(Content::empty()) - } -} - /// The relationship of an embedded file with the document. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum EmbeddedFileRelationship { diff --git a/crates/typst-library/src/routines.rs b/crates/typst-library/src/routines.rs index 59ce83282..01964800f 100644 --- a/crates/typst-library/src/routines.rs +++ b/crates/typst-library/src/routines.rs @@ -1,7 +1,5 @@ -#![allow(unused)] - +use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; -use std::num::NonZeroUsize; use comemo::{Tracked, TrackedMut}; use typst_syntax::{Span, SyntaxMode}; @@ -10,20 +8,12 @@ use typst_utils::LazyHash; use crate::diag::SourceResult; use crate::engine::{Engine, Route, Sink, Traced}; use crate::foundations::{ - Args, Cast, Closure, Content, Context, Func, Packed, Scope, StyleChain, Styles, Value, + Args, Closure, Content, Context, Func, Module, NativeRuleMap, Scope, StyleChain, + Styles, Value, }; use crate::introspection::{Introspector, Locator, SplitLocator}; -use crate::layout::{ - Abs, BoxElem, ColumnsElem, Fragment, Frame, GridElem, InlineItem, MoveElem, PadElem, - PagedDocument, Region, Regions, Rel, RepeatElem, RotateElem, ScaleElem, Size, - SkewElem, StackElem, -}; -use crate::math::EquationElem; -use crate::model::{DocumentInfo, EnumElem, ListElem, TableElem}; -use crate::visualize::{ - CircleElem, CurveElem, EllipseElem, ImageElem, LineElem, PathElem, PolygonElem, - RectElem, SquareElem, -}; +use crate::layout::{Frame, Region}; +use crate::model::DocumentInfo; use crate::World; /// Defines the `Routines` struct. @@ -38,6 +28,8 @@ macro_rules! routines { /// This is essentially dynamic linking and done to allow for crate /// splitting. pub struct Routines { + /// Native show rules. + pub rules: NativeRuleMap, $( $(#[$attr])* pub $name: $(for<$($time),*>)? fn ($($args)*) -> $ret @@ -47,6 +39,12 @@ macro_rules! routines { impl Hash for Routines { fn hash(&self, _: &mut H) {} } + + impl Debug for Routines { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("Routines(..)") + } + } }; } @@ -86,15 +84,6 @@ routines! { styles: StyleChain<'a>, ) -> SourceResult>> - /// Lays out content into multiple regions. - fn layout_fragment( - engine: &mut Engine, - content: &Content, - locator: Locator, - styles: StyleChain, - regions: Regions, - ) -> SourceResult - /// Lays out content into a single region, producing a single frame. fn layout_frame( engine: &mut Engine, @@ -104,232 +93,33 @@ routines! { region: Region, ) -> SourceResult - /// Lays out a [`ListElem`]. - fn layout_list( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, - ) -> SourceResult - - /// Lays out an [`EnumElem`]. - fn layout_enum( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, - ) -> SourceResult - - /// Lays out a [`GridElem`]. - fn layout_grid( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, - ) -> SourceResult - - /// Lays out a [`TableElem`]. - fn layout_table( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, - ) -> SourceResult - - /// Lays out a [`StackElem`]. - fn layout_stack( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, - ) -> SourceResult - - /// Lays out a [`ColumnsElem`]. - fn layout_columns( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, - ) -> SourceResult - - /// Lays out a [`MoveElem`]. - fn layout_move( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out a [`RotateElem`]. - fn layout_rotate( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out a [`ScaleElem`]. - fn layout_scale( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out a [`SkewElem`]. - fn layout_skew( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out a [`RepeatElem`]. - fn layout_repeat( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out a [`PadElem`]. - fn layout_pad( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, - ) -> SourceResult - - /// Lays out a [`LineElem`]. - fn layout_line( - elem: &Packed, - _: &mut Engine, - _: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out a [`CurveElem`]. - fn layout_curve( - elem: &Packed, - _: &mut Engine, - _: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out a [`PathElem`]. - fn layout_path( - elem: &Packed, - _: &mut Engine, - _: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out a [`PolygonElem`]. - fn layout_polygon( - elem: &Packed, - _: &mut Engine, - _: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out a [`RectElem`]. - fn layout_rect( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out a [`SquareElem`]. - fn layout_square( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out a [`EllipseElem`]. - fn layout_ellipse( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out a [`CircleElem`]. - fn layout_circle( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out an [`ImageElem`]. - fn layout_image( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Region, - ) -> SourceResult - - /// Lays out an [`EquationElem`] in a paragraph. - fn layout_equation_inline( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Size, - ) -> SourceResult> - - /// Lays out an [`EquationElem`] in a flow. - fn layout_equation_block( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - regions: Regions, - ) -> SourceResult + /// Constructs the `html` module. + fn html_module() -> Module } /// Defines what kind of realization we are performing. pub enum RealizationKind<'a> { /// This the root realization for layout. Requires a mutable reference /// to document metadata that will be filled from `set document` rules. - LayoutDocument(&'a mut DocumentInfo), + LayoutDocument { info: &'a mut DocumentInfo }, /// A nested realization in a container (e.g. a `block`). Requires a mutable /// reference to an enum that will be set to `FragmentKind::Inline` if the /// fragment's content was fully inline. - LayoutFragment(&'a mut FragmentKind), + LayoutFragment { kind: &'a mut FragmentKind }, /// A nested realization in a paragraph (i.e. a `par`) LayoutPar, - /// This the root realization for HTML. Requires a mutable reference - /// to document metadata that will be filled from `set document` rules. - HtmlDocument(&'a mut DocumentInfo), + /// This the root realization for HTML. Requires a mutable reference to + /// document metadata that will be filled from `set document` rules. + /// + /// The `is_inline` function checks whether content consists of an inline + /// HTML element. It's used by the `PAR` grouping rules. This is slightly + /// hacky and might be replaced by a mechanism to supply the grouping rules + /// as a realization user. + HtmlDocument { info: &'a mut DocumentInfo, is_inline: fn(&Content) -> bool }, /// A nested realization in a container (e.g. a `block`). Requires a mutable /// reference to an enum that will be set to `FragmentKind::Inline` if the /// fragment's content was fully inline. - HtmlFragment(&'a mut FragmentKind), + HtmlFragment { kind: &'a mut FragmentKind, is_inline: fn(&Content) -> bool }, /// A realization within math. Math, } @@ -337,18 +127,20 @@ pub enum RealizationKind<'a> { impl RealizationKind<'_> { /// It this a realization for HTML export? pub fn is_html(&self) -> bool { - matches!(self, Self::HtmlDocument(_) | Self::HtmlFragment(_)) + matches!(self, Self::HtmlDocument { .. } | Self::HtmlFragment { .. }) } /// It this a realization for a container? pub fn is_fragment(&self) -> bool { - matches!(self, Self::LayoutFragment(_) | Self::HtmlFragment(_)) + matches!(self, Self::LayoutFragment { .. } | Self::HtmlFragment { .. }) } /// If this is a document-level realization, accesses the document info. pub fn as_document_mut(&mut self) -> Option<&mut DocumentInfo> { match self { - Self::LayoutDocument(info) | Self::HtmlDocument(info) => Some(*info), + Self::LayoutDocument { info } | Self::HtmlDocument { info, .. } => { + Some(*info) + } _ => None, } } @@ -356,7 +148,9 @@ impl RealizationKind<'_> { /// If this is a container-level realization, accesses the fragment kind. pub fn as_fragment_mut(&mut self) -> Option<&mut FragmentKind> { match self { - Self::LayoutFragment(kind) | Self::HtmlFragment(kind) => Some(*kind), + Self::LayoutFragment { kind } | Self::HtmlFragment { kind, .. } => { + Some(*kind) + } _ => None, } } diff --git a/crates/typst-library/src/text/deco.rs b/crates/typst-library/src/text/deco.rs index 891a6bc49..884333c8d 100644 --- a/crates/typst-library/src/text/deco.rs +++ b/crates/typst-library/src/text/deco.rs @@ -1,14 +1,7 @@ -use smallvec::smallvec; - -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{ - elem, Content, NativeElement, Packed, Show, Smart, StyleChain, TargetElem, -}; -use crate::html::{attr, tag, HtmlElem}; +use crate::foundations::{elem, Content, Smart}; use crate::introspection::Locatable; use crate::layout::{Abs, Corners, Length, Rel, Sides}; -use crate::text::{BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric}; +use crate::text::{BottomEdge, BottomEdgeMetric, TopEdge, TopEdgeMetric}; use crate::visualize::{Color, FixedStroke, Paint, Stroke}; /// Underlines text. @@ -17,7 +10,7 @@ use crate::visualize::{Color, FixedStroke, Paint, Stroke}; /// ```example /// This is #underline[important]. /// ``` -#[elem(Locatable, Show)] +#[elem(Locatable)] pub struct UnderlineElem { /// How to [stroke] the line. /// @@ -79,41 +72,13 @@ pub struct UnderlineElem { pub body: Content, } -impl Show for Packed { - #[typst_macros::time(name = "underline", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - if styles.get(TargetElem::target).is_html() { - // 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. - return Ok(HtmlElem::new(tag::span) - .with_attr(attr::style, "text-decoration: underline") - .with_body(Some(self.body.clone())) - .pack()); - } - - Ok(self.body.clone().set( - TextElem::deco, - smallvec![Decoration { - line: DecoLine::Underline { - stroke: self.stroke.resolve(styles).unwrap_or_default(), - offset: self.offset.resolve(styles), - evade: self.evade.get(styles), - background: self.background.get(styles), - }, - extent: self.extent.resolve(styles), - }], - )) - } -} - /// Adds a line over text. /// /// # Example /// ```example /// #overline[A line over text.] /// ``` -#[elem(Locatable, Show)] +#[elem(Locatable)] pub struct OverlineElem { /// How to [stroke] the line. /// @@ -181,38 +146,13 @@ pub struct OverlineElem { pub body: Content, } -impl Show for Packed { - #[typst_macros::time(name = "overline", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - if styles.get(TargetElem::target).is_html() { - return Ok(HtmlElem::new(tag::span) - .with_attr(attr::style, "text-decoration: overline") - .with_body(Some(self.body.clone())) - .pack()); - } - - Ok(self.body.clone().set( - TextElem::deco, - smallvec![Decoration { - line: DecoLine::Overline { - stroke: self.stroke.resolve(styles).unwrap_or_default(), - offset: self.offset.resolve(styles), - evade: self.evade.get(styles), - background: self.background.get(styles), - }, - extent: self.extent.resolve(styles), - }], - )) - } -} - /// Strikes through text. /// /// # Example /// ```example /// This is #strike[not] relevant. /// ``` -#[elem(title = "Strikethrough", Locatable, Show)] +#[elem(title = "Strikethrough", Locatable)] pub struct StrikeElem { /// How to [stroke] the line. /// @@ -265,35 +205,13 @@ pub struct StrikeElem { pub body: Content, } -impl Show for Packed { - #[typst_macros::time(name = "strike", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - if styles.get(TargetElem::target).is_html() { - return Ok(HtmlElem::new(tag::s).with_body(Some(self.body.clone())).pack()); - } - - Ok(self.body.clone().set( - TextElem::deco, - smallvec![Decoration { - // Note that we do not support evade option for strikethrough. - line: DecoLine::Strikethrough { - stroke: self.stroke.resolve(styles).unwrap_or_default(), - offset: self.offset.resolve(styles), - background: self.background.get(styles), - }, - extent: self.extent.resolve(styles), - }], - )) - } -} - /// Highlights text with a background color. /// /// # Example /// ```example /// This is #highlight[important]. /// ``` -#[elem(Locatable, Show)] +#[elem(Locatable)] pub struct HighlightElem { /// The color to highlight the text with. /// @@ -364,35 +282,6 @@ pub struct HighlightElem { pub body: Content, } -impl Show for Packed { - #[typst_macros::time(name = "highlight", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - if styles.get(TargetElem::target).is_html() { - return Ok(HtmlElem::new(tag::mark) - .with_body(Some(self.body.clone())) - .pack()); - } - - Ok(self.body.clone().set( - TextElem::deco, - smallvec![Decoration { - line: DecoLine::Highlight { - fill: self.fill.get_cloned(styles), - stroke: self - .stroke - .resolve(styles) - .unwrap_or_default() - .map(|stroke| stroke.map(Stroke::unwrap_or_default)), - top_edge: self.top_edge.get(styles), - bottom_edge: self.bottom_edge.get(styles), - radius: self.radius.resolve(styles).unwrap_or_default(), - }, - extent: self.extent.resolve(styles), - }], - )) - } -} - /// A text decoration. /// /// Can be positioned over, under, or on top of text, or highlight the text with diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs index b44beaad1..1c522a185 100644 --- a/crates/typst-library/src/text/raw.rs +++ b/crates/typst-library/src/text/raw.rs @@ -16,15 +16,14 @@ use crate::diag::{ }; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, Bytes, Content, Derived, NativeElement, OneOrMultiple, Packed, - PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, TargetElem, + cast, elem, scope, Bytes, Content, Derived, OneOrMultiple, Packed, PlainText, + ShowSet, Smart, StyleChain, Styles, Synthesize, }; -use crate::html::{tag, HtmlElem}; use crate::introspection::Locatable; -use crate::layout::{BlockBody, BlockElem, Em, HAlignment}; +use crate::layout::{Em, HAlignment}; use crate::loading::{DataSource, Load}; use crate::model::{Figurable, ParElem}; -use crate::text::{FontFamily, FontList, LinebreakElem, LocalName, TextElem, TextSize}; +use crate::text::{FontFamily, FontList, LocalName, TextElem, TextSize}; use crate::visualize::Color; use crate::World; @@ -80,7 +79,6 @@ use crate::World; title = "Raw Text / Code", Synthesize, Locatable, - Show, ShowSet, LocalName, Figurable, @@ -431,46 +429,6 @@ impl Packed { } } -impl Show for Packed { - #[typst_macros::time(name = "raw", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let lines = self.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()); - } - - let mut realized = Content::sequence(seq); - - if styles.get(TargetElem::target).is_html() { - return Ok(HtmlElem::new(if self.block.get(styles) { - tag::pre - } else { - tag::code - }) - .with_body(Some(realized)) - .pack() - .spanned(self.span())); - } - - if self.block.get(styles) { - // Align the text before inserting it into the block. - realized = realized.aligned(self.align.get(styles).into()); - realized = BlockElem::new() - .with_body(Some(BlockBody::Content(realized))) - .pack() - .spanned(self.span()); - } - - Ok(realized) - } -} - impl ShowSet for Packed { fn show_set(&self, styles: StyleChain) -> Styles { let mut out = Styles::new(); @@ -500,7 +458,11 @@ impl PlainText for Packed { } /// The content of the raw text. -#[derive(Debug, Clone, Hash, PartialEq)] +#[derive(Debug, Clone, Hash)] +#[allow( + clippy::derived_hash_with_manual_eq, + reason = "https://github.com/typst/typst/pull/6560#issuecomment-3045393640" +)] pub enum RawContent { /// From a string. Text(EcoString), @@ -525,6 +487,22 @@ impl RawContent { } } +impl PartialEq for RawContent { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (RawContent::Text(a), RawContent::Text(b)) => a == b, + (lines @ RawContent::Lines(_), RawContent::Text(text)) + | (RawContent::Text(text), lines @ RawContent::Lines(_)) => { + *text == lines.get() + } + (RawContent::Lines(a), RawContent::Lines(b)) => Iterator::eq( + a.iter().map(|(line, _)| line), + b.iter().map(|(line, _)| line), + ), + } + } +} + cast! { RawContent, self => self.get().into_value(), @@ -636,7 +614,7 @@ fn format_theme_error(error: syntect::LoadingError) -> LoadError { /// It allows you to access various properties of the line, such as the line /// number, the raw non-highlighted text, the highlighted text, and whether it /// is the first or last line of the raw block. -#[elem(name = "line", title = "Raw Text / Code Line", Locatable, Show, PlainText)] +#[elem(name = "line", title = "Raw Text / Code Line", Locatable, PlainText)] pub struct RawLine { /// The line number of the raw line inside of the raw block, starts at 1. #[required] @@ -655,13 +633,6 @@ pub struct RawLine { pub body: Content, } -impl Show for Packed { - #[typst_macros::time(name = "raw.line", span = self.span())] - fn show(&self, _: &mut Engine, _styles: StyleChain) -> SourceResult { - Ok(self.body.clone()) - } -} - impl PlainText for Packed { fn plain_text(&self, text: &mut EcoString) { text.push_str(&self.text); diff --git a/crates/typst-library/src/text/shift.rs b/crates/typst-library/src/text/shift.rs index ed7b367cd..8c31f50d7 100644 --- a/crates/typst-library/src/text/shift.rs +++ b/crates/typst-library/src/text/shift.rs @@ -1,14 +1,9 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{ - elem, Content, NativeElement, Packed, Show, Smart, StyleChain, TargetElem, -}; -use crate::html::{tag, HtmlElem}; use crate::introspection::Locatable; -use crate::layout::{Em, Length}; -use crate::text::{FontMetrics, TextElem, TextSize}; use ttf_parser::Tag; -use typst_library::text::ScriptMetrics; + +use crate::foundations::{elem, Content, Smart}; +use crate::layout::{Em, Length}; +use crate::text::{FontMetrics, ScriptMetrics, TextSize}; /// Renders text in subscript. /// @@ -18,7 +13,7 @@ use typst_library::text::ScriptMetrics; /// ```example /// Revenue#sub[yearly] /// ``` -#[elem(title = "Subscript", Locatable, Show)] +#[elem(title = "Subscript", Locatable)] pub struct SubElem { /// Whether to create artificial subscripts by lowering and scaling down /// regular glyphs. @@ -65,29 +60,6 @@ pub struct SubElem { pub body: Content, } -impl Show for Packed { - #[typst_macros::time(name = "sub", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let body = self.body.clone(); - - if styles.get(TargetElem::target).is_html() { - return Ok(HtmlElem::new(tag::sub) - .with_body(Some(body)) - .pack() - .spanned(self.span())); - } - - show_script( - styles, - body, - self.typographic.get(styles), - self.baseline.get(styles), - self.size.get(styles), - ScriptKind::Sub, - ) - } -} - /// Renders text in superscript. /// /// The text is rendered smaller and its baseline is raised. @@ -96,7 +68,7 @@ impl Show for Packed { /// ```example /// 1#super[st] try! /// ``` -#[elem(title = "Superscript", Locatable, Show)] +#[elem(title = "Superscript", Locatable)] pub struct SuperElem { /// Whether to create artificial superscripts by raising and scaling down /// regular glyphs. @@ -147,49 +119,6 @@ pub struct SuperElem { pub body: Content, } -impl Show for Packed { - #[typst_macros::time(name = "super", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let body = self.body.clone(); - - if styles.get(TargetElem::target).is_html() { - return Ok(HtmlElem::new(tag::sup) - .with_body(Some(body)) - .pack() - .spanned(self.span())); - } - - show_script( - styles, - body, - self.typographic.get(styles), - self.baseline.get(styles), - self.size.get(styles), - ScriptKind::Super, - ) - } -} - -fn show_script( - styles: StyleChain, - body: Content, - typographic: bool, - baseline: Smart, - size: Smart, - kind: ScriptKind, -) -> SourceResult { - let font_size = styles.resolve(TextElem::size); - Ok(body.set( - TextElem::shift_settings, - Some(ShiftSettings { - typographic, - shift: baseline.map(|l| -Em::from_length(l, font_size)), - size: size.map(|t| Em::from_length(t.0, font_size)), - kind, - }), - )) -} - /// Configuration values for sub- or superscript text. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub struct ShiftSettings { diff --git a/crates/typst-library/src/text/smallcaps.rs b/crates/typst-library/src/text/smallcaps.rs index 1c2838933..199222fed 100644 --- a/crates/typst-library/src/text/smallcaps.rs +++ b/crates/typst-library/src/text/smallcaps.rs @@ -1,7 +1,4 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{elem, Content, Packed, Show, StyleChain}; -use crate::text::TextElem; +use crate::foundations::{elem, Content}; /// Displays text in small capitals. /// @@ -43,7 +40,7 @@ use crate::text::TextElem; /// = Introduction /// #lorem(40) /// ``` -#[elem(title = "Small Capitals", Show)] +#[elem(title = "Small Capitals")] pub struct SmallcapsElem { /// Whether to turn uppercase letters into small capitals as well. /// @@ -61,15 +58,6 @@ pub struct SmallcapsElem { pub body: Content, } -impl Show for Packed { - #[typst_macros::time(name = "smallcaps", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let sc = - if self.all.get(styles) { Smallcaps::All } else { Smallcaps::Minuscules }; - Ok(self.body.clone().set(TextElem::smallcaps, Some(sc))) - } -} - /// What becomes small capitals. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum Smallcaps { diff --git a/crates/typst-library/src/text/smartquote.rs b/crates/typst-library/src/text/smartquote.rs index 24787d062..375b1cf09 100644 --- a/crates/typst-library/src/text/smartquote.rs +++ b/crates/typst-library/src/text/smartquote.rs @@ -5,9 +5,10 @@ use unicode_segmentation::UnicodeSegmentation; use crate::diag::{bail, HintedStrResult, StrResult}; use crate::foundations::{ array, cast, dict, elem, Array, Dict, FromValue, Packed, PlainText, Smart, Str, + StyleChain, }; use crate::layout::Dir; -use crate::text::{Lang, Region}; +use crate::text::{Lang, Region, TextElem}; /// A language-aware quote that reacts to its context. /// @@ -200,6 +201,16 @@ pub struct SmartQuotes<'s> { } impl<'s> SmartQuotes<'s> { + /// Retrieve the smart quotes as configured by the current styles. + pub fn get_in(styles: StyleChain<'s>) -> Self { + Self::get( + styles.get_ref(SmartQuoteElem::quotes), + styles.get(TextElem::lang), + styles.get(TextElem::region), + styles.get(SmartQuoteElem::alternative), + ) + } + /// Create a new `Quotes` struct with the given quotes, optionally falling /// back to the defaults for a language and region. /// diff --git a/crates/typst-library/src/visualize/curve.rs b/crates/typst-library/src/visualize/curve.rs index 587f0d4a2..15ae48c61 100644 --- a/crates/typst-library/src/visualize/curve.rs +++ b/crates/typst-library/src/visualize/curve.rs @@ -2,12 +2,9 @@ use kurbo::ParamCurveExtrema; use typst_macros::{scope, Cast}; use typst_utils::Numeric; -use crate::diag::{bail, HintedStrResult, HintedString, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ - cast, elem, Content, NativeElement, Packed, Show, Smart, StyleChain, -}; -use crate::layout::{Abs, Axes, BlockElem, Length, Point, Rel, Size}; +use crate::diag::{bail, HintedStrResult, HintedString}; +use crate::foundations::{cast, elem, Content, Packed, Smart}; +use crate::layout::{Abs, Axes, Length, Point, Rel, Size}; use crate::visualize::{FillRule, Paint, Stroke}; use super::FixedStroke; @@ -42,7 +39,7 @@ use super::FixedStroke; /// curve.close(), /// ) /// ``` -#[elem(scope, Show)] +#[elem(scope)] pub struct CurveElem { /// How to fill the curve. /// @@ -95,14 +92,6 @@ pub struct CurveElem { pub components: Vec, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_curve) - .pack() - .spanned(self.span())) - } -} - #[scope] impl CurveElem { #[elem] diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index d1c27f6d9..379c25ba7 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -8,6 +8,7 @@ pub use self::raster::{ }; pub use self::svg::SvgImage; +use std::ffi::OsStr; use std::fmt::{self, Debug, Formatter}; use std::sync::Arc; @@ -15,17 +16,17 @@ use ecow::EcoString; use typst_syntax::{Span, Spanned}; use typst_utils::LazyHash; -use crate::diag::{SourceResult, StrResult}; +use crate::diag::{warning, At, LoadedWithin, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, func, scope, Bytes, Cast, Content, Derived, NativeElement, Packed, Show, - Smart, StyleChain, + cast, elem, func, scope, Bytes, Cast, Content, Derived, NativeElement, Packed, Smart, + StyleChain, }; use crate::introspection::Locatable; -use crate::layout::{BlockElem, Length, Rel, Sizing}; +use crate::layout::{Length, Rel, Sizing}; use crate::loading::{DataSource, Load, LoadSource, Loaded, Readable}; use crate::model::Figurable; -use crate::text::LocalName; +use crate::text::{families, LocalName}; /// A raster or vector graphic. /// @@ -45,7 +46,7 @@ use crate::text::LocalName; /// ], /// ) /// ``` -#[elem(scope, Locatable, Show, LocalName, Figurable)] +#[elem(scope, Locatable, LocalName, Figurable)] pub struct ImageElem { /// A [path]($syntax/#paths) to an image file or raw bytes making up an /// image in one of the supported [formats]($image.format). @@ -220,13 +221,78 @@ impl ImageElem { } } -impl Show for Packed { - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_image) - .with_width(self.width.get(styles)) - .with_height(self.height.get(styles)) - .pack() - .spanned(self.span())) +impl Packed { + /// Decodes the image. + pub fn decode(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { + let span = self.span(); + let loaded = &self.source.derived; + let format = self.determine_format(styles).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, + self.icc.get_ref(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)?, + ), + }; + + Ok(Image::new(kind, self.alt.get_cloned(styles), self.scaling.get(styles))) + } + + /// Tries to determine the image format based on the format that was + /// explicitly defined, or else the extension, or else the data. + fn determine_format(&self, styles: StyleChain) -> StrResult { + if let Smart::Custom(v) = self.format.get(styles) { + return Ok(v); + }; + + let Derived { source, derived: loaded } = &self.source; + 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(&loaded.data).ok_or("unknown image format")?) } } diff --git a/crates/typst-library/src/visualize/line.rs b/crates/typst-library/src/visualize/line.rs index d058b926a..7eecfc915 100644 --- a/crates/typst-library/src/visualize/line.rs +++ b/crates/typst-library/src/visualize/line.rs @@ -1,7 +1,5 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain}; -use crate::layout::{Abs, Angle, Axes, BlockElem, Length, Rel}; +use crate::foundations::elem; +use crate::layout::{Abs, Angle, Axes, Length, Rel}; use crate::visualize::Stroke; /// A line from one point to another. @@ -17,7 +15,7 @@ use crate::visualize::Stroke; /// stroke: 2pt + maroon, /// ) /// ``` -#[elem(Show)] +#[elem] pub struct LineElem { /// The start point of the line. /// @@ -50,11 +48,3 @@ pub struct LineElem { #[fold] pub stroke: Stroke, } - -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_line) - .pack() - .spanned(self.span())) - } -} diff --git a/crates/typst-library/src/visualize/path.rs b/crates/typst-library/src/visualize/path.rs index e19e091df..bd8aea02d 100644 --- a/crates/typst-library/src/visualize/path.rs +++ b/crates/typst-library/src/visualize/path.rs @@ -1,11 +1,7 @@ use self::PathVertex::{AllControlPoints, MirroredControlPoint, Vertex}; -use crate::diag::{bail, SourceResult}; -use crate::engine::Engine; -use crate::foundations::{ - array, cast, elem, Array, Content, NativeElement, Packed, Reflect, Show, Smart, - StyleChain, -}; -use crate::layout::{Axes, BlockElem, Length, Rel}; +use crate::diag::bail; +use crate::foundations::{array, cast, elem, Array, Reflect, Smart}; +use crate::layout::{Axes, Length, Rel}; use crate::visualize::{FillRule, Paint, Stroke}; /// A path through a list of points, connected by Bézier curves. @@ -21,7 +17,7 @@ use crate::visualize::{FillRule, Paint, Stroke}; /// ((50%, 0pt), (40pt, 0pt)), /// ) /// ``` -#[elem(Show)] +#[elem] pub struct PathElem { /// How to fill the path. /// @@ -83,14 +79,6 @@ pub struct PathElem { pub vertices: Vec, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_path) - .pack() - .spanned(self.span())) - } -} - /// A component used for path creation. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum PathVertex { diff --git a/crates/typst-library/src/visualize/polygon.rs b/crates/typst-library/src/visualize/polygon.rs index d75e1a657..db75a2670 100644 --- a/crates/typst-library/src/visualize/polygon.rs +++ b/crates/typst-library/src/visualize/polygon.rs @@ -2,12 +2,8 @@ use std::f64::consts::PI; use typst_syntax::Span; -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{ - elem, func, scope, Content, NativeElement, Packed, Show, Smart, StyleChain, -}; -use crate::layout::{Axes, BlockElem, Em, Length, Rel}; +use crate::foundations::{elem, func, scope, Content, NativeElement, Smart}; +use crate::layout::{Axes, Em, Length, Rel}; use crate::visualize::{FillRule, Paint, Stroke}; /// A closed polygon. @@ -25,7 +21,7 @@ use crate::visualize::{FillRule, Paint, Stroke}; /// (0%, 2cm), /// ) /// ``` -#[elem(scope, Show)] +#[elem(scope)] pub struct PolygonElem { /// How to fill the polygon. /// @@ -124,11 +120,3 @@ impl PolygonElem { elem.pack().spanned(span) } } - -impl Show for Packed { - fn show(&self, engine: &mut Engine, _: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_polygon) - .pack() - .spanned(self.span())) - } -} diff --git a/crates/typst-library/src/visualize/shape.rs b/crates/typst-library/src/visualize/shape.rs index f21bf93e9..fc7b8748e 100644 --- a/crates/typst-library/src/visualize/shape.rs +++ b/crates/typst-library/src/visualize/shape.rs @@ -1,9 +1,5 @@ -use crate::diag::SourceResult; -use crate::engine::Engine; -use crate::foundations::{ - elem, Cast, Content, NativeElement, Packed, Show, Smart, StyleChain, -}; -use crate::layout::{Abs, BlockElem, Corners, Length, Point, Rel, Sides, Size, Sizing}; +use crate::foundations::{elem, Cast, Content, Smart}; +use crate::layout::{Abs, Corners, Length, Point, Rel, Sides, Size, Sizing}; use crate::visualize::{Curve, FixedStroke, Paint, Stroke}; /// A rectangle with optional content. @@ -19,7 +15,7 @@ use crate::visualize::{Curve, FixedStroke, Paint, Stroke}; /// to fit the content. /// ] /// ``` -#[elem(title = "Rectangle", Show)] +#[elem(title = "Rectangle")] pub struct RectElem { /// The rectangle's width, relative to its parent container. pub width: Smart>, @@ -122,16 +118,6 @@ pub struct RectElem { pub body: Option, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_rect) - .with_width(self.width.get(styles)) - .with_height(self.height.get(styles)) - .pack() - .spanned(self.span())) - } -} - /// A square with optional content. /// /// # Example @@ -145,7 +131,7 @@ impl Show for Packed { /// sized to fit. /// ] /// ``` -#[elem(Show)] +#[elem] pub struct SquareElem { /// The square's side length. This is mutually exclusive with `width` and /// `height`. @@ -209,16 +195,6 @@ pub struct SquareElem { pub body: Option, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_square) - .with_width(self.width.get(styles)) - .with_height(self.height.get(styles)) - .pack() - .spanned(self.span())) - } -} - /// An ellipse with optional content. /// /// # Example @@ -233,7 +209,7 @@ impl Show for Packed { /// to fit the content. /// ] /// ``` -#[elem(Show)] +#[elem] pub struct EllipseElem { /// The ellipse's width, relative to its parent container. pub width: Smart>, @@ -269,16 +245,6 @@ pub struct EllipseElem { pub body: Option, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_ellipse) - .with_width(self.width.get(styles)) - .with_height(self.height.get(styles)) - .pack() - .spanned(self.span())) - } -} - /// A circle with optional content. /// /// # Example @@ -293,7 +259,7 @@ impl Show for Packed { /// sized to fit. /// ] /// ``` -#[elem(Show)] +#[elem] pub struct CircleElem { /// The circle's radius. This is mutually exclusive with `width` and /// `height`. @@ -354,16 +320,6 @@ pub struct CircleElem { pub body: Option, } -impl Show for Packed { - fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(BlockElem::single_layouter(self.clone(), engine.routines.layout_circle) - .with_width(self.width.get(styles)) - .with_height(self.height.get(styles)) - .pack() - .spanned(self.span())) - } -} - /// A geometric shape with optional fill and stroke. #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct Shape { diff --git a/crates/typst-library/translations/sv.txt b/crates/typst-library/translations/sv.txt index 20cea6f96..538f466b0 100644 --- a/crates/typst-library/translations/sv.txt +++ b/crates/typst-library/translations/sv.txt @@ -1,8 +1,8 @@ figure = Figur table = Tabell equation = Ekvation -bibliography = Bibliografi -heading = Kapitel +bibliography = Referenser +heading = Avsnitt outline = Innehåll -raw = Listing +raw = Kodlistning page = sida diff --git a/crates/typst-realize/src/lib.rs b/crates/typst-realize/src/lib.rs index fcfb40667..5d9e0a23a 100644 --- a/crates/typst-realize/src/lib.rs +++ b/crates/typst-realize/src/lib.rs @@ -14,11 +14,10 @@ use ecow::EcoString; use typst_library::diag::{bail, At, SourceResult}; use typst_library::engine::Engine; use typst_library::foundations::{ - Content, Context, ContextElem, Element, NativeElement, Recipe, RecipeIndex, Selector, - SequenceElem, Show, ShowSet, Style, StyleChain, StyledElem, Styles, SymbolElem, - Synthesize, Transformation, + Content, Context, ContextElem, Element, NativeElement, NativeShowRule, Recipe, + RecipeIndex, Selector, SequenceElem, ShowSet, Style, StyleChain, StyledElem, Styles, + SymbolElem, Synthesize, TargetElem, Transformation, }; -use typst_library::html::{tag, FrameElem, HtmlElem}; use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem}; use typst_library::layout::{ AlignElem, BoxElem, HElem, InlineElem, PageElem, PagebreakElem, VElem, @@ -48,16 +47,16 @@ pub fn realize<'a>( locator, arenas, rules: match kind { - RealizationKind::LayoutDocument(_) => LAYOUT_RULES, - RealizationKind::LayoutFragment(_) => LAYOUT_RULES, + RealizationKind::LayoutDocument { .. } => LAYOUT_RULES, + RealizationKind::LayoutFragment { .. } => LAYOUT_RULES, RealizationKind::LayoutPar => LAYOUT_PAR_RULES, - RealizationKind::HtmlDocument(_) => HTML_DOCUMENT_RULES, - RealizationKind::HtmlFragment(_) => HTML_FRAGMENT_RULES, + RealizationKind::HtmlDocument { .. } => HTML_DOCUMENT_RULES, + RealizationKind::HtmlFragment { .. } => HTML_FRAGMENT_RULES, RealizationKind::Math => MATH_RULES, }, sink: vec![], groupings: ArrayVec::new(), - outside: matches!(kind, RealizationKind::LayoutDocument(_)), + outside: matches!(kind, RealizationKind::LayoutDocument { .. }), may_attach: false, saw_parbreak: false, kind, @@ -113,7 +112,7 @@ struct GroupingRule { /// be visible to `finish`. tags: bool, /// Defines which kinds of elements start and make up this kind of grouping. - trigger: fn(&Content, &RealizationKind) -> bool, + trigger: fn(&Content, &State) -> bool, /// Defines elements that may appear in the interior of the grouping, but /// not at the edges. inner: fn(&Content) -> bool, @@ -160,7 +159,7 @@ enum ShowStep<'a> { /// A user-defined transformational show rule. Recipe(&'a Recipe, RecipeIndex), /// The built-in show rule. - Builtin, + Builtin(NativeShowRule), } /// A match of a regex show rule. @@ -334,13 +333,6 @@ fn visit_kind_rules<'a>( } } - if !s.kind.is_html() { - if let Some(elem) = content.to_packed::() { - visit(s, &elem.body, styles)?; - return Ok(true); - } - } - Ok(false) } @@ -382,9 +374,7 @@ fn visit_show_rules<'a>( } // Apply a built-in show rule. - ShowStep::Builtin => { - output.with::().unwrap().show(s.engine, chained) - } + ShowStep::Builtin(rule) => rule.apply(&output, s.engine, chained), }; // Errors in show rules don't terminate compilation immediately. We just @@ -426,14 +416,14 @@ fn visit_show_rules<'a>( Ok(true) } -/// Inspects a target element and the current styles and determines how to -/// proceed with the styling. +/// Inspects an element and the current styles and determines how to proceed +/// with the styling. fn verdict<'a>( engine: &mut Engine, - target: &'a Content, + elem: &'a Content, styles: StyleChain<'a>, ) -> Option> { - let prepared = target.is_prepared(); + let prepared = elem.is_prepared(); let mut map = Styles::new(); let mut step = None; @@ -441,20 +431,20 @@ fn verdict<'a>( // fields before real synthesis runs (during preparation). It's really // unfortunate that we have to do this, but otherwise // `show figure.where(kind: table)` won't work :( - let mut target = target; + let mut elem = elem; let mut slot; - if !prepared && target.can::() { - slot = target.clone(); + if !prepared && elem.can::() { + slot = elem.clone(); slot.with_mut::() .unwrap() .synthesize(engine, styles) .ok(); - target = &slot; + elem = &slot; } // Lazily computes the total number of recipes in the style chain. We need // it to determine whether a particular show rule was already applied to the - // `target` previously. For this purpose, show rules are indexed from the + // `elem` previously. For this purpose, show rules are indexed from the // top of the chain as the chain might grow to the bottom. let depth = LazyCell::new(|| styles.recipes().count()); @@ -462,7 +452,7 @@ fn verdict<'a>( // We're not interested in recipes that don't match. if !recipe .selector() - .is_some_and(|selector| selector.matches(target, Some(styles))) + .is_some_and(|selector| selector.matches(elem, Some(styles))) { continue; } @@ -480,9 +470,9 @@ fn verdict<'a>( continue; } - // Check whether this show rule was already applied to the target. + // Check whether this show rule was already applied to the element. let index = RecipeIndex(*depth - r); - if target.is_guarded(index) { + if elem.is_guarded(index) { continue; } @@ -498,19 +488,22 @@ fn verdict<'a>( } // If we found no user-defined rule, also consider the built-in show rule. - if step.is_none() && target.can::() { - step = Some(ShowStep::Builtin); + if step.is_none() { + let target = styles.get(TargetElem::target); + if let Some(rule) = engine.routines.rules.get(target, elem) { + step = Some(ShowStep::Builtin(rule)); + } } // If there's no nothing to do, there is also no verdict. if step.is_none() && map.is_empty() && (prepared || { - target.label().is_none() - && target.location().is_none() - && !target.can::() - && !target.can::() - && !target.can::() + elem.label().is_none() + && elem.location().is_none() + && !elem.can::() + && !elem.can::() + && !elem.can::() }) { return None; @@ -523,7 +516,7 @@ fn verdict<'a>( fn prepare( engine: &mut Engine, locator: &mut SplitLocator, - target: &mut Content, + elem: &mut Content, map: &mut Styles, styles: StyleChain, ) -> SourceResult> { @@ -533,43 +526,43 @@ fn prepare( // // The element could already have a location even if it is not prepared // when it stems from a query. - let key = typst_utils::hash128(&target); - if target.location().is_none() - && (target.can::() || target.label().is_some()) + let key = typst_utils::hash128(&elem); + if elem.location().is_none() + && (elem.can::() || elem.label().is_some()) { let loc = locator.next_location(engine.introspector, key); - target.set_location(loc); + elem.set_location(loc); } // Apply built-in show-set rules. User-defined show-set rules are already // considered in the map built while determining the verdict. - if let Some(show_settable) = target.with::() { + if let Some(show_settable) = elem.with::() { map.apply(show_settable.show_set(styles)); } // If necessary, generated "synthesized" fields (which are derived from // other fields or queries). Do this after show-set so that show-set styles // are respected. - if let Some(synthesizable) = target.with_mut::() { + if let Some(synthesizable) = elem.with_mut::() { synthesizable.synthesize(engine, styles.chain(map))?; } // Copy style chain fields into the element itself, so that they are // available in rules. - target.materialize(styles.chain(map)); + elem.materialize(styles.chain(map)); // If the element is locatable, create start and end tags to be able to find // the element in the frames after layout. Do this after synthesis and // materialization, so that it includes the synthesized fields. Do it before // marking as prepared so that show-set rules will apply to this element // when queried. - let tags = target + let tags = elem .location() - .map(|loc| (Tag::Start(target.clone()), Tag::End(loc, key))); + .map(|loc| (Tag::Start(elem.clone()), Tag::End(loc, key))); // Ensure that this preparation only runs once by marking the element as // prepared. - target.mark_prepared(); + elem.mark_prepared(); Ok(tags) } @@ -600,7 +593,7 @@ fn visit_styled<'a>( ); } } else if elem == PageElem::ELEM { - if !matches!(s.kind, RealizationKind::LayoutDocument(_)) { + if !matches!(s.kind, RealizationKind::LayoutDocument { .. }) { bail!( style.span(), "page configuration is not allowed inside of containers" @@ -658,7 +651,7 @@ fn visit_grouping_rules<'a>( content: &'a Content, styles: StyleChain<'a>, ) -> SourceResult { - let matching = s.rules.iter().find(|&rule| (rule.trigger)(content, &s.kind)); + let matching = s.rules.iter().find(|&rule| (rule.trigger)(content, s)); // Try to continue or finish an existing grouping. let mut i = 0; @@ -670,7 +663,7 @@ fn visit_grouping_rules<'a>( // If the element can be added to the active grouping, do it. if !active.interrupted - && ((active.rule.trigger)(content, &s.kind) || (active.rule.inner)(content)) + && ((active.rule.trigger)(content, s) || (active.rule.inner)(content)) { s.sink.push((content, styles)); return Ok(true); @@ -805,7 +798,7 @@ fn finish_innermost_grouping(s: &mut State) -> SourceResult<()> { let Grouping { start, rule, .. } = s.groupings.pop().unwrap(); // Trim trailing non-trigger elements. - let trimmed = s.sink[start..].trim_end_matches(|(c, _)| !(rule.trigger)(c, &s.kind)); + let trimmed = s.sink[start..].trim_end_matches(|(c, _)| !(rule.trigger)(c, s)); let end = start + trimmed.len(); let tail = s.store_slice(&s.sink[end..]); s.sink.truncate(end); @@ -884,7 +877,7 @@ static TEXTUAL: GroupingRule = GroupingRule { static PAR: GroupingRule = GroupingRule { priority: 1, tags: true, - trigger: |content, kind| { + trigger: |content, state| { let elem = content.elem(); elem == TextElem::ELEM || elem == HElem::ELEM @@ -892,10 +885,11 @@ static PAR: GroupingRule = GroupingRule { || elem == SmartQuoteElem::ELEM || elem == InlineElem::ELEM || elem == BoxElem::ELEM - || (kind.is_html() - && content - .to_packed::() - .is_some_and(|elem| tag::is_inline_by_default(elem.tag))) + || match state.kind { + RealizationKind::HtmlDocument { is_inline, .. } + | RealizationKind::HtmlFragment { is_inline, .. } => is_inline(content), + _ => false, + } }, inner: |content| content.elem() == SpaceElem::ELEM, interrupt: |elem| elem == ParElem::ELEM || elem == AlignElem::ELEM, diff --git a/crates/typst-render/src/lib.rs b/crates/typst-render/src/lib.rs index 3ecae4bad..744f2d117 100644 --- a/crates/typst-render/src/lib.rs +++ b/crates/typst-render/src/lib.rs @@ -202,7 +202,7 @@ fn render_group(canvas: &mut sk::Pixmap, state: State, pos: Point, group: &Group mask.intersect_path( &path, sk::FillRule::default(), - false, + true, sk::Transform::default(), ); storage = mask; @@ -218,7 +218,7 @@ fn render_group(canvas: &mut sk::Pixmap, state: State, pos: Point, group: &Group mask.fill_path( &path, sk::FillRule::default(), - false, + true, sk::Transform::default(), ); storage = mask; diff --git a/crates/typst-svg/src/image.rs b/crates/typst-svg/src/image.rs index 1868ca39b..e6dd579f3 100644 --- a/crates/typst-svg/src/image.rs +++ b/crates/typst-svg/src/image.rs @@ -18,21 +18,27 @@ impl SVGRenderer { self.xml.write_attribute("width", &size.x.to_pt()); self.xml.write_attribute("height", &size.y.to_pt()); self.xml.write_attribute("preserveAspectRatio", "none"); - match image.scaling() { - Smart::Auto => {} - Smart::Custom(ImageScaling::Smooth) => { - // This is still experimental and not implemented in all major browsers. - // https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#browser_compatibility - self.xml.write_attribute("style", "image-rendering: smooth") - } - Smart::Custom(ImageScaling::Pixelated) => { - self.xml.write_attribute("style", "image-rendering: pixelated") - } + if let Some(value) = convert_image_scaling(image.scaling()) { + self.xml + .write_attribute("style", &format_args!("image-rendering: {value}")) } self.xml.end_element(); } } +/// Converts an image scaling to a CSS `image-rendering` propery value. +pub fn convert_image_scaling(scaling: Smart) -> Option<&'static str> { + match scaling { + Smart::Auto => None, + Smart::Custom(ImageScaling::Smooth) => { + // This is still experimental and not implemented in all major browsers. + // https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#browser_compatibility + Some("smooth") + } + Smart::Custom(ImageScaling::Pixelated) => Some("pixelated"), + } +} + /// Encode an image into a data URL. The format of the URL is /// `data:image/{format};base64,`. #[comemo::memoize] diff --git a/crates/typst-svg/src/lib.rs b/crates/typst-svg/src/lib.rs index 91975ae37..a91468acf 100644 --- a/crates/typst-svg/src/lib.rs +++ b/crates/typst-svg/src/lib.rs @@ -5,6 +5,8 @@ mod paint; mod shape; mod text; +pub use image::{convert_image_scaling, convert_image_to_base64_url}; + use std::collections::HashMap; use std::fmt::{self, Display, Formatter, Write}; diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs index eee7966a7..8b4e60eee 100644 --- a/crates/typst/src/lib.rs +++ b/crates/typst/src/lib.rs @@ -39,15 +39,16 @@ pub use typst_syntax as syntax; pub use typst_utils as utils; use std::collections::HashSet; +use std::sync::LazyLock; use comemo::{Track, Tracked, Validate}; use ecow::{eco_format, eco_vec, EcoString, EcoVec}; +use typst_html::HtmlDocument; use typst_library::diag::{ bail, warning, FileError, SourceDiagnostic, SourceResult, Warned, }; use typst_library::engine::{Engine, Route, Sink, Traced}; -use typst_library::foundations::{StyleChain, Styles, Value}; -use typst_library::html::HtmlDocument; +use typst_library::foundations::{NativeRuleMap, StyleChain, Styles, Value}; use typst_library::introspection::Introspector; use typst_library::layout::PagedDocument; use typst_library::routines::Routines; @@ -322,37 +323,39 @@ mod sealed { } } +/// Provides ways to construct a [`Library`]. +pub trait LibraryExt { + /// Creates the default library. + fn default() -> Library; + + /// Creates a builder for configuring a library. + fn builder() -> LibraryBuilder; +} + +impl LibraryExt for Library { + fn default() -> Library { + Self::builder().build() + } + + fn builder() -> LibraryBuilder { + LibraryBuilder::from_routines(&ROUTINES) + } +} + /// Defines implementation of various Typst compiler routines as a table of /// function pointers. /// /// This is essentially dynamic linking and done to allow for crate splitting. -pub static ROUTINES: Routines = Routines { +pub static ROUTINES: LazyLock = LazyLock::new(|| Routines { + rules: { + let mut rules = NativeRuleMap::new(); + typst_layout::register(&mut rules); + typst_html::register(&mut rules); + rules + }, eval_string: typst_eval::eval_string, eval_closure: typst_eval::eval_closure, realize: typst_realize::realize, - layout_fragment: typst_layout::layout_fragment, layout_frame: typst_layout::layout_frame, - layout_list: typst_layout::layout_list, - layout_enum: typst_layout::layout_enum, - layout_grid: typst_layout::layout_grid, - layout_table: typst_layout::layout_table, - layout_stack: typst_layout::layout_stack, - layout_columns: typst_layout::layout_columns, - layout_move: typst_layout::layout_move, - layout_rotate: typst_layout::layout_rotate, - layout_scale: typst_layout::layout_scale, - layout_skew: typst_layout::layout_skew, - layout_repeat: typst_layout::layout_repeat, - layout_pad: typst_layout::layout_pad, - layout_line: typst_layout::layout_line, - layout_curve: typst_layout::layout_curve, - layout_path: typst_layout::layout_path, - layout_polygon: typst_layout::layout_polygon, - layout_rect: typst_layout::layout_rect, - layout_square: typst_layout::layout_square, - layout_ellipse: typst_layout::layout_ellipse, - layout_circle: typst_layout::layout_circle, - layout_image: typst_layout::layout_image, - layout_equation_block: typst_layout::layout_equation_block, - layout_equation_inline: typst_layout::layout_equation_inline, -}; + html_module: typst_html::module, +}); diff --git a/docs/reference/groups.yml b/docs/reference/groups.yml index 1aaa8f229..b187443e4 100644 --- a/docs/reference/groups.yml +++ b/docs/reference/groups.yml @@ -5,7 +5,7 @@ title: Variants category: math path: ["math"] - filter: ["serif", "sans", "frak", "mono", "bb", "cal"] + filter: ["serif", "sans", "frak", "mono", "bb", "cal", "scr"] details: | Alternate typefaces within formulas. diff --git a/docs/src/lib.rs b/docs/src/lib.rs index ddc956e60..e3eb21f98 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -24,7 +24,7 @@ use typst::foundations::{ use typst::layout::{Abs, Margin, PageElem, PagedDocument}; use typst::text::{Font, FontBook}; use typst::utils::LazyHash; -use typst::{Category, Feature, Library, LibraryBuilder}; +use typst::{Category, Feature, Library, LibraryExt}; use unicode_math_class::MathClass; macro_rules! load { @@ -51,7 +51,7 @@ static GROUPS: LazyLock> = LazyLock::new(|| { }); static LIBRARY: LazyLock> = LazyLock::new(|| { - let mut lib = LibraryBuilder::default() + let mut lib = Library::builder() .with_features([Feature::Html].into_iter().collect()) .build(); let scope = lib.global.scope_mut(); diff --git a/tests/fuzz/src/compile.rs b/tests/fuzz/src/compile.rs index 3dedfb737..945e9fce8 100644 --- a/tests/fuzz/src/compile.rs +++ b/tests/fuzz/src/compile.rs @@ -7,7 +7,7 @@ use typst::layout::PagedDocument; use typst::syntax::{FileId, Source}; use typst::text::{Font, FontBook}; use typst::utils::LazyHash; -use typst::{Library, World}; +use typst::{Library, LibraryExt, World}; struct FuzzWorld { library: LazyHash, diff --git a/tests/ref/block-clip-svg-glyphs.png b/tests/ref/block-clip-svg-glyphs.png index 0fc2c962f..bf13b7ea6 100644 Binary files a/tests/ref/block-clip-svg-glyphs.png and b/tests/ref/block-clip-svg-glyphs.png differ diff --git a/tests/ref/block-clip-text.png b/tests/ref/block-clip-text.png index 8c82bc309..2b099b48a 100644 Binary files a/tests/ref/block-clip-text.png and b/tests/ref/block-clip-text.png differ diff --git a/tests/ref/block-clipping-multiple-pages.png b/tests/ref/block-clipping-multiple-pages.png index 0b6e7c85f..1305ff204 100644 Binary files a/tests/ref/block-clipping-multiple-pages.png and b/tests/ref/block-clipping-multiple-pages.png differ diff --git a/tests/ref/box-clip-radius-without-stroke.png b/tests/ref/box-clip-radius-without-stroke.png index 2be772cc5..c7d83de22 100644 Binary files a/tests/ref/box-clip-radius-without-stroke.png and b/tests/ref/box-clip-radius-without-stroke.png differ diff --git a/tests/ref/box-clip-radius.png b/tests/ref/box-clip-radius.png index 530d53309..fb27bb74d 100644 Binary files a/tests/ref/box-clip-radius.png and b/tests/ref/box-clip-radius.png differ diff --git a/tests/ref/box-clip-rect.png b/tests/ref/box-clip-rect.png index fd62bbec8..8f307f754 100644 Binary files a/tests/ref/box-clip-rect.png and b/tests/ref/box-clip-rect.png differ diff --git a/tests/ref/closure-path-resolve-in-layout-phase.png b/tests/ref/closure-path-resolve-in-layout-phase.png index a3d699981..ca22e458c 100644 Binary files a/tests/ref/closure-path-resolve-in-layout-phase.png and b/tests/ref/closure-path-resolve-in-layout-phase.png differ diff --git a/tests/ref/hide-image.png b/tests/ref/hide-image.png index d671a0734..75975d249 100644 Binary files a/tests/ref/hide-image.png and b/tests/ref/hide-image.png differ diff --git a/tests/ref/html/cases-content-html.html b/tests/ref/html/cases-content-html.html new file mode 100644 index 000000000..0890f061a --- /dev/null +++ b/tests/ref/html/cases-content-html.html @@ -0,0 +1,10 @@ + + + + + + + +

my lower a
MY UPPER A

+ + diff --git a/tests/ref/html/image-jpg-html-base64.html b/tests/ref/html/image-jpg-html-base64.html new file mode 100644 index 000000000..89075323c --- /dev/null +++ b/tests/ref/html/image-jpg-html-base64.html @@ -0,0 +1,8 @@ + + + + + + + The letter F + diff --git a/tests/ref/html/image-scaling-methods.html b/tests/ref/html/image-scaling-methods.html new file mode 100644 index 000000000..a15664d51 --- /dev/null +++ b/tests/ref/html/image-scaling-methods.html @@ -0,0 +1,10 @@ + + + + + + + +
+ + diff --git a/tests/ref/image-baseline-with-box.png b/tests/ref/image-baseline-with-box.png index ade90e2f5..37403c809 100644 Binary files a/tests/ref/image-baseline-with-box.png and b/tests/ref/image-baseline-with-box.png differ diff --git a/tests/ref/issue-5499-text-fill-in-clip-block.png b/tests/ref/issue-5499-text-fill-in-clip-block.png index 5f7962d3b..ac81fe8ef 100644 Binary files a/tests/ref/issue-5499-text-fill-in-clip-block.png and b/tests/ref/issue-5499-text-fill-in-clip-block.png differ diff --git a/tests/ref/issue-6267-clip-anti-alias.png b/tests/ref/issue-6267-clip-anti-alias.png new file mode 100644 index 000000000..00a61bc2d Binary files /dev/null and b/tests/ref/issue-6267-clip-anti-alias.png differ diff --git a/tests/ref/math-style-fallback.png b/tests/ref/math-style-fallback.png new file mode 100644 index 000000000..de0283762 Binary files /dev/null and b/tests/ref/math-style-fallback.png differ diff --git a/tests/ref/math-style-hebrew-exceptions.png b/tests/ref/math-style-hebrew-exceptions.png index 723466e8a..a6f511e0e 100644 Binary files a/tests/ref/math-style-hebrew-exceptions.png and b/tests/ref/math-style-hebrew-exceptions.png differ diff --git a/tests/ref/math-style-script.png b/tests/ref/math-style-script.png new file mode 100644 index 000000000..379d270e7 Binary files /dev/null and b/tests/ref/math-style-script.png differ diff --git a/tests/ref/transform-rotate-relative-sizing.png b/tests/ref/transform-rotate-relative-sizing.png index 5951ff8ab..9b1d365df 100644 Binary files a/tests/ref/transform-rotate-relative-sizing.png and b/tests/ref/transform-rotate-relative-sizing.png differ diff --git a/tests/ref/transform-scale-relative-sizing.png b/tests/ref/transform-scale-relative-sizing.png index c53243c4b..01f0878b3 100644 Binary files a/tests/ref/transform-scale-relative-sizing.png and b/tests/ref/transform-scale-relative-sizing.png differ diff --git a/tests/ref/transform-skew-relative-sizing.png b/tests/ref/transform-skew-relative-sizing.png index af44fee98..4453a4811 100644 Binary files a/tests/ref/transform-skew-relative-sizing.png and b/tests/ref/transform-skew-relative-sizing.png differ diff --git a/tests/src/run.rs b/tests/src/run.rs index 1d93ba392..9af5c7899 100644 --- a/tests/src/run.rs +++ b/tests/src/run.rs @@ -5,10 +5,10 @@ use std::path::PathBuf; use ecow::eco_vec; use tiny_skia as sk; use typst::diag::{SourceDiagnostic, SourceResult, Warned}; -use typst::html::HtmlDocument; use typst::layout::{Abs, Frame, FrameItem, PagedDocument, Transform}; use typst::visualize::Color; use typst::{Document, WorldExt}; +use typst_html::HtmlDocument; use typst_pdf::PdfOptions; use typst_syntax::{FileId, Lines}; diff --git a/tests/src/world.rs b/tests/src/world.rs index 9b16d6126..4b6cf5a34 100644 --- a/tests/src/world.rs +++ b/tests/src/world.rs @@ -19,7 +19,7 @@ use typst::syntax::{FileId, Source, Span}; use typst::text::{Font, FontBook, TextElem, TextSize}; use typst::utils::{singleton, LazyHash}; use typst::visualize::Color; -use typst::{Feature, Library, World}; +use typst::{Feature, Library, LibraryExt, World}; use typst_syntax::Lines; /// A world that provides access to the tests environment. diff --git a/tests/suite/foundations/str.typ b/tests/suite/foundations/str.typ index 66fb912c0..aeaa0a0af 100644 --- a/tests/suite/foundations/str.typ +++ b/tests/suite/foundations/str.typ @@ -103,6 +103,10 @@ #test("Hello".last(), "o") #test("🏳️‍🌈A🏳️‍⚧️".first(), "🏳️‍🌈") #test("🏳️‍🌈A🏳️‍⚧️".last(), "🏳️‍⚧️") +#test("hey".first(default: "d"), "h") +#test("".first(default: "d"), "d") +#test("hey".last(default: "d"), "y") +#test("".last(default: "d"), "d") --- string-first-empty --- // Error: 2-12 string is empty diff --git a/tests/suite/layout/container.typ b/tests/suite/layout/container.typ index f15ddfe4a..489c88925 100644 --- a/tests/suite/layout/container.typ +++ b/tests/suite/layout/container.typ @@ -325,3 +325,10 @@ b a #block(height: -25pt)[b] c + +--- issue-6267-clip-anti-alias --- +#block( + clip: true, + radius: 100%, + rect(fill: gray, height: 1cm, width: 1cm), +) diff --git a/tests/suite/math/style.typ b/tests/suite/math/style.typ index 1fa2695e6..3ecf856b3 100644 --- a/tests/suite/math/style.typ +++ b/tests/suite/math/style.typ @@ -12,6 +12,15 @@ $A, italic(A), upright(A), bold(A), bold(upright(A)), \ bb("hello") + bold(cal("world")), \ mono("SQRT")(x) wreath mono(123 + 456)$ +--- math-style-fallback --- +// Test how math styles fallback. +$upright(frak(bold(alpha))) = upright(bold(alpha)) \ +bold(mono(ϝ)) = bold(ϝ) \ +sans(Theta) = bold(sans(Theta)) \ +bold(upright(planck)) != planck \ +bb(e) != italic(bb(e)) \ +serif(sans(A)) != serif(A)$ + --- math-style-dotless --- // Test styling dotless i and j. $ dotless.i dotless.j, @@ -21,7 +30,7 @@ $ dotless.i dotless.j, bb(dotless.i) bb(dotless.j), cal(dotless.i) cal(dotless.j), frak(dotless.i) frak(dotless.j), - mono(dotless.i) mono(dotless.j), + mono(dotless.i) mono(dotless.j), bold(frak(dotless.i)) upright(sans(dotless.j)), italic(bb(dotless.i)) frak(sans(dotless.j)) $ @@ -38,7 +47,15 @@ $bb(Gamma) , bb(gamma), bb(Pi), bb(pi), bb(sum)$ --- math-style-hebrew-exceptions --- // Test hebrew exceptions. -$aleph, beth, gimel, daleth$ +$aleph, beth, gimel, daleth$ \ +$upright(aleph), upright(beth), upright(gimel), upright(daleth)$ + +--- math-style-script --- +// Test variation selectors for scr and cal. +$cal(A) scr(A) bold(cal(O)) scr(bold(O))$ + +#show math.equation: set text(font: "Noto Sans Math") +$scr(E) cal(E) bold(scr(Y)) cal(bold(Y))$ --- issue-3650-italic-equation --- _abc $sin(x) "abc"$_ \ diff --git a/tests/suite/text/case.typ b/tests/suite/text/case.typ index 964ff28b6..c045ce7a6 100644 --- a/tests/suite/text/case.typ +++ b/tests/suite/text/case.typ @@ -14,6 +14,10 @@ // Check that cases are applied to symbols nested in content #lower($H I !$.body) +--- cases-content-html html --- +#lower[MY #html.strong[Lower] #symbol("A")] \ +#upper[my #html.strong[Upper] #symbol("a")] \ + --- upper-bad-type --- // Error: 8-9 expected string or content, found integer #upper(1) diff --git a/tests/suite/text/raw.typ b/tests/suite/text/raw.typ index a7f58a8d0..827edaf8c 100644 --- a/tests/suite/text/raw.typ +++ b/tests/suite/text/raw.typ @@ -687,6 +687,11 @@ a b c -------------------- #let hi = "你好world" ``` +--- issue-6559-equality-between-raws --- + +#test(`foo`, `foo`) +#assert.ne(`foo`, `bar`) + --- raw-theme-set-to-auto --- ```typ #let hi = "Hello World" diff --git a/tests/suite/visualize/image.typ b/tests/suite/visualize/image.typ index 45c70c4b8..36ec06cb1 100644 --- a/tests/suite/visualize/image.typ +++ b/tests/suite/visualize/image.typ @@ -9,6 +9,9 @@ #set page(height: 60pt) #image("/assets/images/tiger.jpg") +--- image-jpg-html-base64 html --- +#image("/assets/images/f2t.jpg", alt: "The letter F") + --- image-sizing --- // Test configuring the size and fitting behaviour of images. @@ -128,7 +131,7 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B width: 1cm, ) ---- image-scaling-methods --- +--- image-scaling-methods render html --- #let img(scaling) = image( bytes(( 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, @@ -144,14 +147,26 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B scaling: scaling, ) -#stack( - dir: ltr, - spacing: 4pt, +#let images = ( img(auto), img("smooth"), img("pixelated"), ) +#context if target() == "html" { + // TODO: Remove this once `stack` is supported in HTML export. + html.div( + style: "display: flex; flex-direction: row; gap: 4pt", + images.join(), + ) +} else { + stack( + dir: ltr, + spacing: 4pt, + ..images, + ) +} + --- image-natural-dpi-sizing --- // Test that images aren't upscaled. // Image is just 48x80 at 220dpi. It should not be scaled to fit the page