diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000..53e2ac1a7 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +testit = "test --workspace --test tests --" diff --git a/Cargo.lock b/Cargo.lock index 569b4d67d..282de6e5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,42 +448,19 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" -[[package]] -name = "comemo" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df6916408a724339aa77b18214233355f3eb04c42eb895e5f8909215bd8a7a91" -dependencies = [ - "comemo-macros 0.4.0", - "once_cell", - "parking_lot", - "siphasher", -] - [[package]] name = "comemo" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "649d7b2d867b569729c03c0f6968db10bc95921182a1f2b2012b1b549492f39d" dependencies = [ - "comemo-macros 0.5.0", + "comemo-macros", "parking_lot", "rustc-hash", "siphasher", "slab", ] -[[package]] -name = "comemo-macros" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8936e42f9b4f5bdfaf23700609ac1f11cb03ad4c1ec128a4ee4fd0903e228db" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "comemo-macros" version = "0.5.0" @@ -1445,11 +1422,11 @@ dependencies = [ [[package]] name = "krilla" version = "0.4.0" -source = "git+https://github.com/LaurenzV/krilla?branch=main#1246755ed5ff18a9a8c888694e4b91f3bed1b41a" +source = "git+https://github.com/LaurenzV/krilla?rev=1246755#1246755ed5ff18a9a8c888694e4b91f3bed1b41a" dependencies = [ "base64", "bumpalo", - "comemo 0.5.0", + "comemo", "flate2", "float-cmp 0.10.0", "gif", @@ -1475,7 +1452,7 @@ dependencies = [ [[package]] name = "krilla-svg" version = "0.1.0" -source = "git+https://github.com/LaurenzV/krilla?branch=main#1246755ed5ff18a9a8c888694e4b91f3bed1b41a" +source = "git+https://github.com/LaurenzV/krilla?rev=1246755#1246755ed5ff18a9a8c888694e4b91f3bed1b41a" dependencies = [ "flate2", "fontdb", @@ -2941,8 +2918,9 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" name = "typst" version = "0.13.1" dependencies = [ - "comemo 0.4.0", + "comemo", "ecow", + "rustc-hash", "typst-eval", "typst-html", "typst-layout", @@ -2969,7 +2947,7 @@ dependencies = [ "clap_mangen", "codespan-reporting", "color-print", - "comemo 0.4.0", + "comemo", "dirs", "ecow", "fs_extra", @@ -2978,6 +2956,7 @@ dependencies = [ "parking_lot", "pathdiff", "rayon", + "rustc-hash", "same-file", "self-replace", "semver", @@ -3018,6 +2997,7 @@ dependencies = [ "ecow", "heck", "pulldown-cmark", + "rustc-hash", "serde", "serde_json", "serde_yaml 0.9.34+deprecated", @@ -3037,9 +3017,10 @@ dependencies = [ name = "typst-eval" version = "0.13.1" dependencies = [ - "comemo 0.4.0", + "comemo", "ecow", "indexmap 2.7.1", + "rustc-hash", "stacker", "toml", "typst-library", @@ -3054,7 +3035,7 @@ dependencies = [ name = "typst-fuzz" version = "0.13.1" dependencies = [ - "comemo 0.4.0", + "comemo", "libfuzzer-sys", "typst", "typst-assets", @@ -3067,9 +3048,10 @@ name = "typst-html" version = "0.13.1" dependencies = [ "bumpalo", - "comemo 0.4.0", + "comemo", "ecow", "palette", + "rustc-hash", "time", "typst-assets", "typst-library", @@ -3084,10 +3066,11 @@ dependencies = [ name = "typst-ide" version = "0.13.1" dependencies = [ - "comemo 0.4.0", + "comemo", "ecow", "once_cell", "pathdiff", + "rustc-hash", "serde", "typst", "typst-assets", @@ -3127,7 +3110,7 @@ dependencies = [ "az", "bumpalo", "codex", - "comemo 0.4.0", + "comemo", "ecow", "hypher", "icu_properties", @@ -3137,6 +3120,7 @@ dependencies = [ "icu_segmenter", "kurbo", "memchr", + "rustc-hash", "rustybuzz", "smallvec", "ttf-parser", @@ -3162,7 +3146,7 @@ dependencies = [ "chinese-number", "ciborium", "codex", - "comemo 0.4.0", + "comemo", "csv", "ecow", "flate2", @@ -3188,6 +3172,7 @@ dependencies = [ "regex-syntax", "roxmltree", "rust_decimal", + "rustc-hash", "rustybuzz", "serde", "serde_json", @@ -3232,13 +3217,14 @@ version = "0.13.1" dependencies = [ "az", "bytemuck", - "comemo 0.4.0", + "comemo", "ecow", "image", "infer", "krilla", "krilla-svg", "pretty_assertions", + "rustc-hash", "serde", "smallvec", "typst-assets", @@ -3255,7 +3241,7 @@ version = "0.13.1" dependencies = [ "arrayvec", "bumpalo", - "comemo 0.4.0", + "comemo", "ecow", "regex", "typst-library", @@ -3270,7 +3256,7 @@ name = "typst-render" version = "0.13.1" dependencies = [ "bytemuck", - "comemo 0.4.0", + "comemo", "hayro", "image", "pixglyph", @@ -3288,11 +3274,12 @@ name = "typst-svg" version = "0.13.1" dependencies = [ "base64", - "comemo 0.4.0", + "comemo", "ecow", "flate2", "hayro", "image", + "rustc-hash", "ttf-parser", "typst-assets", "typst-library", @@ -3308,6 +3295,7 @@ name = "typst-syntax" version = "0.13.1" dependencies = [ "ecow", + "rustc-hash", "serde", "toml", "typst-timing", @@ -3324,12 +3312,13 @@ name = "typst-tests" version = "0.13.1" dependencies = [ "clap", - "comemo 0.4.0", + "comemo", "ecow", "oxipng", "parking_lot", "rayon", "regex", + "rustc-hash", "tiny-skia", "typst", "typst-assets", @@ -3361,6 +3350,7 @@ dependencies = [ "once_cell", "portable-atomic", "rayon", + "rustc-hash", "siphasher", "thin-vec", "unicode-math-class", diff --git a/Cargo.toml b/Cargo.toml index ce6c97f7a..8b095cb2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,7 @@ clap_mangen = "0.2.10" codespan-reporting = "0.11" codex = { git = "https://github.com/typst/codex", rev = "9ac86f9" } color-print = "0.3.6" -comemo = "0.4" +comemo = "0.5.0" csv = "1" ctrlc = "3.4.1" dirs = "6" @@ -59,6 +59,7 @@ fastrand = "2.3" flate2 = "1" fontdb = { version = "0.23", default-features = false } fs_extra = "1.3" +rustc-hash = "2.1" glidesort = "0.1.2" hayagriva = "0.8.1" hayro-syntax = { git = "https://github.com/LaurenzV/hayro", rev = "e701f95" } @@ -74,8 +75,8 @@ image = { version = "0.25.5", default-features = false, features = ["png", "jpeg indexmap = { version = "2", features = ["serde"] } infer = { version = "0.19.0", default-features = false } kamadak-exif = "0.6" -krilla = { git = "https://github.com/LaurenzV/krilla", branch = "main", default-features = false, features = ["raster-images", "comemo", "rayon", "pdf"] } -krilla-svg = { git = "https://github.com/LaurenzV/krilla", branch = "main" } +krilla = { git = "https://github.com/LaurenzV/krilla", rev = "1246755", default-features = false, features = ["raster-images", "comemo", "rayon", "pdf"] } +krilla-svg = { git = "https://github.com/LaurenzV/krilla", rev = "1246755" } kurbo = "0.11" libfuzzer-sys = "0.4" lipsum = "0.9" diff --git a/crates/typst-cli/Cargo.toml b/crates/typst-cli/Cargo.toml index 792cabae1..dcc4af868 100644 --- a/crates/typst-cli/Cargo.toml +++ b/crates/typst-cli/Cargo.toml @@ -41,6 +41,7 @@ open = { workspace = true } parking_lot = { workspace = true } pathdiff = { workspace = true } rayon = { workspace = true } +rustc-hash = { workspace = true } same-file = { workspace = true } self-replace = { workspace = true, optional = true } semver = { workspace = true } diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs index 092c09f4c..023b68ce7 100644 --- a/crates/typst-cli/src/args.rs +++ b/crates/typst-cli/src/args.rs @@ -155,6 +155,10 @@ pub struct QueryCommand { #[clap(long)] pub pretty: bool, + /// The target to compile for. + #[clap(long, default_value_t)] + pub target: Target, + /// World arguments. #[clap(flatten)] pub world: WorldArgs, @@ -464,6 +468,18 @@ pub enum OutputFormat { display_possible_values!(OutputFormat); +/// The target to compile for. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ValueEnum)] +pub enum Target { + /// PDF and image formats. + #[default] + Paged, + /// HTML. + Html, +} + +display_possible_values!(Target); + /// Which format to use for diagnostics. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ValueEnum)] pub enum DiagnosticFormat { diff --git a/crates/typst-cli/src/query.rs b/crates/typst-cli/src/query.rs index 30688b3b6..19c0b0348 100644 --- a/crates/typst-cli/src/query.rs +++ b/crates/typst-cli/src/query.rs @@ -5,11 +5,13 @@ use typst::World; use typst::diag::{HintedStrResult, StrResult, Warned, bail}; use typst::engine::Sink; use typst::foundations::{Content, IntoValue, LocatableSelector, Scope}; +use typst::introspection::Introspector; use typst::layout::PagedDocument; use typst::syntax::{Span, SyntaxMode}; use typst_eval::eval_string; +use typst_html::HtmlDocument; -use crate::args::{QueryCommand, SerializationFormat}; +use crate::args::{QueryCommand, SerializationFormat, Target}; use crate::compile::print_diagnostics; use crate::set_failed; use crate::world::SystemWorld; @@ -22,12 +24,17 @@ pub fn query(command: &QueryCommand) -> HintedStrResult<()> { world.reset(); world.source(world.main()).map_err(|err| err.to_string())?; - let Warned { output, warnings } = typst::compile(&world); + let Warned { output, warnings } = match command.target { + Target::Paged => typst::compile::(&world) + .map(|output| output.map(|document| document.introspector)), + Target::Html => typst::compile::(&world) + .map(|output| output.map(|document| document.introspector)), + }; match output { // Retrieve and print query results. - Ok(document) => { - let data = retrieve(&world, command, &document)?; + Ok(introspector) => { + let data = retrieve(&world, command, &introspector)?; let serialized = format(data, command)?; println!("{serialized}"); print_diagnostics(&world, &[], &warnings, command.process.diagnostic_format) @@ -54,7 +61,7 @@ pub fn query(command: &QueryCommand) -> HintedStrResult<()> { fn retrieve( world: &dyn World, command: &QueryCommand, - document: &PagedDocument, + introspector: &Introspector, ) -> HintedStrResult> { let selector = eval_string( &typst::ROUTINES, @@ -76,11 +83,7 @@ fn retrieve( })? .cast::()?; - Ok(document - .introspector - .query(&selector.0) - .into_iter() - .collect::>()) + Ok(introspector.query(&selector.0).into_iter().collect::>()) } /// Format the query result in the output format. diff --git a/crates/typst-cli/src/watch.rs b/crates/typst-cli/src/watch.rs index 151c0ca1d..860ae8ebd 100644 --- a/crates/typst-cli/src/watch.rs +++ b/crates/typst-cli/src/watch.rs @@ -1,4 +1,3 @@ -use std::collections::{HashMap, HashSet}; use std::io::{self, Write}; use std::iter; use std::path::PathBuf; @@ -9,6 +8,7 @@ use codespan_reporting::term::termcolor::WriteColor; use codespan_reporting::term::{self, termcolor}; use ecow::eco_format; use notify::{Event, RecommendedWatcher, RecursiveMode, Watcher as _}; +use rustc_hash::{FxHashMap, FxHashSet}; use same_file::is_same_file; use typst::diag::{StrResult, bail, warning}; use typst::syntax::Span; @@ -91,10 +91,10 @@ struct Watcher { /// Keeps track of which paths are watched via `watcher`. The boolean is /// used during updating for mark-and-sweep garbage collection of paths we /// should unwatch. - watched: HashMap, + watched: FxHashMap, /// A set of files that should be watched, but don't exist. We manually poll /// for those. - missing: HashSet, + missing: FxHashSet, } impl Watcher { @@ -127,8 +127,8 @@ impl Watcher { output, rx, watcher, - watched: HashMap::new(), - missing: HashSet::new(), + watched: FxHashMap::default(), + missing: FxHashSet::default(), }) } diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs index 34ebda64b..6dd362524 100644 --- a/crates/typst-cli/src/world.rs +++ b/crates/typst-cli/src/world.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::io::Read; use std::path::{Path, PathBuf}; use std::sync::{LazyLock, OnceLock}; @@ -7,6 +6,7 @@ use std::{fmt, fs, io, mem}; use chrono::{DateTime, Datelike, FixedOffset, Local, Utc}; use ecow::{EcoString, eco_format}; use parking_lot::Mutex; +use rustc_hash::FxHashMap; use typst::diag::{FileError, FileResult}; use typst::foundations::{Bytes, Datetime, Dict, IntoValue}; use typst::syntax::{FileId, Lines, Source, VirtualPath}; @@ -41,7 +41,7 @@ pub struct SystemWorld { /// Locations of and storage for lazily loaded fonts. fonts: Vec, /// Maps file ids to source files and buffers. - slots: Mutex>, + slots: Mutex>, /// Holds information about where packages are stored. package_storage: PackageStorage, /// The current datetime if requested. This is stored here to ensure it is @@ -139,7 +139,7 @@ impl SystemWorld { library: LazyHash::new(library), book: LazyHash::new(fonts.book), fonts: fonts.fonts, - slots: Mutex::new(HashMap::new()), + slots: Mutex::new(FxHashMap::default()), package_storage: package::storage(&world_args.package), now, }) diff --git a/crates/typst-eval/Cargo.toml b/crates/typst-eval/Cargo.toml index b39382ffe..d4fb0235b 100644 --- a/crates/typst-eval/Cargo.toml +++ b/crates/typst-eval/Cargo.toml @@ -21,6 +21,7 @@ typst-utils = { workspace = true } comemo = { workspace = true } ecow = { workspace = true } indexmap = { workspace = true } +rustc-hash = { workspace = true } toml = { workspace = true } unicode-segmentation = { workspace = true } diff --git a/crates/typst-eval/src/binding.rs b/crates/typst-eval/src/binding.rs index 9a53ac069..ccfb38583 100644 --- a/crates/typst-eval/src/binding.rs +++ b/crates/typst-eval/src/binding.rs @@ -1,6 +1,5 @@ -use std::collections::HashSet; - use ecow::eco_format; +use rustc_hash::FxHashSet; use typst_library::diag::{At, SourceDiagnostic, SourceResult, bail, error}; use typst_library::foundations::{Array, Dict, Value}; use typst_syntax::ast::{self, AstNode}; @@ -137,7 +136,7 @@ where F: Fn(&mut Vm, ast::Expr, Value) -> SourceResult<()>, { let mut sink = None; - let mut used = HashSet::new(); + let mut used = FxHashSet::default(); for p in destruct.items() { match p { diff --git a/crates/typst-eval/src/code.rs b/crates/typst-eval/src/code.rs index d3e0c937f..d15089f9a 100644 --- a/crates/typst-eval/src/code.rs +++ b/crates/typst-eval/src/code.rs @@ -246,7 +246,7 @@ impl Eval for ast::Dict<'_> { type Output = Dict; fn eval(self, vm: &mut Vm) -> SourceResult { - let mut map = indexmap::IndexMap::new(); + let mut map = indexmap::IndexMap::default(); let mut invalid_keys = eco_vec![]; for item in self.items() { diff --git a/crates/typst-eval/src/math.rs b/crates/typst-eval/src/math.rs index c2325a8c5..994ac9808 100644 --- a/crates/typst-eval/src/math.rs +++ b/crates/typst-eval/src/math.rs @@ -1,5 +1,5 @@ use ecow::eco_format; -use typst_library::diag::{At, SourceResult}; +use typst_library::diag::{At, SourceResult, warning}; use typst_library::foundations::{Content, NativeElement, Symbol, SymbolElem, Value}; use typst_library::math::{ AlignPointElem, AttachElem, FracElem, LrElem, PrimesElem, RootElem, @@ -80,7 +80,14 @@ impl Eval for ast::MathAttach<'_> { let mut elem = AttachElem::new(base); if let Some(expr) = self.top() { - elem.t.set(Some(expr.eval_display(vm)?)); + let top = expr.eval(vm)?; + if let Value::Func(_) = top { + vm.engine.sink.warn(warning!( + expr.span(), "function literal used as superscript"; + hint: "wrap the entire function call in parentheses", + )); + } + elem.t.set(Some(top.display().spanned(self.span()))); } // Always attach primes in scripts style (not limits style), @@ -90,7 +97,14 @@ impl Eval for ast::MathAttach<'_> { } if let Some(expr) = self.bottom() { - elem.b.set(Some(expr.eval_display(vm)?)); + let bottom = expr.eval(vm)?; + if let Value::Func(_) = bottom { + vm.engine.sink.warn(warning!( + expr.span(), "function literal used as subscript"; + hint: "wrap the entire function call in parentheses", + )); + } + elem.b.set(Some(bottom.display().spanned(self.span()))); } Ok(elem.pack()) diff --git a/crates/typst-html/Cargo.toml b/crates/typst-html/Cargo.toml index 54cad0124..8d592bc5a 100644 --- a/crates/typst-html/Cargo.toml +++ b/crates/typst-html/Cargo.toml @@ -24,6 +24,7 @@ bumpalo = { workspace = true } comemo = { workspace = true } ecow = { workspace = true } palette = { workspace = true } +rustc-hash = { workspace = true } time = { workspace = true } [lints] diff --git a/crates/typst-html/src/convert.rs b/crates/typst-html/src/convert.rs index 09d89313c..208ea3dd3 100644 --- a/crates/typst-html/src/convert.rs +++ b/crates/typst-html/src/convert.rs @@ -1,3 +1,4 @@ +use ecow::EcoVec; use typst_library::diag::{SourceResult, warning}; use typst_library::engine::Engine; use typst_library::foundations::{Content, StyleChain, Target, TargetElem}; @@ -15,8 +16,8 @@ pub fn convert_to_nodes<'a>( engine: &mut Engine, locator: &mut SplitLocator, children: impl IntoIterator>, -) -> SourceResult> { - let mut output = Vec::new(); +) -> SourceResult> { + let mut output = EcoVec::new(); for (child, styles) in children { handle(engine, child, locator, styles, &mut output)?; } @@ -29,12 +30,12 @@ fn handle( child: &Content, locator: &mut SplitLocator, styles: StyleChain, - output: &mut Vec, + output: &mut EcoVec, ) -> 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![]; + let mut children = EcoVec::new(); if let Some(body) = elem.body.get_ref(styles) { children = html_fragment(engine, body, locator.next(&elem.span()), styles)?; } diff --git a/crates/typst-html/src/css.rs b/crates/typst-html/src/css.rs index 5916d3147..ba0b8942f 100644 --- a/crates/typst-html/src/css.rs +++ b/crates/typst-html/src/css.rs @@ -26,7 +26,6 @@ impl Properties { } /// 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 diff --git a/crates/typst-html/src/document.rs b/crates/typst-html/src/document.rs index ca84d3354..d5ba536ca 100644 --- a/crates/typst-html/src/document.rs +++ b/crates/typst-html/src/document.rs @@ -1,7 +1,8 @@ -use std::collections::HashSet; use std::num::NonZeroUsize; use comemo::{Tracked, TrackedMut}; +use ecow::{EcoVec, eco_vec}; +use rustc_hash::FxHashSet; use typst_library::World; use typst_library::diag::{SourceResult, bail}; use typst_library::engine::{Engine, Route, Sink, Traced}; @@ -87,7 +88,7 @@ fn html_document_impl( children.iter().copied(), )?; - let mut link_targets = HashSet::new(); + let mut link_targets = FxHashSet::default(); let mut introspector = introspect_html(&output, &mut link_targets); let mut root = root_element(output, &info)?; crate::link::identify_link_targets(&mut root, &mut introspector, link_targets); @@ -99,12 +100,12 @@ fn html_document_impl( #[typst_macros::time(name = "introspect html")] fn introspect_html( output: &[HtmlNode], - link_targets: &mut HashSet, + link_targets: &mut FxHashSet, ) -> Introspector { fn discover( builder: &mut IntrospectorBuilder, sink: &mut Vec<(Content, Position)>, - link_targets: &mut HashSet, + link_targets: &mut FxHashSet, nodes: &[HtmlNode], ) { for node in nodes { @@ -141,19 +142,22 @@ fn introspect_html( /// Wrap the nodes in `` and `` if they are not yet rooted, /// supplying a suitable ``. -fn root_element(output: Vec, info: &DocumentInfo) -> SourceResult { +fn root_element( + output: EcoVec, + 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()])) + Ok(HtmlElement::new(tag::html).with_children(eco_vec![head.into(), body.into()])) } /// Generate a `` element. fn head_element(info: &DocumentInfo) -> HtmlElement { - let mut children = vec![]; + let mut children = EcoVec::new(); children.push(HtmlElement::new(tag::meta).with_attr(attr::charset, "utf-8").into()); @@ -167,7 +171,7 @@ fn head_element(info: &DocumentInfo) -> HtmlElement { if let Some(title) = &info.title { children.push( HtmlElement::new(tag::title) - .with_children(vec![HtmlNode::Text(title.clone(), Span::detached())]) + .with_children(eco_vec![HtmlNode::Text(title.clone(), Span::detached())]) .into(), ); } @@ -203,9 +207,9 @@ fn head_element(info: &DocumentInfo) -> HtmlElement { } /// Determine which kind of output the user generated. -fn classify_output(mut output: Vec) -> SourceResult { +fn classify_output(mut output: EcoVec) -> SourceResult { let count = output.iter().filter(|node| !matches!(node, HtmlNode::Tag(_))).count(); - for node in &mut output { + for node in output.make_mut() { let HtmlNode::Element(elem) = node else { continue }; let tag = elem.tag; let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html)); @@ -232,5 +236,5 @@ enum OutputKind { /// one, but need supply the `` element. Body(HtmlElement), /// The user generated leafs which we wrap in a `` and ``. - Leafs(Vec), + Leafs(EcoVec), } diff --git a/crates/typst-html/src/dom.rs b/crates/typst-html/src/dom.rs index 2c9a2e640..5f40473a0 100644 --- a/crates/typst-html/src/dom.rs +++ b/crates/typst-html/src/dom.rs @@ -57,7 +57,7 @@ pub struct HtmlElement { /// The element's attributes. pub attrs: HtmlAttrs, /// The element's children. - pub children: Vec, + pub children: EcoVec, /// The span from which the element originated, if any. pub span: Span, } @@ -68,7 +68,7 @@ impl HtmlElement { Self { tag, attrs: HtmlAttrs::default(), - children: vec![], + children: EcoVec::new(), span: Span::detached(), } } @@ -76,7 +76,7 @@ impl HtmlElement { /// Attach children to the element. /// /// Note: This overwrites potential previous children. - pub fn with_children(mut self, children: Vec) -> Self { + pub fn with_children(mut self, children: EcoVec) -> Self { self.children = children; self } @@ -105,8 +105,53 @@ impl HtmlTag { 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()); + let mut has_hyphen = false; + let mut has_uppercase = false; + + for c in string.chars() { + if c == '-' { + has_hyphen = true; + } else if !charsets::is_valid_in_tag_name(c) { + bail!("the character {} is not valid in a tag name", c.repr()); + } else { + has_uppercase |= c.is_ascii_uppercase(); + } + } + + // If we encounter a hyphen, we are dealing with a custom element rather + // than a standard HTML element. + // + // A valid custom element name must: + // - Contain at least one hyphen (U+002D) + // - Start with an ASCII lowercase letter (a-z) + // - Not contain any ASCII uppercase letters (A-Z) + // - Not be one of the reserved names + // - Only contain valid characters (ASCII alphanumeric and hyphens) + // + // See https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name + if has_hyphen { + if !string.starts_with(|c: char| c.is_ascii_lowercase()) { + bail!("custom element name must start with a lowercase letter"); + } + if has_uppercase { + bail!("custom element name must not contain uppercase letters"); + } + + // These names are used in SVG and MathML. Since `html.elem` only + // supports creation of _HTML_ elements, they are forbidden. + if matches!( + string, + "annotation-xml" + | "color-profile" + | "font-face" + | "font-face-src" + | "font-face-uri" + | "font-face-format" + | "font-face-name" + | "missing-glyph" + ) { + bail!("name is reserved and not valid for a custom element"); + } } Ok(Self(PicoStr::intern(string))) @@ -292,7 +337,7 @@ pub struct HtmlFrame { /// An ID to assign to the SVG itself. pub id: Option, /// IDs to assign to destination jump points within the SVG. - pub link_points: Vec<(Point, EcoString)>, + pub link_points: EcoVec<(Point, EcoString)>, } impl HtmlFrame { @@ -302,7 +347,7 @@ impl HtmlFrame { inner, text_size: styles.resolve(TextElem::size), id: None, - link_points: vec![], + link_points: EcoVec::new(), } } } diff --git a/crates/typst-html/src/fragment.rs b/crates/typst-html/src/fragment.rs index 66a59b678..73a5ed878 100644 --- a/crates/typst-html/src/fragment.rs +++ b/crates/typst-html/src/fragment.rs @@ -1,4 +1,5 @@ use comemo::{Track, Tracked, TrackedMut}; +use ecow::EcoVec; use typst_library::diag::{At, SourceResult}; use typst_library::engine::{Engine, Route, Sink, Traced}; use typst_library::foundations::{Content, StyleChain}; @@ -16,7 +17,7 @@ pub fn html_fragment( content: &Content, locator: Locator, styles: StyleChain, -) -> SourceResult> { +) -> SourceResult> { html_fragment_impl( engine.routines, engine.world, @@ -43,7 +44,7 @@ fn html_fragment_impl( content: &Content, locator: Tracked, styles: StyleChain, -) -> SourceResult> { +) -> SourceResult> { let link = LocatorLink::new(locator); let mut locator = Locator::link(&link).split(); let mut engine = Engine { diff --git a/crates/typst-html/src/lib.rs b/crates/typst-html/src/lib.rs index 373da607f..68311e8a3 100644 --- a/crates/typst-html/src/lib.rs +++ b/crates/typst-html/src/lib.rs @@ -16,7 +16,7 @@ mod typed; pub use self::document::html_document; pub use self::dom::*; pub use self::encode::html; -pub use self::rules::register; +pub use self::rules::{html_span_filled, register}; use ecow::EcoString; use typst_library::Category; diff --git a/crates/typst-html/src/link.rs b/crates/typst-html/src/link.rs index ad70f06fe..885458fd1 100644 --- a/crates/typst-html/src/link.rs +++ b/crates/typst-html/src/link.rs @@ -1,7 +1,8 @@ -use std::collections::{HashMap, HashSet, VecDeque}; +use std::collections::VecDeque; use comemo::Track; -use ecow::{EcoString, eco_format}; +use ecow::{EcoString, EcoVec, eco_format, eco_vec}; +use rustc_hash::{FxHashMap, FxHashSet}; use typst_library::foundations::{Label, NativeElement}; use typst_library::introspection::{Introspector, Location, Tag}; use typst_library::layout::{Frame, FrameItem, Point}; @@ -16,7 +17,7 @@ use crate::{HtmlElement, HtmlNode, attr, tag}; /// in favor of the query in `identify_link_targets`. For the time being, some /// links are created without existence of a `LinkElem`, so this is /// unfortunately necessary. -pub fn introspect_frame_links(frame: &Frame, targets: &mut HashSet) { +pub fn introspect_frame_links(frame: &Frame, targets: &mut FxHashSet) { for (_, item) in frame.items() { match item { FrameItem::Link(Destination::Location(loc), _) => { @@ -35,7 +36,7 @@ pub fn introspect_frame_links(frame: &Frame, targets: &mut HashSet) { pub fn identify_link_targets( root: &mut HtmlElement, introspector: &mut Introspector, - mut targets: HashSet, + mut targets: FxHashSet, ) { // Query for all links with an intra-doc (i.e. `Location`) destination to // know what needs IDs. @@ -72,13 +73,13 @@ pub fn identify_link_targets( /// Traverses a list of nodes. fn traverse( work: &mut Work, - targets: &HashSet, + targets: &FxHashSet, identificator: &mut Identificator<'_>, - nodes: &mut Vec, + nodes: &mut EcoVec, ) { let mut i = 0; while i < nodes.len() { - let node = &mut nodes[i]; + let node = &mut nodes.make_mut()[i]; match node { // When visiting a start tag, we check whether the element needs an // ID and if so, add it to the queue, so that its first child node @@ -114,7 +115,7 @@ fn traverse( HtmlNode::Text(..) => { work.drain(|label| { let mut element = - HtmlElement::new(tag::span).with_children(vec![node.clone()]); + HtmlElement::new(tag::span).with_children(eco_vec![node.clone()]); let id = identificator.assign(&mut element, label); *node = HtmlNode::Element(element); id @@ -144,10 +145,10 @@ fn traverse( /// Traverses a frame embedded in HTML. fn traverse_frame( work: &mut Work, - targets: &HashSet, + targets: &FxHashSet, identificator: &mut Identificator<'_>, frame: &Frame, - link_points: &mut Vec<(Point, EcoString)>, + link_points: &mut EcoVec<(Point, EcoString)>, ) { for (_, item) in frame.items() { match item { @@ -174,13 +175,13 @@ struct Work { /// now. queue: VecDeque<(Location, Option