diff --git a/Cargo.lock b/Cargo.lock index bea66f1e5..ec658f5db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3094,6 +3094,7 @@ dependencies = [ "parking_lot", "serde", "serde_json", + "web-sys", ] [[package]] @@ -3419,6 +3420,16 @@ dependencies = [ "indexmap-nostd", ] +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "weezl" version = "0.1.8" diff --git a/Cargo.toml b/Cargo.toml index ea4678839..290d1f207 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -134,6 +134,7 @@ ureq = { version = "2", default-features = false, features = ["native-tls", "gzi usvg = { version = "0.43", default-features = false, features = ["text"] } walkdir = "2" wasmi = "0.39.0" +web-sys = "0.3" xmlparser = "0.13.5" xmlwriter = "0.1.0" xmp-writer = "0.3" diff --git a/README.md b/README.md index 5d5c4798a..a5d20d2e6 100644 --- a/README.md +++ b/README.md @@ -5,19 +5,19 @@

Documentation + > Typst App + > Discord Server + > Apache-2 License + > Jobs at Typst + >

Typst is a new markup-based typesetting system that is designed to be as powerful @@ -39,7 +39,7 @@ A [gentle introduction][tutorial] to Typst is available in our documentation. However, if you want to see the power of Typst encapsulated in one image, here it is:

- Example + Example

diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs index 83c4c8f9e..d6855d100 100644 --- a/crates/typst-cli/src/args.rs +++ b/crates/typst-cli/src/args.rs @@ -473,6 +473,9 @@ pub enum PdfStandard { /// PDF/A-2b. #[value(name = "a-2b")] A_2b, + /// PDF/A-3b. + #[value(name = "a-3b")] + A_3b, } display_possible_values!(PdfStandard); diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index adeef0f2d..515a777a2 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -136,6 +136,7 @@ impl CompileConfig { .map(|standard| match standard { PdfStandard::V_1_7 => typst_pdf::PdfStandard::V_1_7, PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b, + PdfStandard::A_3b => typst_pdf::PdfStandard::A_3b, }) .collect::>(); PdfStandards::new(&list)? diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs index af6cf228f..12e80d273 100644 --- a/crates/typst-cli/src/world.rs +++ b/crates/typst-cli/src/world.rs @@ -305,7 +305,7 @@ impl FileSlot { ) -> FileResult { self.file.get_or_init( || read(self.id, project_root, package_storage), - |data, _| Ok(data.into()), + |data, _| Ok(Bytes::new(data)), ) } } diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs index fc934cef5..f59235c78 100644 --- a/crates/typst-eval/src/call.rs +++ b/crates/typst-eval/src/call.rs @@ -7,12 +7,11 @@ use typst_library::diag::{ use typst_library::engine::{Engine, Sink, Traced}; use typst_library::foundations::{ Arg, Args, Bytes, Capturer, Closure, Content, Context, Func, IntoValue, - NativeElement, Scope, Scopes, Value, + NativeElement, Scope, Scopes, SymbolElem, Value, }; use typst_library::introspection::Introspector; use typst_library::math::LrElem; use typst_library::routines::Routines; -use typst_library::text::TextElem; use typst_library::World; use typst_syntax::ast::{self, AstNode, Ident}; use typst_syntax::{Span, Spanned, SyntaxNode}; @@ -325,6 +324,13 @@ fn eval_field_call( } else if let Some(callee) = target.ty().scope().get(&field) { args.insert(0, target_expr.span(), target); Ok(FieldCall::Normal(callee.clone(), args)) + } else if let Value::Content(content) = &target { + if let Some(callee) = content.elem().scope().get(&field) { + args.insert(0, target_expr.span(), target); + Ok(FieldCall::Normal(callee.clone(), args)) + } else { + bail!(missing_field_call_error(target, field)) + } } else if matches!( target, Value::Symbol(_) | Value::Func(_) | Value::Type(_) | Value::Module(_) @@ -341,8 +347,20 @@ fn eval_field_call( /// Produce an error when we cannot call the field. fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic { - let mut error = - error!(field.span(), "type {} has no method `{}`", target.ty(), field.as_str()); + let mut error = match &target { + Value::Content(content) => error!( + field.span(), + "element {} has no method `{}`", + content.elem().name(), + field.as_str(), + ), + _ => error!( + field.span(), + "type {} has no method `{}`", + target.ty(), + field.as_str() + ), + }; match target { Value::Dict(ref dict) if matches!(dict.get(&field), Ok(Value::Func(_))) => { @@ -360,6 +378,7 @@ fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic { } _ => {} } + error } @@ -382,16 +401,16 @@ fn wrap_args_in_math( let mut body = Content::empty(); for (i, arg) in args.all::()?.into_iter().enumerate() { if i > 0 { - body += TextElem::packed(','); + body += SymbolElem::packed(','); } body += arg; } if trailing_comma { - body += TextElem::packed(','); + body += SymbolElem::packed(','); } Ok(Value::Content( callee.display().spanned(callee_span) - + LrElem::new(TextElem::packed('(') + body + TextElem::packed(')')) + + LrElem::new(SymbolElem::packed('(') + body + SymbolElem::packed(')')) .pack() .spanned(args.span), )) @@ -685,8 +704,7 @@ mod tests { // Named-params. test(s, "$ foo(bar: y) $", &["foo"]); - // This should be updated when we improve named-param parsing: - test(s, "$ foo(x-y: 1, bar-z: 2) $", &["bar", "foo"]); + test(s, "$ foo(x-y: 1, bar-z: 2) $", &["foo"]); // Field access in math. test(s, "$ foo.bar $", &["foo"]); diff --git a/crates/typst-eval/src/code.rs b/crates/typst-eval/src/code.rs index 34373fd4a..2baf4ea9e 100644 --- a/crates/typst-eval/src/code.rs +++ b/crates/typst-eval/src/code.rs @@ -99,6 +99,7 @@ impl Eval for ast::Expr<'_> { Self::Term(v) => v.eval(vm).map(Value::Content), Self::Equation(v) => v.eval(vm).map(Value::Content), Self::Math(v) => v.eval(vm).map(Value::Content), + Self::MathText(v) => v.eval(vm).map(Value::Content), Self::MathIdent(v) => v.eval(vm), Self::MathShorthand(v) => v.eval(vm), Self::MathAlignPoint(v) => v.eval(vm).map(Value::Content), diff --git a/crates/typst-eval/src/import.rs b/crates/typst-eval/src/import.rs index 5b67c0608..2060d25f1 100644 --- a/crates/typst-eval/src/import.rs +++ b/crates/typst-eval/src/import.rs @@ -211,7 +211,7 @@ fn resolve_package( // Evaluate the manifest. let manifest_id = FileId::new(Some(spec.clone()), VirtualPath::new("typst.toml")); let bytes = engine.world.file(manifest_id).at(span)?; - let string = std::str::from_utf8(&bytes).map_err(FileError::from).at(span)?; + let string = bytes.as_str().map_err(FileError::from).at(span)?; let manifest: PackageManifest = toml::from_str(string) .map_err(|err| eco_format!("package manifest is malformed ({})", err.message())) .at(span)?; diff --git a/crates/typst-eval/src/math.rs b/crates/typst-eval/src/math.rs index 51dc0a3d5..bfb54aa87 100644 --- a/crates/typst-eval/src/math.rs +++ b/crates/typst-eval/src/math.rs @@ -1,11 +1,11 @@ use ecow::eco_format; use typst_library::diag::{At, SourceResult}; -use typst_library::foundations::{Content, NativeElement, Symbol, Value}; +use typst_library::foundations::{Content, NativeElement, Symbol, SymbolElem, Value}; use typst_library::math::{ AlignPointElem, AttachElem, FracElem, LrElem, PrimesElem, RootElem, }; use typst_library::text::TextElem; -use typst_syntax::ast::{self, AstNode}; +use typst_syntax::ast::{self, AstNode, MathTextKind}; use crate::{Eval, Vm}; @@ -20,6 +20,17 @@ impl Eval for ast::Math<'_> { } } +impl Eval for ast::MathText<'_> { + type Output = Content; + + fn eval(self, _: &mut Vm) -> SourceResult { + match self.get() { + MathTextKind::Character(c) => Ok(SymbolElem::packed(c)), + MathTextKind::Number(text) => Ok(TextElem::packed(text.clone())), + } + } +} + impl Eval for ast::MathIdent<'_> { type Output = Value; @@ -102,6 +113,7 @@ impl Eval for ast::MathRoot<'_> { type Output = Content; fn eval(self, vm: &mut Vm) -> SourceResult { + // Use `TextElem` to match `MathTextKind::Number` above. let index = self.index().map(|i| TextElem::packed(eco_format!("{i}"))); let radicand = self.radicand().eval_display(vm)?; Ok(RootElem::new(radicand).with_index(index).pack()) diff --git a/crates/typst-html/src/encode.rs b/crates/typst-html/src/encode.rs index b87b0e1d6..612f923fc 100644 --- a/crates/typst-html/src/encode.rs +++ b/crates/typst-html/src/encode.rs @@ -2,7 +2,7 @@ use std::fmt::Write; use typst_library::diag::{bail, At, SourceResult, StrResult}; use typst_library::foundations::Repr; -use typst_library::html::{charsets, tag, HtmlDocument, HtmlElement, HtmlNode}; +use typst_library::html::{charsets, tag, HtmlDocument, HtmlElement, HtmlNode, HtmlTag}; use typst_library::layout::Frame; use typst_syntax::Span; @@ -12,15 +12,19 @@ pub fn html(document: &HtmlDocument) -> SourceResult { w.buf.push_str(""); write_indent(&mut w); write_element(&mut w, &document.root)?; + if w.pretty { + w.buf.push('\n'); + } Ok(w.buf) } #[derive(Default)] struct Writer { + /// The output buffer. buf: String, - /// current indentation level + /// The current indentation level level: usize, - /// pretty printing enabled? + /// Whether pretty printing is enabled. pretty: bool, } @@ -85,26 +89,32 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> { let pretty = w.pretty; if !element.children.is_empty() { - w.pretty &= is_pretty(element); + let pretty_inside = allows_pretty_inside(element.tag) + && element.children.iter().any(|node| match node { + HtmlNode::Element(child) => wants_pretty_around(child.tag), + _ => false, + }); + + w.pretty &= pretty_inside; let mut indent = w.pretty; w.level += 1; for c in &element.children { - let pretty_child = match c { + let pretty_around = match c { HtmlNode::Tag(_) => continue, - HtmlNode::Element(element) => is_pretty(element), + HtmlNode::Element(child) => w.pretty && wants_pretty_around(child.tag), HtmlNode::Text(..) | HtmlNode::Frame(_) => false, }; - if core::mem::take(&mut indent) || pretty_child { + if core::mem::take(&mut indent) || pretty_around { write_indent(w); } write_node(w, c)?; - indent = pretty_child; + indent = pretty_around; } w.level -= 1; - write_indent(w) + write_indent(w); } w.pretty = pretty; @@ -115,9 +125,27 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> { Ok(()) } -/// Whether the element should be pretty-printed. -fn is_pretty(element: &HtmlElement) -> bool { - tag::is_block_by_default(element.tag) || matches!(element.tag, tag::meta) +/// Whether we are allowed to add an extra newline at the start and end of the +/// element's contents. +/// +/// Technically, users can change CSS `display` properties such that the +/// insertion of whitespace may actually impact the visual output. For example, +/// shows how adding CSS +/// rules to `

` can make it sensitive to whitespace. For this reason, we +/// should also respect the `style` tag in the future. +fn allows_pretty_inside(tag: HtmlTag) -> bool { + (tag::is_block_by_default(tag) && tag != tag::pre) + || tag::is_tabular_by_default(tag) + || tag == tag::li +} + +/// Whether newlines should be added before and after the element if the parent +/// allows it. +/// +/// In contrast to `allows_pretty_inside`, which is purely spec-driven, this is +/// more subjective and depends on preference. +fn wants_pretty_around(tag: HtmlTag) -> bool { + allows_pretty_inside(tag) || tag::is_metadata(tag) || tag == tag::pre } /// Escape a character. diff --git a/crates/typst-html/src/lib.rs b/crates/typst-html/src/lib.rs index ffd8e2505..25d0cd5d8 100644 --- a/crates/typst-html/src/lib.rs +++ b/crates/typst-html/src/lib.rs @@ -14,9 +14,9 @@ use typst_library::html::{ use typst_library::introspection::{ Introspector, Locator, LocatorLink, SplitLocator, TagElem, }; -use typst_library::layout::{Abs, Axes, BoxElem, Region, Size}; +use typst_library::layout::{Abs, Axes, BlockBody, BlockElem, BoxElem, Region, Size}; use typst_library::model::{DocumentInfo, ParElem}; -use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; +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; @@ -139,7 +139,9 @@ fn html_fragment_impl( let arenas = Arenas::default(); let children = (engine.routines.realize)( - RealizationKind::HtmlFragment, + // No need to know about the `FragmentKind` because we handle both + // uniformly. + RealizationKind::HtmlFragment(&mut FragmentKind::Block), &mut engine, &mut locator, &arenas, @@ -189,7 +191,8 @@ fn handle( }; output.push(element.into()); } else if let Some(elem) = child.to_packed::() { - let children = handle_list(engine, locator, elem.children.iter(&styles))?; + let children = + html_fragment(engine, &elem.body, locator.next(&elem.span()), styles)?; output.push( HtmlElement::new(tag::p) .with_children(children) @@ -197,13 +200,34 @@ fn handle( .into(), ); } else if let Some(elem) = child.to_packed::() { - // FIXME: Very incomplete and hacky, but makes boxes kind fulfill their - // purpose for now. + // TODO: This is rather incomplete. if let Some(body) = elem.body(styles) { let children = html_fragment(engine, body, locator.next(&elem.span()), styles)?; - output.extend(children); + 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(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::() { diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index c22ea7e40..0f8abddb7 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -817,19 +817,8 @@ fn param_value_completions<'a>( ) { if param.name == "font" { ctx.font_completions(); - } else if param.name == "path" { - ctx.file_completions_with_extensions(match func.name() { - Some("image") => &["png", "jpg", "jpeg", "gif", "svg", "svgz"], - Some("csv") => &["csv"], - Some("plugin") => &["wasm"], - Some("cbor") => &["cbor"], - Some("json") => &["json"], - Some("toml") => &["toml"], - Some("xml") => &["xml"], - Some("yaml") => &["yml", "yaml"], - Some("bibliography") => &["bib", "yml", "yaml"], - _ => &[], - }); + } else if let Some(extensions) = path_completion(func, param) { + ctx.file_completions_with_extensions(extensions); } else if func.name() == Some("figure") && param.name == "body" { ctx.snippet_completion("image", "image(\"${}\"),", "An image in a figure."); ctx.snippet_completion("table", "table(\n ${}\n),", "A table in a figure."); @@ -838,6 +827,28 @@ fn param_value_completions<'a>( ctx.cast_completions(¶m.input); } +/// Returns which file extensions to complete for the given parameter if any. +fn path_completion(func: &Func, param: &ParamInfo) -> Option<&'static [&'static str]> { + Some(match (func.name(), param.name) { + (Some("image"), "source") => &["png", "jpg", "jpeg", "gif", "svg", "svgz"], + (Some("csv"), "source") => &["csv"], + (Some("plugin"), "source") => &["wasm"], + (Some("cbor"), "source") => &["cbor"], + (Some("json"), "source") => &["json"], + (Some("toml"), "source") => &["toml"], + (Some("xml"), "source") => &["xml"], + (Some("yaml"), "source") => &["yml", "yaml"], + (Some("bibliography"), "sources") => &["bib", "yml", "yaml"], + (Some("bibliography"), "style") => &["csl"], + (Some("cite"), "style") => &["csl"], + (Some("raw"), "syntaxes") => &["sublime-syntax"], + (Some("raw"), "theme") => &["tmtheme"], + (Some("embed"), "path") => &[], + (None, "path") => &[], + _ => return None, + }) +} + /// Resolve a callee expression to a global function. fn resolve_global_callee<'a>( ctx: &CompletionContext<'a>, diff --git a/crates/typst-ide/src/matchers.rs b/crates/typst-ide/src/matchers.rs index d02eb2a95..b92cbf557 100644 --- a/crates/typst-ide/src/matchers.rs +++ b/crates/typst-ide/src/matchers.rs @@ -89,15 +89,21 @@ pub fn named_items( // ``` Some(ast::Imports::Items(items)) => { for item in items.iter() { - let original = item.original_name(); let bound = item.bound_name(); - let scope = source.and_then(|(value, _)| value.scope()); - let span = scope - .and_then(|s| s.get_span(&original)) - .unwrap_or(Span::detached()) - .or(bound.span()); - let value = scope.and_then(|s| s.get(&original)); + let (span, value) = item.path().iter().fold( + (bound.span(), source.map(|(value, _)| value)), + |(span, value), path_ident| { + let scope = value.and_then(|v| v.scope()); + let span = scope + .and_then(|s| s.get_span(&path_ident)) + .unwrap_or(Span::detached()) + .or(span); + let value = scope.and_then(|s| s.get(&path_ident)); + (span, value) + }, + ); + if let Some(res) = recv(NamedItem::Import(bound.get(), span, value)) { @@ -269,16 +275,18 @@ mod tests { use std::borrow::Borrow; use ecow::EcoString; + use typst::foundations::Value; use typst::syntax::{LinkedNode, Side}; use super::named_items; - use crate::tests::{FilePos, WorldLike}; + use crate::tests::{FilePos, TestWorld, WorldLike}; - type Response = Vec; + type Response = Vec<(EcoString, Option)>; trait ResponseExt { fn must_include<'a>(&self, includes: impl IntoIterator) -> &Self; fn must_exclude<'a>(&self, excludes: impl IntoIterator) -> &Self; + fn must_include_value(&self, name_value: (&str, Option<&Value>)) -> &Self; } impl ResponseExt for Response { @@ -286,7 +294,7 @@ mod tests { fn must_include<'a>(&self, includes: impl IntoIterator) -> &Self { for item in includes { assert!( - self.iter().any(|v| v == item), + self.iter().any(|v| v.0 == item), "{item:?} was not contained in {self:?}", ); } @@ -297,12 +305,21 @@ mod tests { fn must_exclude<'a>(&self, excludes: impl IntoIterator) -> &Self { for item in excludes { assert!( - !self.iter().any(|v| v == item), + !self.iter().any(|v| v.0 == item), "{item:?} was wrongly contained in {self:?}", ); } self } + + #[track_caller] + fn must_include_value(&self, name_value: (&str, Option<&Value>)) -> &Self { + assert!( + self.iter().any(|v| (v.0.as_str(), v.1.as_ref()) == name_value), + "{name_value:?} was not contained in {self:?}", + ); + self + } } #[track_caller] @@ -314,7 +331,7 @@ mod tests { let leaf = node.leaf_at(cursor, Side::After).unwrap(); let mut items = vec![]; named_items(world, leaf, |s| { - items.push(s.name().clone()); + items.push((s.name().clone(), s.value().clone())); None::<()> }); items @@ -340,5 +357,10 @@ mod tests { #[test] fn test_named_items_import() { test("#import \"foo.typ\": a; #(a);", 2).must_include(["a"]); + + let world = TestWorld::new("#import \"foo.typ\": a.b; #(b);") + .with_source("foo.typ", "#import \"a.typ\"") + .with_source("a.typ", "#let b = 1;"); + test(&world, 2).must_include_value(("b", Some(&Value::Int(1)))); } } diff --git a/crates/typst-ide/src/tests.rs b/crates/typst-ide/src/tests.rs index f41808dac..6678ab841 100644 --- a/crates/typst-ide/src/tests.rs +++ b/crates/typst-ide/src/tests.rs @@ -55,7 +55,7 @@ impl TestWorld { pub fn with_asset_at(mut self, path: &str, filename: &str) -> Self { let id = FileId::new(None, VirtualPath::new(path)); let data = typst_dev_assets::get_by_name(filename).unwrap(); - let bytes = Bytes::from_static(data); + let bytes = Bytes::new(data); Arc::make_mut(&mut self.files).assets.insert(id, bytes); self } @@ -152,7 +152,7 @@ impl Default for TestBase { fn default() -> Self { let fonts: Vec<_> = typst_assets::fonts() .chain(typst_dev_assets::fonts()) - .flat_map(|data| Font::iter(Bytes::from_static(data))) + .flat_map(|data| Font::iter(Bytes::new(data))) .collect(); Self { diff --git a/crates/typst-kit/src/fonts.rs b/crates/typst-kit/src/fonts.rs index 83e13fd8f..c15d739ec 100644 --- a/crates/typst-kit/src/fonts.rs +++ b/crates/typst-kit/src/fonts.rs @@ -13,6 +13,7 @@ use std::path::{Path, PathBuf}; use std::sync::OnceLock; use fontdb::{Database, Source}; +use typst_library::foundations::Bytes; use typst_library::text::{Font, FontBook, FontInfo}; use typst_timing::TimingScope; @@ -52,9 +53,8 @@ impl FontSlot { .as_ref() .expect("`path` is not `None` if `font` is uninitialized"), ) - .ok()? - .into(); - Font::new(data, self.index) + .ok()?; + Font::new(Bytes::new(data), self.index) }) .clone() } @@ -196,7 +196,7 @@ impl FontSearcher { #[cfg(feature = "embed-fonts")] fn add_embedded(&mut self) { for data in typst_assets::fonts() { - let buffer = typst_library::foundations::Bytes::from_static(data); + let buffer = Bytes::new(data); for (i, font) in Font::iter(buffer).enumerate() { self.book.push(font.info().clone()); self.fonts.push(FontSlot { diff --git a/crates/typst-layout/src/flow/collect.rs b/crates/typst-layout/src/flow/collect.rs index 12cfa152e..34362a6c5 100644 --- a/crates/typst-layout/src/flow/collect.rs +++ b/crates/typst-layout/src/flow/collect.rs @@ -20,12 +20,16 @@ use typst_library::model::ParElem; use typst_library::routines::{Pair, Routines}; use typst_library::text::TextElem; use typst_library::World; +use typst_utils::SliceExt; -use super::{layout_multi_block, layout_single_block}; +use super::{layout_multi_block, layout_single_block, FlowMode}; +use crate::inline::ParSituation; +use crate::modifiers::layout_and_modify; /// Collects all elements of the flow into prepared children. These are much /// simpler to handle than the raw elements. #[typst_macros::time] +#[allow(clippy::too_many_arguments)] pub fn collect<'a>( engine: &mut Engine, bump: &'a Bump, @@ -33,6 +37,7 @@ pub fn collect<'a>( locator: Locator<'a>, base: Size, expand: bool, + mode: FlowMode, ) -> SourceResult>> { Collector { engine, @@ -42,9 +47,9 @@ pub fn collect<'a>( base, expand, output: Vec::with_capacity(children.len()), - last_was_par: false, + par_situation: ParSituation::First, } - .run() + .run(mode) } /// State for collection. @@ -56,12 +61,20 @@ struct Collector<'a, 'x, 'y> { expand: bool, locator: SplitLocator<'a>, output: Vec>, - last_was_par: bool, + par_situation: ParSituation, } impl<'a> Collector<'a, '_, '_> { /// Perform the collection. - fn run(mut self) -> SourceResult>> { + fn run(self, mode: FlowMode) -> SourceResult>> { + match mode { + FlowMode::Root | FlowMode::Block => self.run_block(), + FlowMode::Inline => self.run_inline(), + } + } + + /// Perform collection for block-level children. + fn run_block(mut self) -> SourceResult>> { for &(child, styles) in self.children { if let Some(elem) = child.to_packed::() { self.output.push(Child::Tag(&elem.tag)); @@ -94,6 +107,42 @@ impl<'a> Collector<'a, '_, '_> { Ok(self.output) } + /// Perform collection for inline-level children. + fn run_inline(mut self) -> SourceResult>> { + // Extract leading and trailing tags. + let (start, end) = self.children.split_prefix_suffix(|(c, _)| c.is::()); + let inner = &self.children[start..end]; + + // Compute the shared styles, ignoring tags. + let styles = StyleChain::trunk(inner.iter().map(|&(_, s)| s)).unwrap_or_default(); + + // Layout the lines. + let lines = crate::inline::layout_inline( + self.engine, + inner, + &mut self.locator, + styles, + self.base, + self.expand, + None, + )? + .into_frames(); + + for (c, _) in &self.children[..start] { + let elem = c.to_packed::().unwrap(); + self.output.push(Child::Tag(&elem.tag)); + } + + self.lines(lines, styles); + + for (c, _) in &self.children[end..] { + let elem = c.to_packed::().unwrap(); + self.output.push(Child::Tag(&elem.tag)); + } + + Ok(self.output) + } + /// Collect vertical spacing into a relative or fractional child. fn v(&mut self, elem: &'a Packed, styles: StyleChain<'a>) { self.output.push(match elem.amount { @@ -109,24 +158,34 @@ impl<'a> Collector<'a, '_, '_> { elem: &'a Packed, styles: StyleChain<'a>, ) -> SourceResult<()> { - let align = AlignElem::alignment_in(styles).resolve(styles); - let leading = ParElem::leading_in(styles); - let spacing = ParElem::spacing_in(styles); - let costs = TextElem::costs_in(styles); - - let lines = crate::layout_inline( + let lines = crate::inline::layout_par( + elem, self.engine, - &elem.children, self.locator.next(&elem.span()), styles, - self.last_was_par, self.base, self.expand, + self.par_situation, )? .into_frames(); + let spacing = ParElem::spacing_in(styles); self.output.push(Child::Rel(spacing.into(), 4)); + self.lines(lines, styles); + + self.output.push(Child::Rel(spacing.into(), 4)); + self.par_situation = ParSituation::Consecutive; + + Ok(()) + } + + /// Collect laid-out lines. + fn lines(&mut self, lines: Vec, styles: StyleChain<'a>) { + let align = AlignElem::alignment_in(styles).resolve(styles); + let leading = ParElem::leading_in(styles); + let costs = TextElem::costs_in(styles); + // Determine whether to prevent widow and orphans. let len = lines.len(); let prevent_orphans = @@ -165,11 +224,6 @@ impl<'a> Collector<'a, '_, '_> { self.output .push(Child::Line(self.boxed(LineChild { frame, align, need }))); } - - self.output.push(Child::Rel(spacing.into(), 4)); - self.last_was_par = true; - - Ok(()) } /// Collect a block into a [`SingleChild`] or [`MultiChild`] depending on @@ -218,7 +272,7 @@ impl<'a> Collector<'a, '_, '_> { }; self.output.push(spacing(elem.below(styles))); - self.last_was_par = false; + self.par_situation = ParSituation::Other; } /// Collects a placed element into a [`PlacedChild`]. @@ -377,8 +431,9 @@ fn layout_single_impl( route: Route::extend(route), }; - layout_single_block(elem, &mut engine, locator, styles, region) - .map(|frame| frame.post_processed(styles)) + layout_and_modify(styles, |styles| { + layout_single_block(elem, &mut engine, locator, styles, region) + }) } /// A child that encapsulates a prepared breakable block. @@ -473,11 +528,8 @@ fn layout_multi_impl( route: Route::extend(route), }; - layout_multi_block(elem, &mut engine, locator, styles, regions).map(|mut fragment| { - for frame in &mut fragment { - frame.post_process(styles); - } - fragment + layout_and_modify(styles, |styles| { + layout_multi_block(elem, &mut engine, locator, styles, regions) }) } @@ -579,20 +631,23 @@ impl PlacedChild<'_> { self.cell.get_or_init(base, |base| { let align = self.alignment.unwrap_or_else(|| Alignment::CENTER); let aligned = AlignElem::set_alignment(align).wrap(); + let styles = self.styles.chain(&aligned); - let mut frame = crate::layout_frame( - engine, - &self.elem.body, - self.locator.relayout(), - self.styles.chain(&aligned), - Region::new(base, Axes::splat(false)), - )?; + let mut frame = layout_and_modify(styles, |styles| { + crate::layout_frame( + engine, + &self.elem.body, + self.locator.relayout(), + styles, + Region::new(base, Axes::splat(false)), + ) + })?; if self.float { frame.set_parent(self.elem.location().unwrap()); } - Ok(frame.post_processed(self.styles)) + Ok(frame) }) } diff --git a/crates/typst-layout/src/flow/compose.rs b/crates/typst-layout/src/flow/compose.rs index 3cf66f9e3..76af8f650 100644 --- a/crates/typst-layout/src/flow/compose.rs +++ b/crates/typst-layout/src/flow/compose.rs @@ -17,7 +17,9 @@ use typst_library::model::{ use typst_syntax::Span; use typst_utils::{NonZeroExt, Numeric}; -use super::{distribute, Config, FlowResult, LineNumberConfig, PlacedChild, Stop, Work}; +use super::{ + distribute, Config, FlowMode, FlowResult, LineNumberConfig, PlacedChild, Stop, Work, +}; /// Composes the contents of a single page/region. A region can have multiple /// columns/subregions. @@ -356,7 +358,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { migratable: bool, ) -> FlowResult<()> { // Footnotes are only supported at the root level. - if !self.config.root { + if self.config.mode != FlowMode::Root { return Ok(()); } diff --git a/crates/typst-layout/src/flow/mod.rs b/crates/typst-layout/src/flow/mod.rs index 2f0ec39a9..2acbbcef3 100644 --- a/crates/typst-layout/src/flow/mod.rs +++ b/crates/typst-layout/src/flow/mod.rs @@ -25,7 +25,7 @@ use typst_library::layout::{ Regions, Rel, Size, }; use typst_library::model::{FootnoteElem, FootnoteEntry, LineNumberingScope, ParLine}; -use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; +use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind, Routines}; use typst_library::text::TextElem; use typst_library::World; use typst_utils::{NonZeroExt, Numeric}; @@ -140,9 +140,10 @@ fn layout_fragment_impl( engine.route.check_layout_depth().at(content.span())?; + let mut kind = FragmentKind::Block; let arenas = Arenas::default(); let children = (engine.routines.realize)( - RealizationKind::LayoutFragment, + RealizationKind::LayoutFragment(&mut kind), &mut engine, &mut locator, &arenas, @@ -158,25 +159,46 @@ fn layout_fragment_impl( regions, columns, column_gutter, - false, + kind.into(), ) } +/// The mode a flow can be laid out in. +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub enum FlowMode { + /// A root flow with block-level elements. Like `FlowMode::Block`, but can + /// additionally host footnotes and line numbers. + Root, + /// A flow whose children are block-level elements. + Block, + /// A flow whose children are inline-level elements. + Inline, +} + +impl From for FlowMode { + fn from(value: FragmentKind) -> Self { + match value { + FragmentKind::Inline => Self::Inline, + FragmentKind::Block => Self::Block, + } + } +} + /// Lays out realized content into regions, potentially with columns. #[allow(clippy::too_many_arguments)] -pub(crate) fn layout_flow( +pub fn layout_flow<'a>( engine: &mut Engine, - children: &[Pair], - locator: &mut SplitLocator, - shared: StyleChain, + children: &[Pair<'a>], + locator: &mut SplitLocator<'a>, + shared: StyleChain<'a>, mut regions: Regions, columns: NonZeroUsize, column_gutter: Rel, - root: bool, + mode: FlowMode, ) -> SourceResult { // Prepare configuration that is shared across the whole flow. let config = Config { - root, + mode, shared, columns: { let mut count = columns.get(); @@ -195,7 +217,7 @@ pub(crate) fn layout_flow( gap: FootnoteEntry::gap_in(shared), expand: regions.expand.x, }, - line_numbers: root.then(|| LineNumberConfig { + line_numbers: (mode == FlowMode::Root).then(|| LineNumberConfig { scope: ParLine::numbering_scope_in(shared), default_clearance: { let width = if PageElem::flipped_in(shared) { @@ -225,6 +247,7 @@ pub(crate) fn layout_flow( locator.next(&()), Size::new(config.columns.width, regions.full), regions.expand.x, + mode, )?; let mut work = Work::new(&children); @@ -318,7 +341,7 @@ impl<'a, 'b> Work<'a, 'b> { struct Config<'x> { /// Whether this is the root flow, which can host footnotes and line /// numbers. - root: bool, + mode: FlowMode, /// The styles shared by the whole flow. This is used for footnotes and line /// numbers. shared: StyleChain<'x>, diff --git a/crates/typst-layout/src/grid/layouter.rs b/crates/typst-layout/src/grid/layouter.rs index 7c94617dc..1f9cf6796 100644 --- a/crates/typst-layout/src/grid/layouter.rs +++ b/crates/typst-layout/src/grid/layouter.rs @@ -3,6 +3,7 @@ use std::fmt::Debug; use typst_library::diag::{bail, SourceResult}; use typst_library::engine::Engine; use typst_library::foundations::{Resolve, StyleChain}; +use typst_library::layout::grid::resolve::{Cell, CellGrid, LinePosition, Repeatable}; use typst_library::layout::{ Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel, Size, Sizing, @@ -13,8 +14,8 @@ use typst_syntax::Span; use typst_utils::{MaybeReverseIter, Numeric}; use super::{ - generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Cell, CellGrid, - LinePosition, LineSegment, Repeatable, Rowspan, UnbreakableRowGroup, + generate_line_segments, hline_stroke_at_column, layout_cell, vline_stroke_at_row, + LineSegment, Rowspan, UnbreakableRowGroup, }; /// Performs grid layout. @@ -843,7 +844,8 @@ impl<'a> GridLayouter<'a> { let size = Size::new(available, height); let pod = Region::new(size, Axes::splat(false)); - let frame = cell.layout(engine, 0, self.styles, pod.into())?.into_frame(); + let frame = + layout_cell(cell, engine, 0, self.styles, pod.into())?.into_frame(); resolved.set_max(frame.width() - already_covered_width); } @@ -1086,7 +1088,7 @@ impl<'a> GridLayouter<'a> { }; let frames = - cell.layout(engine, disambiguator, self.styles, pod)?.into_frames(); + layout_cell(cell, engine, disambiguator, self.styles, pod)?.into_frames(); // Skip the first region if one cell in it is empty. Then, // remeasure. @@ -1252,9 +1254,9 @@ impl<'a> GridLayouter<'a> { // rows. pod.full = self.regions.full; } - let frame = cell - .layout(engine, disambiguator, self.styles, pod)? - .into_frame(); + let frame = + layout_cell(cell, engine, disambiguator, self.styles, pod)? + .into_frame(); let mut pos = pos; if self.is_rtl { // In the grid, cell colspans expand to the right, @@ -1310,7 +1312,7 @@ impl<'a> GridLayouter<'a> { // Push the layouted frames into the individual output frames. let fragment = - cell.layout(engine, disambiguator, self.styles, pod)?; + layout_cell(cell, engine, disambiguator, self.styles, pod)?; for (output, frame) in outputs.iter_mut().zip(fragment) { let mut pos = pos; if self.is_rtl { diff --git a/crates/typst-layout/src/grid/lines.rs b/crates/typst-layout/src/grid/lines.rs index 3e89612a1..1227953d1 100644 --- a/crates/typst-layout/src/grid/lines.rs +++ b/crates/typst-layout/src/grid/lines.rs @@ -1,41 +1,11 @@ -use std::num::NonZeroUsize; use std::sync::Arc; use typst_library::foundations::{AlternativeFold, Fold}; +use typst_library::layout::grid::resolve::{CellGrid, Line, Repeatable}; use typst_library::layout::Abs; use typst_library::visualize::Stroke; -use super::{CellGrid, LinePosition, Repeatable, RowPiece}; - -/// Represents an explicit grid line (horizontal or vertical) specified by the -/// user. -pub struct Line { - /// The index of the track after this line. This will be the index of the - /// row a horizontal line is above of, or of the column right after a - /// vertical line. - /// - /// Must be within `0..=tracks.len()` (where `tracks` is either `grid.cols` - /// or `grid.rows`, ignoring gutter tracks, as appropriate). - pub index: usize, - /// The index of the track at which this line starts being drawn. - /// This is the first column a horizontal line appears in, or the first row - /// a vertical line appears in. - /// - /// Must be within `0..tracks.len()` minus gutter tracks. - pub start: usize, - /// The index after the last track through which the line is drawn. - /// Thus, the line is drawn through tracks `start..end` (note that `end` is - /// exclusive). - /// - /// Must be within `1..=tracks.len()` minus gutter tracks. - /// `None` indicates the line should go all the way to the end. - pub end: Option, - /// The line's stroke. This is `None` when the line is explicitly used to - /// override a previously specified line. - pub stroke: Option>>, - /// The line's position in relation to the track with its index. - pub position: LinePosition, -} +use super::RowPiece; /// Indicates which priority a particular grid line segment should have, based /// on the highest priority configuration that defined the segment's stroke. @@ -588,13 +558,13 @@ pub fn hline_stroke_at_column( #[cfg(test)] mod test { + use std::num::NonZeroUsize; use typst_library::foundations::Content; use typst_library::introspection::Locator; + use typst_library::layout::grid::resolve::{Cell, Entry, LinePosition}; use typst_library::layout::{Axes, Sides, Sizing}; use typst_utils::NonZeroExt; - use super::super::cells::Entry; - use super::super::Cell; use super::*; fn sample_cell() -> Cell<'static> { diff --git a/crates/typst-layout/src/grid/mod.rs b/crates/typst-layout/src/grid/mod.rs index 769bef8c5..1b4380f0a 100644 --- a/crates/typst-layout/src/grid/mod.rs +++ b/crates/typst-layout/src/grid/mod.rs @@ -1,40 +1,44 @@ -mod cells; mod layouter; mod lines; mod repeated; mod rowspans; -pub use self::cells::{Cell, CellGrid}; pub use self::layouter::GridLayouter; -use std::num::NonZeroUsize; -use std::sync::Arc; - -use ecow::eco_format; -use typst_library::diag::{SourceResult, Trace, Tracepoint}; +use typst_library::diag::SourceResult; use typst_library::engine::Engine; -use typst_library::foundations::{Fold, Packed, Smart, StyleChain}; +use typst_library::foundations::{Packed, StyleChain}; use typst_library::introspection::Locator; -use typst_library::layout::{ - Abs, Alignment, Axes, Dir, Fragment, GridCell, GridChild, GridElem, GridItem, Length, - OuterHAlignment, OuterVAlignment, Regions, Rel, Sides, -}; -use typst_library::model::{TableCell, TableChild, TableElem, TableItem}; -use typst_library::text::TextElem; -use typst_library::visualize::{Paint, Stroke}; -use typst_syntax::Span; +use typst_library::layout::grid::resolve::{grid_to_cellgrid, table_to_cellgrid, Cell}; +use typst_library::layout::{Fragment, GridElem, Regions}; +use typst_library::model::TableElem; -use self::cells::{ - LinePosition, ResolvableCell, ResolvableGridChild, ResolvableGridItem, -}; use self::layouter::RowPiece; use self::lines::{ - generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, Line, - LineSegment, + generate_line_segments, hline_stroke_at_column, vline_stroke_at_row, LineSegment, }; -use self::repeated::{Footer, Header, Repeatable}; use self::rowspans::{Rowspan, UnbreakableRowGroup}; +/// Layout the cell into the given regions. +/// +/// The `disambiguator` indicates which instance of this cell this should be +/// layouted as. For normal cells, it is always `0`, but for headers and +/// footers, it indicates the index of the header/footer among all. See the +/// [`Locator`] docs for more details on the concepts behind this. +pub fn layout_cell( + cell: &Cell, + engine: &mut Engine, + disambiguator: usize, + styles: StyleChain, + regions: Regions, +) -> SourceResult { + let mut locator = cell.locator.relayout(); + if disambiguator > 0 { + locator = locator.split().next_inner(disambiguator as u128); + } + crate::layout_fragment(engine, &cell.body, locator, styles, regions) +} + /// Layout the grid. #[typst_macros::time(span = elem.span())] pub fn layout_grid( @@ -44,54 +48,8 @@ pub fn layout_grid( styles: StyleChain, regions: Regions, ) -> SourceResult { - let inset = elem.inset(styles); - let align = elem.align(styles); - let columns = elem.columns(styles); - let rows = elem.rows(styles); - let column_gutter = elem.column_gutter(styles); - let row_gutter = elem.row_gutter(styles); - let fill = elem.fill(styles); - let stroke = elem.stroke(styles); - - let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); - let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice()); - // Use trace to link back to the grid when a specific cell errors - let tracepoint = || Tracepoint::Call(Some(eco_format!("grid"))); - let resolve_item = |item: &GridItem| grid_item_to_resolvable(item, styles); - let children = elem.children().iter().map(|child| match child { - GridChild::Header(header) => ResolvableGridChild::Header { - repeat: header.repeat(styles), - span: header.span(), - items: header.children().iter().map(resolve_item), - }, - GridChild::Footer(footer) => ResolvableGridChild::Footer { - repeat: footer.repeat(styles), - span: footer.span(), - items: footer.children().iter().map(resolve_item), - }, - GridChild::Item(item) => { - ResolvableGridChild::Item(grid_item_to_resolvable(item, styles)) - } - }); - let grid = CellGrid::resolve( - tracks, - gutter, - locator, - children, - fill, - align, - &inset, - &stroke, - engine, - styles, - elem.span(), - ) - .trace(engine.world, tracepoint, elem.span())?; - - let layouter = GridLayouter::new(&grid, regions, styles, elem.span()); - - // Measure the columns and layout the grid row-by-row. - layouter.layout(engine) + let grid = grid_to_cellgrid(elem, engine, locator, styles)?; + GridLayouter::new(&grid, regions, styles, elem.span()).layout(engine) } /// Layout the table. @@ -103,314 +61,6 @@ pub fn layout_table( styles: StyleChain, regions: Regions, ) -> SourceResult { - let inset = elem.inset(styles); - let align = elem.align(styles); - let columns = elem.columns(styles); - let rows = elem.rows(styles); - let column_gutter = elem.column_gutter(styles); - let row_gutter = elem.row_gutter(styles); - let fill = elem.fill(styles); - let stroke = elem.stroke(styles); - - let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); - let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice()); - // Use trace to link back to the table when a specific cell errors - let tracepoint = || Tracepoint::Call(Some(eco_format!("table"))); - let resolve_item = |item: &TableItem| table_item_to_resolvable(item, styles); - let children = elem.children().iter().map(|child| match child { - TableChild::Header(header) => ResolvableGridChild::Header { - repeat: header.repeat(styles), - span: header.span(), - items: header.children().iter().map(resolve_item), - }, - TableChild::Footer(footer) => ResolvableGridChild::Footer { - repeat: footer.repeat(styles), - span: footer.span(), - items: footer.children().iter().map(resolve_item), - }, - TableChild::Item(item) => { - ResolvableGridChild::Item(table_item_to_resolvable(item, styles)) - } - }); - let grid = CellGrid::resolve( - tracks, - gutter, - locator, - children, - fill, - align, - &inset, - &stroke, - engine, - styles, - elem.span(), - ) - .trace(engine.world, tracepoint, elem.span())?; - - let layouter = GridLayouter::new(&grid, regions, styles, elem.span()); - layouter.layout(engine) -} - -fn grid_item_to_resolvable( - item: &GridItem, - styles: StyleChain, -) -> ResolvableGridItem> { - match item { - GridItem::HLine(hline) => ResolvableGridItem::HLine { - y: hline.y(styles), - start: hline.start(styles), - end: hline.end(styles), - stroke: hline.stroke(styles), - span: hline.span(), - position: match hline.position(styles) { - OuterVAlignment::Top => LinePosition::Before, - OuterVAlignment::Bottom => LinePosition::After, - }, - }, - GridItem::VLine(vline) => ResolvableGridItem::VLine { - x: vline.x(styles), - start: vline.start(styles), - end: vline.end(styles), - stroke: vline.stroke(styles), - span: vline.span(), - position: match vline.position(styles) { - OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => { - LinePosition::After - } - OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => { - LinePosition::Before - } - OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before, - OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After, - }, - }, - GridItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()), - } -} - -fn table_item_to_resolvable( - item: &TableItem, - styles: StyleChain, -) -> ResolvableGridItem> { - match item { - TableItem::HLine(hline) => ResolvableGridItem::HLine { - y: hline.y(styles), - start: hline.start(styles), - end: hline.end(styles), - stroke: hline.stroke(styles), - span: hline.span(), - position: match hline.position(styles) { - OuterVAlignment::Top => LinePosition::Before, - OuterVAlignment::Bottom => LinePosition::After, - }, - }, - TableItem::VLine(vline) => ResolvableGridItem::VLine { - x: vline.x(styles), - start: vline.start(styles), - end: vline.end(styles), - stroke: vline.stroke(styles), - span: vline.span(), - position: match vline.position(styles) { - OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => { - LinePosition::After - } - OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => { - LinePosition::Before - } - OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before, - OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After, - }, - }, - TableItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()), - } -} - -impl ResolvableCell for Packed { - fn resolve_cell<'a>( - mut self, - x: usize, - y: usize, - fill: &Option, - align: Smart, - inset: Sides>>, - stroke: Sides>>>>, - breakable: bool, - locator: Locator<'a>, - styles: StyleChain, - ) -> Cell<'a> { - let cell = &mut *self; - let colspan = cell.colspan(styles); - let rowspan = cell.rowspan(styles); - let breakable = cell.breakable(styles).unwrap_or(breakable); - let fill = cell.fill(styles).unwrap_or_else(|| fill.clone()); - - let cell_stroke = cell.stroke(styles); - let stroke_overridden = - cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_)))); - - // Using a typical 'Sides' fold, an unspecified side loses to a - // specified side. Additionally, when both are specified, an inner - // None wins over the outer Some, and vice-versa. When both are - // specified and Some, fold occurs, which, remarkably, leads to an Arc - // clone. - // - // In the end, we flatten because, for layout purposes, an unspecified - // cell stroke is the same as specifying 'none', so we equate the two - // concepts. - let stroke = cell_stroke.fold(stroke).map(Option::flatten); - cell.push_x(Smart::Custom(x)); - cell.push_y(Smart::Custom(y)); - cell.push_fill(Smart::Custom(fill.clone())); - cell.push_align(match align { - Smart::Custom(align) => { - Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align))) - } - // Don't fold if the table is using outer alignment. Use the - // cell's alignment instead (which, in the end, will fold with - // the outer alignment when it is effectively displayed). - Smart::Auto => cell.align(styles), - }); - cell.push_inset(Smart::Custom( - cell.inset(styles).map_or(inset, |inner| inner.fold(inset)), - )); - cell.push_stroke( - // Here we convert the resolved stroke to a regular stroke, however - // with resolved units (that is, 'em' converted to absolute units). - // We also convert any stroke unspecified by both the cell and the - // outer stroke ('None' in the folded stroke) to 'none', that is, - // all sides are present in the resulting Sides object accessible - // by show rules on table cells. - stroke.as_ref().map(|side| { - Some(side.as_ref().map(|cell_stroke| { - Arc::new((**cell_stroke).clone().map(Length::from)) - })) - }), - ); - cell.push_breakable(Smart::Custom(breakable)); - Cell { - body: self.pack(), - locator, - fill, - colspan, - rowspan, - stroke, - stroke_overridden, - breakable, - } - } - - fn x(&self, styles: StyleChain) -> Smart { - (**self).x(styles) - } - - fn y(&self, styles: StyleChain) -> Smart { - (**self).y(styles) - } - - fn colspan(&self, styles: StyleChain) -> NonZeroUsize { - (**self).colspan(styles) - } - - fn rowspan(&self, styles: StyleChain) -> NonZeroUsize { - (**self).rowspan(styles) - } - - fn span(&self) -> Span { - Packed::span(self) - } -} - -impl ResolvableCell for Packed { - fn resolve_cell<'a>( - mut self, - x: usize, - y: usize, - fill: &Option, - align: Smart, - inset: Sides>>, - stroke: Sides>>>>, - breakable: bool, - locator: Locator<'a>, - styles: StyleChain, - ) -> Cell<'a> { - let cell = &mut *self; - let colspan = cell.colspan(styles); - let rowspan = cell.rowspan(styles); - let breakable = cell.breakable(styles).unwrap_or(breakable); - let fill = cell.fill(styles).unwrap_or_else(|| fill.clone()); - - let cell_stroke = cell.stroke(styles); - let stroke_overridden = - cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_)))); - - // Using a typical 'Sides' fold, an unspecified side loses to a - // specified side. Additionally, when both are specified, an inner - // None wins over the outer Some, and vice-versa. When both are - // specified and Some, fold occurs, which, remarkably, leads to an Arc - // clone. - // - // In the end, we flatten because, for layout purposes, an unspecified - // cell stroke is the same as specifying 'none', so we equate the two - // concepts. - let stroke = cell_stroke.fold(stroke).map(Option::flatten); - cell.push_x(Smart::Custom(x)); - cell.push_y(Smart::Custom(y)); - cell.push_fill(Smart::Custom(fill.clone())); - cell.push_align(match align { - Smart::Custom(align) => { - Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align))) - } - // Don't fold if the grid is using outer alignment. Use the - // cell's alignment instead (which, in the end, will fold with - // the outer alignment when it is effectively displayed). - Smart::Auto => cell.align(styles), - }); - cell.push_inset(Smart::Custom( - cell.inset(styles).map_or(inset, |inner| inner.fold(inset)), - )); - cell.push_stroke( - // Here we convert the resolved stroke to a regular stroke, however - // with resolved units (that is, 'em' converted to absolute units). - // We also convert any stroke unspecified by both the cell and the - // outer stroke ('None' in the folded stroke) to 'none', that is, - // all sides are present in the resulting Sides object accessible - // by show rules on grid cells. - stroke.as_ref().map(|side| { - Some(side.as_ref().map(|cell_stroke| { - Arc::new((**cell_stroke).clone().map(Length::from)) - })) - }), - ); - cell.push_breakable(Smart::Custom(breakable)); - Cell { - body: self.pack(), - locator, - fill, - colspan, - rowspan, - stroke, - stroke_overridden, - breakable, - } - } - - fn x(&self, styles: StyleChain) -> Smart { - (**self).x(styles) - } - - fn y(&self, styles: StyleChain) -> Smart { - (**self).y(styles) - } - - fn colspan(&self, styles: StyleChain) -> NonZeroUsize { - (**self).colspan(styles) - } - - fn rowspan(&self, styles: StyleChain) -> NonZeroUsize { - (**self).rowspan(styles) - } - - fn span(&self) -> Span { - Packed::span(self) - } + let grid = table_to_cellgrid(elem, engine, locator, styles)?; + GridLayouter::new(&grid, regions, styles, elem.span()).layout(engine) } diff --git a/crates/typst-layout/src/grid/repeated.rs b/crates/typst-layout/src/grid/repeated.rs index 8d08d56db..22d2a09ef 100644 --- a/crates/typst-layout/src/grid/repeated.rs +++ b/crates/typst-layout/src/grid/repeated.rs @@ -1,50 +1,11 @@ use typst_library::diag::SourceResult; use typst_library::engine::Engine; +use typst_library::layout::grid::resolve::{Footer, Header, Repeatable}; use typst_library::layout::{Abs, Axes, Frame, Regions}; use super::layouter::GridLayouter; use super::rowspans::UnbreakableRowGroup; -/// A repeatable grid header. Starts at the first row. -pub struct Header { - /// The index after the last row included in this header. - pub end: usize, -} - -/// A repeatable grid footer. Stops at the last row. -pub struct Footer { - /// The first row included in this footer. - pub start: usize, -} - -/// A possibly repeatable grid object. -/// It still exists even when not repeatable, but must not have additional -/// considerations by grid layout, other than for consistency (such as making -/// a certain group of rows unbreakable). -pub enum Repeatable { - Repeated(T), - NotRepeated(T), -} - -impl Repeatable { - /// Gets the value inside this repeatable, regardless of whether - /// it repeats. - pub fn unwrap(&self) -> &T { - match self { - Self::Repeated(repeated) => repeated, - Self::NotRepeated(not_repeated) => not_repeated, - } - } - - /// Returns `Some` if the value is repeated, `None` otherwise. - pub fn as_repeated(&self) -> Option<&T> { - match self { - Self::Repeated(repeated) => Some(repeated), - Self::NotRepeated(_) => None, - } - } -} - impl GridLayouter<'_> { /// Layouts the header's rows. /// Skips regions as necessary. diff --git a/crates/typst-layout/src/grid/rowspans.rs b/crates/typst-layout/src/grid/rowspans.rs index 93d4c960d..5039695d8 100644 --- a/crates/typst-layout/src/grid/rowspans.rs +++ b/crates/typst-layout/src/grid/rowspans.rs @@ -1,12 +1,12 @@ use typst_library::diag::SourceResult; use typst_library::engine::Engine; use typst_library::foundations::Resolve; +use typst_library::layout::grid::resolve::Repeatable; use typst_library::layout::{Abs, Axes, Frame, Point, Region, Regions, Size, Sizing}; use typst_utils::MaybeReverseIter; use super::layouter::{in_last_with_offset, points, Row, RowPiece}; -use super::repeated::Repeatable; -use super::{Cell, GridLayouter}; +use super::{layout_cell, Cell, GridLayouter}; /// All information needed to layout a single rowspan. pub struct Rowspan { @@ -141,7 +141,7 @@ impl GridLayouter<'_> { } // Push the layouted frames directly into the finished frames. - let fragment = cell.layout(engine, disambiguator, self.styles, pod)?; + let fragment = layout_cell(cell, engine, disambiguator, self.styles, pod)?; let (current_region, current_rrows) = current_region_data.unzip(); for ((i, finished), frame) in self .finished diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index 6a64371e3..69a72e750 100644 --- a/crates/typst-layout/src/image.rs +++ b/crates/typst-layout/src/image.rs @@ -2,12 +2,12 @@ use std::ffi::OsStr; use typst_library::diag::{bail, warning, At, SourceResult, StrResult}; use typst_library::engine::Engine; -use typst_library::foundations::{Packed, Smart, StyleChain}; +use typst_library::foundations::{Bytes, Derived, Packed, Smart, StyleChain}; use typst_library::introspection::Locator; use typst_library::layout::{ Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size, }; -use typst_library::loading::Readable; +use typst_library::loading::DataSource; use typst_library::text::families; use typst_library::visualize::{ Curve, Image, ImageElem, ImageFit, ImageFormat, ImageOptions, ImageSource, @@ -27,7 +27,7 @@ pub fn layout_image( // Take the format that was explicitly defined, or parse the extension, // or try to detect the format. - let source = elem.source(); + let Derived { source, derived: data } = &elem.source; let format = match (elem.format(styles), source) { (Smart::Custom(v), _) => v, (Smart::Auto, ImageSource::Readable(data)) => { @@ -130,25 +130,23 @@ pub fn layout_image( Ok(frame) } -/// Determine the image format based on path and data. -fn determine_format(path: &str, data: &Readable) -> StrResult { - let ext = std::path::Path::new(path) - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default() - .to_lowercase(); +/// 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(); - Ok(match ext.as_str() { - "png" => ImageFormat::Raster(RasterFormat::Png), - "jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg), - "gif" => ImageFormat::Raster(RasterFormat::Gif), - "svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg), - _ => match &data { - Readable::Str(_) => ImageFormat::Vector(VectorFormat::Svg), - Readable::Bytes(bytes) => match RasterFormat::detect(bytes) { - Some(f) => ImageFormat::Raster(f), - None => bail!("unknown image format"), - }, - }, - }) + match ext.as_str() { + "png" => return Ok(ImageFormat::Raster(RasterFormat::Png)), + "jpg" | "jpeg" => return Ok(ImageFormat::Raster(RasterFormat::Jpg)), + "gif" => return Ok(ImageFormat::Raster(RasterFormat::Gif)), + "svg" | "svgz" => return Ok(ImageFormat::Vector(VectorFormat::Svg)), + _ => {} + } + } + + Ok(ImageFormat::detect(data).ok_or("unknown image format")?) } diff --git a/crates/typst-layout/src/inline/box.rs b/crates/typst-layout/src/inline/box.rs index 6dfbc9696..e21928d3c 100644 --- a/crates/typst-layout/src/inline/box.rs +++ b/crates/typst-layout/src/inline/box.rs @@ -11,7 +11,7 @@ use typst_utils::Numeric; use crate::flow::unbreakable_pod; use crate::shapes::{clip_rect, fill_and_stroke}; -/// Lay out a box as part of a paragraph. +/// Lay out a box as part of inline layout. #[typst_macros::time(name = "box", span = elem.span())] pub fn layout_box( elem: &Packed, diff --git a/crates/typst-layout/src/inline/collect.rs b/crates/typst-layout/src/inline/collect.rs index 23e82c417..14cf2e3b8 100644 --- a/crates/typst-layout/src/inline/collect.rs +++ b/crates/typst-layout/src/inline/collect.rs @@ -1,10 +1,12 @@ -use typst_library::diag::bail; +use typst_library::diag::warning; use typst_library::foundations::{Packed, Resolve}; use typst_library::introspection::{SplitLocator, Tag, TagElem}; use typst_library::layout::{ Abs, AlignElem, BoxElem, Dir, Fr, Frame, HElem, InlineElem, InlineItem, Sizing, Spacing, }; +use typst_library::model::{EnumElem, ListElem, TermsElem}; +use typst_library::routines::Pair; use typst_library::text::{ is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes, SpaceElem, TextElem, @@ -13,9 +15,10 @@ use typst_syntax::Span; use typst_utils::Numeric; use super::*; +use crate::modifiers::{layout_and_modify, FrameModifiers, FrameModify}; // The characters by which spacing, inline content and pins are replaced in the -// paragraph's full text. +// full text. const SPACING_REPLACE: &str = " "; // Space const OBJ_REPLACE: &str = "\u{FFFC}"; // Object Replacement Character @@ -26,7 +29,7 @@ const POP_EMBEDDING: &str = "\u{202C}"; const LTR_ISOLATE: &str = "\u{2066}"; const POP_ISOLATE: &str = "\u{2069}"; -/// A prepared item in a paragraph layout. +/// A prepared item in a inline layout. #[derive(Debug)] pub enum Item<'a> { /// A shaped text run with consistent style and direction. @@ -36,7 +39,7 @@ pub enum Item<'a> { /// Fractional spacing between other items. Fractional(Fr, Option<(&'a Packed, Locator<'a>, StyleChain<'a>)>), /// Layouted inline-level content. - Frame(Frame, StyleChain<'a>), + Frame(Frame), /// A tag. Tag(&'a Tag), /// An item that is invisible and needs to be skipped, e.g. a Unicode @@ -67,7 +70,7 @@ impl<'a> Item<'a> { match self { Self::Text(shaped) => shaped.text, Self::Absolute(_, _) | Self::Fractional(_, _) => SPACING_REPLACE, - Self::Frame(_, _) => OBJ_REPLACE, + Self::Frame(_) => OBJ_REPLACE, Self::Tag(_) => "", Self::Skip(s) => s, } @@ -83,7 +86,7 @@ impl<'a> Item<'a> { match self { Self::Text(shaped) => shaped.width, Self::Absolute(v, _) => *v, - Self::Frame(frame, _) => frame.width(), + Self::Frame(frame) => frame.width(), Self::Fractional(_, _) | Self::Tag(_) => Abs::zero(), Self::Skip(_) => Abs::zero(), } @@ -112,38 +115,51 @@ impl Segment<'_> { } } -/// Collects all text of the paragraph into one string and a collection of -/// segments that correspond to pieces of that string. This also performs -/// string-level preprocessing like case transformations. +/// Collects all text into one string and a collection of segments that +/// correspond to pieces of that string. This also performs string-level +/// preprocessing like case transformations. #[typst_macros::time] pub fn collect<'a>( - children: &'a StyleVec, + children: &[Pair<'a>], engine: &mut Engine<'_>, locator: &mut SplitLocator<'a>, - styles: &'a StyleChain<'a>, + styles: StyleChain<'a>, region: Size, - consecutive: bool, + situation: Option, ) -> SourceResult<(String, Vec>, SpanMapper)> { let mut collector = Collector::new(2 + children.len()); let mut quoter = SmartQuoter::new(); - let outer_dir = TextElem::dir_in(*styles); - let first_line_indent = ParElem::first_line_indent_in(*styles); - if !first_line_indent.is_zero() - && consecutive - && AlignElem::alignment_in(*styles).resolve(*styles).x == outer_dir.start().into() - { - collector.push_item(Item::Absolute(first_line_indent.resolve(*styles), false)); - collector.spans.push(1, Span::detached()); + let outer_dir = TextElem::dir_in(styles); + + if let Some(situation) = situation { + let first_line_indent = ParElem::first_line_indent_in(styles); + if !first_line_indent.amount.is_zero() + && match situation { + // First-line indent for the first paragraph after a list bullet + // just looks bad. + ParSituation::First => first_line_indent.all && !in_list(styles), + ParSituation::Consecutive => true, + ParSituation::Other => first_line_indent.all, + } + && AlignElem::alignment_in(styles).resolve(styles).x + == outer_dir.start().into() + { + collector.push_item(Item::Absolute( + first_line_indent.amount.resolve(styles), + false, + )); + collector.spans.push(1, Span::detached()); + } + + let hang = ParElem::hanging_indent_in(styles); + if !hang.is_zero() { + collector.push_item(Item::Absolute(-hang, false)); + collector.spans.push(1, Span::detached()); + } } - let hang = ParElem::hanging_indent_in(*styles); - if !hang.is_zero() { - collector.push_item(Item::Absolute(-hang, false)); - collector.spans.push(1, Span::detached()); - } - - for (child, styles) in children.iter(styles) { + for &(child, styles) in children { let prev_len = collector.full.len(); if child.is::() { @@ -161,9 +177,9 @@ pub fn collect<'a>( } if let Some(case) = TextElem::case_in(styles) { - full.push_str(&case.apply(elem.text())); + full.push_str(&case.apply(&elem.text)); } else { - full.push_str(elem.text()); + full.push_str(&elem.text); } if dir != outer_dir { @@ -172,13 +188,12 @@ pub fn collect<'a>( } }); } else if let Some(elem) = child.to_packed::() { - let amount = elem.amount(); - if amount.is_zero() { + if elem.amount.is_zero() { continue; } - collector.push_item(match amount { - Spacing::Fr(fr) => Item::Fractional(*fr, None), + collector.push_item(match elem.amount { + Spacing::Fr(fr) => Item::Fractional(fr, None), Spacing::Rel(rel) => Item::Absolute( rel.resolve(styles).relative_to(region.x), elem.weak(styles), @@ -211,8 +226,10 @@ pub fn collect<'a>( InlineItem::Space(space, weak) => { collector.push_item(Item::Absolute(space, weak)); } - InlineItem::Frame(frame) => { - collector.push_item(Item::Frame(frame, styles)); + InlineItem::Frame(mut frame) => { + frame.modify(&FrameModifiers::get_in(styles)); + apply_baseline_shift(&mut frame, styles); + collector.push_item(Item::Frame(frame)); } } } @@ -223,13 +240,22 @@ pub fn collect<'a>( if let Sizing::Fr(v) = elem.width(styles) { collector.push_item(Item::Fractional(v, Some((elem, loc, styles)))); } else { - let frame = layout_box(elem, engine, loc, styles, region)?; - collector.push_item(Item::Frame(frame, styles)); + let mut frame = layout_and_modify(styles, |styles| { + layout_box(elem, engine, loc, styles, region) + })?; + apply_baseline_shift(&mut frame, styles); + collector.push_item(Item::Frame(frame)); } } else if let Some(elem) = child.to_packed::() { collector.push_item(Item::Tag(&elem.tag)); } else { - bail!(child.span(), "unexpected paragraph child"); + // Non-paragraph inline layout should never trigger this since it + // only won't be triggered if we see any non-inline content. + engine.sink.warn(warning!( + child.span(), + "{} may not occur inside of a paragraph and was ignored", + child.func().name() + )); }; let len = collector.full.len() - prev_len; @@ -239,6 +265,16 @@ pub fn collect<'a>( Ok((collector.full, collector.segments, collector.spans)) } +/// Whether we have a list ancestor. +/// +/// When we support some kind of more general ancestry mechanism, this can +/// become more elegant. +fn in_list(styles: StyleChain) -> bool { + ListElem::depth_in(styles).0 > 0 + || !EnumElem::parents_in(styles).is_empty() + || TermsElem::within_in(styles) +} + /// Collects segments. struct Collector<'a> { full: String, diff --git a/crates/typst-layout/src/inline/finalize.rs b/crates/typst-layout/src/inline/finalize.rs index 57044f0ec..7ad287c45 100644 --- a/crates/typst-layout/src/inline/finalize.rs +++ b/crates/typst-layout/src/inline/finalize.rs @@ -14,7 +14,7 @@ pub fn finalize( expand: bool, locator: &mut SplitLocator<'_>, ) -> SourceResult { - // Determine the paragraph's width: Full width of the region if we should + // Determine the resulting width: Full width of the region if we should // expand or there's fractional spacing, fit-to-width otherwise. let width = if !region.x.is_finite() || (!expand && lines.iter().all(|line| line.fr().is_zero())) diff --git a/crates/typst-layout/src/inline/line.rs b/crates/typst-layout/src/inline/line.rs index ef7e26c3c..9f6973807 100644 --- a/crates/typst-layout/src/inline/line.rs +++ b/crates/typst-layout/src/inline/line.rs @@ -10,6 +10,7 @@ use typst_library::text::{Lang, TextElem}; use typst_utils::Numeric; use super::*; +use crate::modifiers::layout_and_modify; const SHY: char = '\u{ad}'; const HYPHEN: char = '-'; @@ -17,12 +18,12 @@ const EN_DASH: char = '–'; const EM_DASH: char = '—'; const LINE_SEPARATOR: char = '\u{2028}'; // We use LS to distinguish justified breaks. -/// A layouted line, consisting of a sequence of layouted paragraph items that -/// are mostly borrowed from the preparation phase. This type enables you to -/// measure the size of a line in a range before committing to building the -/// line's frame. +/// A layouted line, consisting of a sequence of layouted inline items that are +/// mostly borrowed from the preparation phase. This type enables you to measure +/// the size of a line in a range before committing to building the line's +/// frame. /// -/// At most two paragraph items must be created individually for this line: The +/// At most two inline items must be created individually for this line: The /// first and last one since they may be broken apart by the start or end of the /// line, respectively. But even those can partially reuse previous results when /// the break index is safe-to-break per rustybuzz. @@ -93,7 +94,7 @@ impl Line<'_> { pub fn has_negative_width_items(&self) -> bool { self.items.iter().any(|item| match item { Item::Absolute(amount, _) => *amount < Abs::zero(), - Item::Frame(frame, _) => frame.width() < Abs::zero(), + Item::Frame(frame) => frame.width() < Abs::zero(), _ => false, }) } @@ -409,6 +410,11 @@ fn should_repeat_hyphen(pred_line: &Line, text: &str) -> bool { } } +/// Apply the current baseline shift to a frame. +pub fn apply_baseline_shift(frame: &mut Frame, styles: StyleChain) { + frame.translate(Point::with_y(TextElem::baseline_in(styles))); +} + /// Commit to a line and build its frame. #[allow(clippy::too_many_arguments)] pub fn commit( @@ -424,7 +430,7 @@ pub fn commit( let mut offset = Abs::zero(); // We always build the line from left to right. In an LTR paragraph, we must - // thus add the hanging indent to the offset. When the paragraph is RTL, the + // thus add the hanging indent to the offset. In an RTL paragraph, the // hanging indent arises naturally due to the line width. if p.dir == Dir::LTR { offset += p.hang; @@ -509,10 +515,11 @@ pub fn commit( let amount = v.share(fr, remaining); if let Some((elem, loc, styles)) = elem { let region = Size::new(amount, full); - let mut frame = - layout_box(elem, engine, loc.relayout(), *styles, region)?; - frame.translate(Point::with_y(TextElem::baseline_in(*styles))); - push(&mut offset, frame.post_processed(*styles)); + let mut frame = layout_and_modify(*styles, |styles| { + layout_box(elem, engine, loc.relayout(), styles, region) + })?; + apply_baseline_shift(&mut frame, *styles); + push(&mut offset, frame); } else { offset += amount; } @@ -524,12 +531,10 @@ pub fn commit( justification_ratio, extra_justification, ); - push(&mut offset, frame.post_processed(shaped.styles)); + push(&mut offset, frame); } - Item::Frame(frame, styles) => { - let mut frame = frame.clone(); - frame.translate(Point::with_y(TextElem::baseline_in(*styles))); - push(&mut offset, frame.post_processed(*styles)); + Item::Frame(frame) => { + push(&mut offset, frame.clone()); } Item::Tag(tag) => { let mut frame = Frame::soft(Size::zero()); @@ -626,7 +631,7 @@ fn overhang(c: char) -> f64 { } } -/// A collection of owned or borrowed paragraph items. +/// A collection of owned or borrowed inline items. pub struct Items<'a>(Vec>); impl<'a> Items<'a> { diff --git a/crates/typst-layout/src/inline/linebreak.rs b/crates/typst-layout/src/inline/linebreak.rs index 7b66fcdb4..87113c689 100644 --- a/crates/typst-layout/src/inline/linebreak.rs +++ b/crates/typst-layout/src/inline/linebreak.rs @@ -17,7 +17,7 @@ use unicode_segmentation::UnicodeSegmentation; use super::*; -/// The cost of a line or paragraph layout. +/// The cost of a line or inline layout. type Cost = f64; // Cost parameters. @@ -104,7 +104,7 @@ impl Breakpoint { } } -/// Breaks the paragraph into lines. +/// Breaks the text into lines. pub fn linebreak<'a>( engine: &Engine, p: &'a Preparation<'a>, @@ -181,13 +181,12 @@ fn linebreak_simple<'a>( /// lines with hyphens even more. /// /// To find the layout with the minimal total cost the algorithm uses dynamic -/// programming: For each possible breakpoint it determines the optimal -/// paragraph layout _up to that point_. It walks over all possible start points -/// for a line ending at that point and finds the one for which the cost of the -/// line plus the cost of the optimal paragraph up to the start point (already -/// computed and stored in dynamic programming table) is minimal. The final -/// result is simply the layout determined for the last breakpoint at the end of -/// text. +/// programming: For each possible breakpoint, it determines the optimal layout +/// _up to that point_. It walks over all possible start points for a line +/// ending at that point and finds the one for which the cost of the line plus +/// the cost of the optimal layout up to the start point (already computed and +/// stored in dynamic programming table) is minimal. The final result is simply +/// the layout determined for the last breakpoint at the end of text. #[typst_macros::time] fn linebreak_optimized<'a>( engine: &Engine, @@ -215,7 +214,7 @@ fn linebreak_optimized_bounded<'a>( metrics: &CostMetrics, upper_bound: Cost, ) -> Vec> { - /// An entry in the dynamic programming table for paragraph optimization. + /// An entry in the dynamic programming table for inline layout optimization. struct Entry<'a> { pred: usize, total: Cost, @@ -321,7 +320,7 @@ fn linebreak_optimized_bounded<'a>( // This should only happen if our bound was faulty. Which shouldn't happen! if table[idx].end != p.text.len() { #[cfg(debug_assertions)] - panic!("bounded paragraph layout is incomplete"); + panic!("bounded inline layout is incomplete"); #[cfg(not(debug_assertions))] return linebreak_optimized_bounded(engine, p, width, metrics, Cost::INFINITY); @@ -342,7 +341,7 @@ fn linebreak_optimized_bounded<'a>( /// (which is costly) to determine costs, it determines approximate costs using /// cumulative arrays. /// -/// This results in a likely good paragraph layouts, for which we then compute +/// This results in a likely good inline layouts, for which we then compute /// the exact cost. This cost is an upper bound for proper optimized /// linebreaking. We can use it to heavily prune the search space. #[typst_macros::time] @@ -355,7 +354,7 @@ fn linebreak_optimized_approximate( // Determine the cumulative estimation metrics. let estimates = Estimates::compute(p); - /// An entry in the dynamic programming table for paragraph optimization. + /// An entry in the dynamic programming table for inline layout optimization. struct Entry { pred: usize, total: Cost, @@ -862,7 +861,7 @@ struct CostMetrics { } impl CostMetrics { - /// Compute shared metrics for paragraph optimization. + /// Compute shared metrics for inline layout optimization. fn compute(p: &Preparation) -> Self { Self { // When justifying, we may stretch spaces below their natural width. diff --git a/crates/typst-layout/src/inline/mod.rs b/crates/typst-layout/src/inline/mod.rs index 658e30846..f8a36368d 100644 --- a/crates/typst-layout/src/inline/mod.rs +++ b/crates/typst-layout/src/inline/mod.rs @@ -13,17 +13,17 @@ pub use self::box_::layout_box; use comemo::{Track, Tracked, TrackedMut}; use typst_library::diag::SourceResult; use typst_library::engine::{Engine, Route, Sink, Traced}; -use typst_library::foundations::{StyleChain, StyleVec}; -use typst_library::introspection::{Introspector, Locator, LocatorLink}; +use typst_library::foundations::{Packed, StyleChain}; +use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator}; use typst_library::layout::{Fragment, Size}; use typst_library::model::ParElem; -use typst_library::routines::Routines; +use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; use typst_library::World; use self::collect::{collect, Item, Segment, SpanMapper}; use self::deco::decorate; use self::finalize::finalize; -use self::line::{commit, line, Line}; +use self::line::{apply_baseline_shift, commit, line, Line}; use self::linebreak::{linebreak, Breakpoint}; use self::prepare::{prepare, Preparation}; use self::shaping::{ @@ -34,18 +34,18 @@ use self::shaping::{ /// Range of a substring of text. type Range = std::ops::Range; -/// Layouts content inline. -pub fn layout_inline( +/// Layouts the paragraph. +pub fn layout_par( + elem: &Packed, engine: &mut Engine, - children: &StyleVec, locator: Locator, styles: StyleChain, - consecutive: bool, region: Size, expand: bool, + situation: ParSituation, ) -> SourceResult { - layout_inline_impl( - children, + layout_par_impl( + elem, engine.routines, engine.world, engine.introspector, @@ -54,17 +54,17 @@ pub fn layout_inline( engine.route.track(), locator.track(), styles, - consecutive, region, expand, + situation, ) } -/// The internal, memoized implementation of `layout_inline`. +/// The internal, memoized implementation of `layout_par`. #[comemo::memoize] #[allow(clippy::too_many_arguments)] -fn layout_inline_impl( - children: &StyleVec, +fn layout_par_impl( + elem: &Packed, routines: &Routines, world: Tracked, introspector: Tracked, @@ -73,12 +73,12 @@ fn layout_inline_impl( route: Tracked, locator: Tracked, styles: StyleChain, - consecutive: bool, region: Size, expand: bool, + situation: ParSituation, ) -> SourceResult { let link = LocatorLink::new(locator); - let locator = Locator::link(&link); + let mut locator = Locator::link(&link).split(); let mut engine = Engine { routines, world, @@ -88,18 +88,63 @@ fn layout_inline_impl( route: Route::extend(route), }; - let mut locator = locator.split(); + let arenas = Arenas::default(); + let children = (engine.routines.realize)( + RealizationKind::LayoutPar, + &mut engine, + &mut locator, + &arenas, + &elem.body, + styles, + )?; + layout_inline( + &mut engine, + &children, + &mut locator, + styles, + region, + expand, + Some(situation), + ) +} + +/// Lays out realized content with inline layout. +#[allow(clippy::too_many_arguments)] +pub fn layout_inline<'a>( + engine: &mut Engine, + children: &[Pair<'a>], + locator: &mut SplitLocator<'a>, + styles: StyleChain<'a>, + region: Size, + expand: bool, + par: Option, +) -> SourceResult { // Collect all text into one string for BiDi analysis. let (text, segments, spans) = - collect(children, &mut engine, &mut locator, &styles, region, consecutive)?; + collect(children, engine, locator, styles, region, par)?; - // Perform BiDi analysis and then prepares paragraph layout. - let p = prepare(&mut engine, children, &text, segments, spans, styles)?; + // Perform BiDi analysis and performs some preparation steps before we + // proceed to line breaking. + let p = prepare(engine, children, &text, segments, spans, styles, par)?; - // Break the paragraph into lines. - let lines = linebreak(&engine, &p, region.x - p.hang); + // Break the text into lines. + let lines = linebreak(engine, &p, region.x - p.hang); // Turn the selected lines into frames. - finalize(&mut engine, &p, &lines, styles, region, expand, &mut locator) + finalize(engine, &p, &lines, styles, region, expand, locator) +} + +/// Distinguishes between a few different kinds of paragraphs. +/// +/// In the form `Option`, `None` implies that we are creating an +/// inline layout that isn't a semantic paragraph. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum ParSituation { + /// The paragraph is the first thing in the flow. + First, + /// The paragraph follows another paragraph. + Consecutive, + /// Any other kind of paragraph. + Other, } diff --git a/crates/typst-layout/src/inline/prepare.rs b/crates/typst-layout/src/inline/prepare.rs index 2dd79aecf..0344d4331 100644 --- a/crates/typst-layout/src/inline/prepare.rs +++ b/crates/typst-layout/src/inline/prepare.rs @@ -1,23 +1,26 @@ use typst_library::foundations::{Resolve, Smart}; use typst_library::layout::{Abs, AlignElem, Dir, Em, FixedAlignment}; use typst_library::model::Linebreaks; +use typst_library::routines::Pair; use typst_library::text::{Costs, Lang, TextElem}; +use typst_utils::SliceExt; use unicode_bidi::{BidiInfo, Level as BidiLevel}; use super::*; -/// A paragraph representation in which children are already layouted and text -/// is already preshaped. +/// A representation in which children are already layouted and text is already +/// preshaped. /// /// In many cases, we can directly reuse these results when constructing a line. /// Only when a line break falls onto a text index that is not safe-to-break per /// rustybuzz, we have to reshape that portion. pub struct Preparation<'a> { - /// The paragraph's full text. + /// The full text. pub text: &'a str, - /// Bidirectional text embedding levels for the paragraph. + /// Bidirectional text embedding levels. /// - /// This is `None` if the paragraph is BiDi-uniform (all the base direction). + /// This is `None` if all text directions are uniform (all the base + /// direction). pub bidi: Option>, /// Text runs, spacing and layouted elements. pub items: Vec<(Range, Item<'a>)>, @@ -33,15 +36,15 @@ pub struct Preparation<'a> { pub dir: Dir, /// The text language if it's the same for all children. pub lang: Option, - /// The paragraph's resolved horizontal alignment. + /// The resolved horizontal alignment. pub align: FixedAlignment, - /// Whether to justify the paragraph. + /// Whether to justify text. pub justify: bool, - /// The paragraph's hanging indent. + /// Hanging indent to apply. pub hang: Abs, /// Whether to add spacing between CJK and Latin characters. pub cjk_latin_spacing: bool, - /// Whether font fallback is enabled for this paragraph. + /// Whether font fallback is enabled. pub fallback: bool, /// How to determine line breaks. pub linebreaks: Smart, @@ -71,17 +74,18 @@ impl<'a> Preparation<'a> { } } -/// Performs BiDi analysis and then prepares paragraph layout by building a +/// Performs BiDi analysis and then prepares further layout by building a /// representation on which we can do line breaking without layouting each and /// every line from scratch. #[typst_macros::time] pub fn prepare<'a>( engine: &mut Engine, - children: &'a StyleVec, + children: &[Pair<'a>], text: &'a str, segments: Vec>, spans: SpanMapper, styles: StyleChain<'a>, + situation: Option, ) -> SourceResult> { let dir = TextElem::dir_in(styles); let default_level = match dir { @@ -125,19 +129,26 @@ pub fn prepare<'a>( add_cjk_latin_spacing(&mut items); } + // Only apply hanging indent to real paragraphs. + let hang = if situation.is_some() { + ParElem::hanging_indent_in(styles) + } else { + Abs::zero() + }; + Ok(Preparation { text, bidi: is_bidi.then_some(bidi), items, indices, spans, - hyphenate: children.shared_get(styles, TextElem::hyphenate_in), + hyphenate: shared_get(children, styles, TextElem::hyphenate_in), costs: TextElem::costs_in(styles), dir, - lang: children.shared_get(styles, TextElem::lang_in), + lang: shared_get(children, styles, TextElem::lang_in), align: AlignElem::alignment_in(styles).resolve(styles).x, justify: ParElem::justify_in(styles), - hang: ParElem::hanging_indent_in(styles), + hang, cjk_latin_spacing, fallback: TextElem::fallback_in(styles), linebreaks: ParElem::linebreaks_in(styles), @@ -145,6 +156,19 @@ pub fn prepare<'a>( }) } +/// Get a style property, but only if it is the same for all of the children. +fn shared_get( + children: &[Pair], + styles: StyleChain<'_>, + getter: fn(StyleChain) -> T, +) -> Option { + let value = getter(styles); + children + .group_by_key(|&(_, s)| s) + .all(|(s, _)| getter(s) == value) + .then_some(value) +} + /// Add some spacing between Han characters and western characters. See /// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition /// in Horizontal Written Mode diff --git a/crates/typst-layout/src/inline/shaping.rs b/crates/typst-layout/src/inline/shaping.rs index d6b7632b6..b688981ae 100644 --- a/crates/typst-layout/src/inline/shaping.rs +++ b/crates/typst-layout/src/inline/shaping.rs @@ -20,6 +20,7 @@ use unicode_bidi::{BidiInfo, Level as BidiLevel}; use unicode_script::{Script, UnicodeScript}; use super::{decorate, Item, Range, SpanMapper}; +use crate::modifiers::{FrameModifiers, FrameModify}; /// The result of shaping text. /// @@ -28,7 +29,7 @@ use super::{decorate, Item, Range, SpanMapper}; /// frame. #[derive(Clone)] pub struct ShapedText<'a> { - /// The start of the text in the full paragraph. + /// The start of the text in the full text. pub base: usize, /// The text that was shaped. pub text: &'a str, @@ -65,9 +66,9 @@ pub struct ShapedGlyph { pub y_offset: Em, /// The adjustability of the glyph. pub adjustability: Adjustability, - /// The byte range of this glyph's cluster in the full paragraph. A cluster - /// is a sequence of one or multiple glyphs that cannot be separated and - /// must always be treated as a union. + /// The byte range of this glyph's cluster in the full inline layout. A + /// cluster is a sequence of one or multiple glyphs that cannot be separated + /// and must always be treated as a union. /// /// The range values of the glyphs in a [`ShapedText`] should not overlap /// with each other, and they should be monotonically increasing (for @@ -326,6 +327,7 @@ impl<'a> ShapedText<'a> { offset += width; } + frame.modify(&FrameModifiers::get_in(self.styles)); frame } @@ -403,7 +405,7 @@ impl<'a> ShapedText<'a> { /// Reshape a range of the shaped text, reusing information from this /// shaping process if possible. /// - /// The text `range` is relative to the whole paragraph. + /// The text `range` is relative to the whole inline layout. pub fn reshape(&'a self, engine: &Engine, text_range: Range) -> ShapedText<'a> { let text = &self.text[text_range.start - self.base..text_range.end - self.base]; if let Some(glyphs) = self.slice_safe_to_break(text_range.clone()) { diff --git a/crates/typst-layout/src/lib.rs b/crates/typst-layout/src/lib.rs index 2e8c1129b..443e90d61 100644 --- a/crates/typst-layout/src/lib.rs +++ b/crates/typst-layout/src/lib.rs @@ -6,6 +6,7 @@ mod image; mod inline; mod lists; mod math; +mod modifiers; mod pad; mod pages; mod repeat; @@ -16,7 +17,6 @@ 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::inline::{layout_box, layout_inline}; pub use self::lists::{layout_enum, layout_list}; pub use self::math::{layout_equation_block, layout_equation_inline}; pub use self::pad::layout_pad; diff --git a/crates/typst-layout/src/lists.rs b/crates/typst-layout/src/lists.rs index 0d51a1e4e..f8d910abf 100644 --- a/crates/typst-layout/src/lists.rs +++ b/crates/typst-layout/src/lists.rs @@ -4,11 +4,12 @@ use typst_library::diag::SourceResult; use typst_library::engine::Engine; use typst_library::foundations::{Content, Context, Depth, Packed, StyleChain}; use typst_library::introspection::Locator; +use typst_library::layout::grid::resolve::{Cell, CellGrid}; use typst_library::layout::{Axes, Fragment, HAlignment, Regions, Sizing, VAlignment}; -use typst_library::model::{EnumElem, ListElem, Numbering, ParElem}; +use typst_library::model::{EnumElem, ListElem, Numbering, ParElem, ParbreakElem}; use typst_library::text::TextElem; -use crate::grid::{Cell, CellGrid, GridLayouter}; +use crate::grid::GridLayouter; /// Layout the list. #[typst_macros::time(span = elem.span())] @@ -21,8 +22,9 @@ pub fn layout_list( ) -> SourceResult { let indent = elem.indent(styles); let body_indent = elem.body_indent(styles); + let tight = elem.tight(styles); let gutter = elem.spacing(styles).unwrap_or_else(|| { - if elem.tight(styles) { + if tight { ParElem::leading_in(styles).into() } else { ParElem::spacing_in(styles).into() @@ -39,12 +41,18 @@ pub fn layout_list( let mut cells = vec![]; let mut locator = locator.split(); - for item in elem.children() { + for item in &elem.children { + // Text in wide lists shall always turn into paragraphs. + let mut body = item.body.clone(); + if !tight { + body += ParbreakElem::shared(); + } + cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new(marker.clone(), locator.next(&marker.span()))); cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new( - item.body.clone().styled(ListElem::set_depth(Depth(1))), + body.styled(ListElem::set_depth(Depth(1))), locator.next(&item.body.span()), )); } @@ -77,8 +85,9 @@ pub fn layout_enum( let reversed = elem.reversed(styles); let indent = elem.indent(styles); let body_indent = elem.body_indent(styles); + let tight = elem.tight(styles); let gutter = elem.spacing(styles).unwrap_or_else(|| { - if elem.tight(styles) { + if tight { ParElem::leading_in(styles).into() } else { ParElem::spacing_in(styles).into() @@ -100,7 +109,7 @@ pub fn layout_enum( // relation to the item it refers to. let number_align = elem.number_align(styles); - for item in elem.children() { + for item in &elem.children { number = item.number(styles).unwrap_or(number); let context = Context::new(None, Some(styles)); @@ -123,11 +132,17 @@ pub fn layout_enum( let resolved = resolved.aligned(number_align).styled(TextElem::set_overhang(false)); + // Text in wide enums shall always turn into paragraphs. + let mut body = item.body.clone(); + if !tight { + body += ParbreakElem::shared(); + } + cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new(resolved, locator.next(&()))); cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new( - item.body.clone().styled(EnumElem::set_parents(smallvec![number])), + body.styled(EnumElem::set_parents(smallvec![number])), locator.next(&item.body.span()), )); number = diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs index 0ebe785f1..951870d68 100644 --- a/crates/typst-layout/src/math/accent.rs +++ b/crates/typst-layout/src/math/accent.rs @@ -16,7 +16,7 @@ pub fn layout_accent( styles: StyleChain, ) -> SourceResult<()> { let cramped = style_cramped(); - let mut base = ctx.layout_into_fragment(elem.base(), styles.chain(&cramped))?; + let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?; // Try to replace a glyph with its dotless variant. if let MathFragment::Glyph(glyph) = &mut base { @@ -29,8 +29,8 @@ pub fn layout_accent( let width = elem.size(styles).relative_to(base.width()); - let Accent(c) = elem.accent(); - let mut glyph = GlyphFragment::new(ctx, styles, *c, elem.span()); + let Accent(c) = elem.accent; + let mut glyph = GlyphFragment::new(ctx, styles, c, elem.span()); // Try to replace accent glyph with flattened variant. let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); diff --git a/crates/typst-layout/src/math/attach.rs b/crates/typst-layout/src/math/attach.rs index 263fc5c6d..e1d7d7c9d 100644 --- a/crates/typst-layout/src/math/attach.rs +++ b/crates/typst-layout/src/math/attach.rs @@ -1,10 +1,9 @@ use typst_library::diag::SourceResult; -use typst_library::foundations::{Packed, StyleChain}; +use typst_library::foundations::{Packed, StyleChain, SymbolElem}; use typst_library::layout::{Abs, Axis, Corner, Frame, Point, Rel, Size}; use typst_library::math::{ AttachElem, EquationElem, LimitsElem, PrimesElem, ScriptsElem, StretchElem, }; -use typst_library::text::TextElem; use typst_utils::OptionExt; use super::{ @@ -29,7 +28,7 @@ pub fn layout_attach( let elem = merged.as_ref().unwrap_or(elem); let stretch = stretch_size(styles, elem); - let mut base = ctx.layout_into_fragment(elem.base(), styles)?; + let mut base = ctx.layout_into_fragment(&elem.base, styles)?; let sup_style = style_for_superscript(styles); let sup_style_chain = styles.chain(&sup_style); let tl = elem.tl(sup_style_chain); @@ -95,7 +94,7 @@ pub fn layout_primes( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - match *elem.count() { + match elem.count { count @ 1..=4 => { let c = match count { 1 => '′', @@ -104,13 +103,14 @@ pub fn layout_primes( 4 => '⁗', _ => unreachable!(), }; - let f = ctx.layout_into_fragment(&TextElem::packed(c), styles)?; + let f = ctx.layout_into_fragment(&SymbolElem::packed(c), styles)?; ctx.push(f); } count => { // Custom amount of primes - let prime = - ctx.layout_into_fragment(&TextElem::packed('′'), styles)?.into_frame(); + let prime = ctx + .layout_into_fragment(&SymbolElem::packed('′'), styles)? + .into_frame(); let width = prime.width() * (count + 1) as f64 / 2.0; let mut frame = Frame::soft(Size::new(width, prime.height())); frame.set_baseline(prime.ascent()); @@ -134,7 +134,7 @@ pub fn layout_scripts( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?; + let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?; fragment.set_limits(Limits::Never); ctx.push(fragment); Ok(()) @@ -148,7 +148,7 @@ pub fn layout_limits( styles: StyleChain, ) -> SourceResult<()> { let limits = if elem.inline(styles) { Limits::Always } else { Limits::Display }; - let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?; + let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?; fragment.set_limits(limits); ctx.push(fragment); Ok(()) @@ -157,9 +157,9 @@ pub fn layout_limits( /// Get the size to stretch the base to. fn stretch_size(styles: StyleChain, elem: &Packed) -> Option> { // Extract from an EquationElem. - let mut base = elem.base(); + let mut base = &elem.base; while let Some(equation) = base.to_packed::() { - base = equation.body(); + base = &equation.body; } base.to_packed::().map(|stretch| stretch.size(styles)) diff --git a/crates/typst-layout/src/math/cancel.rs b/crates/typst-layout/src/math/cancel.rs index 716832fbf..9826397fa 100644 --- a/crates/typst-layout/src/math/cancel.rs +++ b/crates/typst-layout/src/math/cancel.rs @@ -16,7 +16,7 @@ pub fn layout_cancel( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let body = ctx.layout_into_fragment(elem.body(), styles)?; + let body = ctx.layout_into_fragment(&elem.body, styles)?; // Preserve properties of body. let body_class = body.class(); diff --git a/crates/typst-layout/src/math/frac.rs b/crates/typst-layout/src/math/frac.rs index fdc3be172..6d3caac45 100644 --- a/crates/typst-layout/src/math/frac.rs +++ b/crates/typst-layout/src/math/frac.rs @@ -1,5 +1,5 @@ use typst_library::diag::SourceResult; -use typst_library::foundations::{Content, Packed, Resolve, StyleChain}; +use typst_library::foundations::{Content, Packed, Resolve, StyleChain, SymbolElem}; use typst_library::layout::{Em, Frame, FrameItem, Point, Size}; use typst_library::math::{BinomElem, FracElem}; use typst_library::text::TextElem; @@ -23,8 +23,8 @@ pub fn layout_frac( layout_frac_like( ctx, styles, - elem.num(), - std::slice::from_ref(elem.denom()), + &elem.num, + std::slice::from_ref(&elem.denom), false, elem.span(), ) @@ -37,7 +37,7 @@ pub fn layout_binom( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - layout_frac_like(ctx, styles, elem.upper(), elem.lower(), true, elem.span()) + layout_frac_like(ctx, styles, &elem.upper, &elem.lower, true, elem.span()) } /// Layout a fraction or binomial. @@ -80,7 +80,10 @@ fn layout_frac_like( let denom = ctx.layout_into_frame( &Content::sequence( // Add a comma between each element. - denom.iter().flat_map(|a| [TextElem::packed(','), a.clone()]).skip(1), + denom + .iter() + .flat_map(|a| [SymbolElem::packed(','), a.clone()]) + .skip(1), ), styles.chain(&denom_style), )?; diff --git a/crates/typst-layout/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs index a0453c14f..81b726bad 100644 --- a/crates/typst-layout/src/math/fragment.rs +++ b/crates/typst-layout/src/math/fragment.rs @@ -1,23 +1,22 @@ use std::fmt::{self, Debug, Formatter}; use rustybuzz::Feature; -use smallvec::SmallVec; use ttf_parser::gsub::{AlternateSubstitution, SingleSubstitution, SubstitutionSubtable}; use ttf_parser::opentype_layout::LayoutTable; use ttf_parser::{GlyphId, Rect}; use typst_library::foundations::StyleChain; use typst_library::introspection::Tag; use typst_library::layout::{ - Abs, Axis, Corner, Em, Frame, FrameItem, HideElem, Point, Size, VAlignment, + Abs, Axis, Corner, Em, Frame, FrameItem, Point, Size, VAlignment, }; use typst_library::math::{EquationElem, MathSize}; -use typst_library::model::{Destination, LinkElem}; use typst_library::text::{Font, Glyph, Lang, Region, TextElem, TextItem}; use typst_library::visualize::Paint; use typst_syntax::Span; use unicode_math_class::MathClass; use super::{stretch_glyph, MathContext, Scaled}; +use crate::modifiers::{FrameModifiers, FrameModify}; #[derive(Debug, Clone)] pub enum MathFragment { @@ -245,8 +244,7 @@ pub struct GlyphFragment { pub class: MathClass, pub math_size: MathSize, pub span: Span, - pub dests: SmallVec<[Destination; 1]>, - pub hidden: bool, + pub modifiers: FrameModifiers, pub limits: Limits, pub extended_shape: bool, } @@ -302,8 +300,7 @@ impl GlyphFragment { accent_attach: Abs::zero(), class, span, - dests: LinkElem::dests_in(styles), - hidden: HideElem::hidden_in(styles), + modifiers: FrameModifiers::get_in(styles), extended_shape: false, }; fragment.set_id(ctx, id); @@ -390,7 +387,7 @@ impl GlyphFragment { let mut frame = Frame::soft(size); frame.set_baseline(self.ascent); frame.push(Point::with_y(self.ascent + self.shift), FrameItem::Text(item)); - frame.post_process_raw(self.dests, self.hidden); + frame.modify(&self.modifiers); frame } @@ -516,7 +513,7 @@ impl FrameFragment { let base_ascent = frame.ascent(); let accent_attach = frame.width() / 2.0; Self { - frame: frame.post_processed(styles), + frame: frame.modified(&FrameModifiers::get_in(styles)), font_size: TextElem::size_in(styles), class: EquationElem::class_in(styles).unwrap_or(MathClass::Normal), math_size: EquationElem::size_in(styles), diff --git a/crates/typst-layout/src/math/lr.rs b/crates/typst-layout/src/math/lr.rs index 2f4556fe5..bf8235411 100644 --- a/crates/typst-layout/src/math/lr.rs +++ b/crates/typst-layout/src/math/lr.rs @@ -2,6 +2,7 @@ use typst_library::diag::SourceResult; use typst_library::foundations::{Packed, StyleChain}; use typst_library::layout::{Abs, Axis, Rel}; use typst_library::math::{EquationElem, LrElem, MidElem}; +use typst_utils::SliceExt; use unicode_math_class::MathClass; use super::{stretch_fragment, MathContext, MathFragment, DELIM_SHORT_FALL}; @@ -13,32 +14,23 @@ pub fn layout_lr( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let mut body = elem.body(); - // Extract from an EquationElem. + let mut body = &elem.body; if let Some(equation) = body.to_packed::() { - body = equation.body(); + body = &equation.body; } // Extract implicit LrElem. if let Some(lr) = body.to_packed::() { if lr.size(styles).is_one() { - body = lr.body(); + body = &lr.body; } } let mut fragments = ctx.layout_into_fragments(body, styles)?; // Ignore leading and trailing ignorant fragments. - let start_idx = fragments - .iter() - .position(|f| !f.is_ignorant()) - .unwrap_or(fragments.len()); - let end_idx = fragments - .iter() - .skip(start_idx) - .rposition(|f| !f.is_ignorant()) - .map_or(start_idx, |i| start_idx + i + 1); + let (start_idx, end_idx) = fragments.split_prefix_suffix(|f| f.is_ignorant()); let inner_fragments = &mut fragments[start_idx..end_idx]; let axis = scaled!(ctx, styles, axis_height); @@ -100,7 +92,7 @@ pub fn layout_mid( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let mut fragments = ctx.layout_into_fragments(elem.body(), styles)?; + let mut fragments = ctx.layout_into_fragments(&elem.body, styles)?; for fragment in &mut fragments { match fragment { diff --git a/crates/typst-layout/src/math/mat.rs b/crates/typst-layout/src/math/mat.rs index d28bb037d..bf4929026 100644 --- a/crates/typst-layout/src/math/mat.rs +++ b/crates/typst-layout/src/math/mat.rs @@ -27,7 +27,7 @@ pub fn layout_vec( let frame = layout_vec_body( ctx, styles, - elem.children(), + &elem.children, elem.align(styles), elem.gap(styles), LeftRightAlternator::Right, @@ -44,7 +44,7 @@ pub fn layout_mat( styles: StyleChain, ) -> SourceResult<()> { let augment = elem.augment(styles); - let rows = elem.rows(); + let rows = &elem.rows; if let Some(aug) = &augment { for &offset in &aug.hline.0 { @@ -58,7 +58,7 @@ pub fn layout_mat( } } - let ncols = elem.rows().first().map_or(0, |row| row.len()); + let ncols = rows.first().map_or(0, |row| row.len()); for &offset in &aug.vline.0 { if offset == 0 || offset.unsigned_abs() >= ncols { @@ -97,7 +97,7 @@ pub fn layout_cases( let frame = layout_vec_body( ctx, styles, - elem.children(), + &elem.children, FixedAlignment::Start, elem.gap(styles), LeftRightAlternator::None, diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs index 62ecd1725..e5a3d94c9 100644 --- a/crates/typst-layout/src/math/mod.rs +++ b/crates/typst-layout/src/math/mod.rs @@ -17,7 +17,9 @@ use rustybuzz::Feature; use ttf_parser::Tag; use typst_library::diag::{bail, SourceResult}; use typst_library::engine::Engine; -use typst_library::foundations::{Content, NativeElement, Packed, Resolve, StyleChain}; +use typst_library::foundations::{ + Content, NativeElement, Packed, Resolve, StyleChain, SymbolElem, +}; use typst_library::introspection::{Counter, Locator, SplitLocator, TagElem}; use typst_library::layout::{ Abs, AlignElem, Axes, BlockElem, BoxElem, Em, FixedAlignment, Fragment, Frame, HElem, @@ -200,8 +202,7 @@ pub fn layout_equation_block( let counter = Counter::of(EquationElem::elem()) .display_at_loc(engine, elem.location().unwrap(), styles, numbering)? .spanned(span); - let number = - (engine.routines.layout_frame)(engine, &counter, locator.next(&()), styles, pod)?; + let number = crate::layout_frame(engine, &counter, locator.next(&()), styles, pod)?; static NUMBER_GUTTER: Em = Em::new(0.5); let full_number_width = number.width() + NUMBER_GUTTER.resolve(styles); @@ -535,6 +536,8 @@ fn layout_realized( layout_h(elem, ctx, styles)?; } else if let Some(elem) = elem.to_packed::() { self::text::layout_text(elem, ctx, styles)?; + } else if let Some(elem) = elem.to_packed::() { + self::text::layout_symbol(elem, ctx, styles)?; } else if let Some(elem) = elem.to_packed::() { layout_box(elem, ctx, styles)?; } else if elem.is::() { @@ -615,7 +618,7 @@ fn layout_box( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let frame = (ctx.engine.routines.layout_box)( + let frame = crate::inline::layout_box( elem, ctx.engine, ctx.locator.next(&elem.span()), @@ -632,7 +635,7 @@ fn layout_h( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - if let Spacing::Rel(rel) = elem.amount() { + if let Spacing::Rel(rel) = elem.amount { if rel.rel.is_zero() { ctx.push(MathFragment::Spacing(rel.abs.resolve(styles), elem.weak(styles))); } @@ -647,11 +650,10 @@ fn layout_class( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let class = *elem.class(); - let style = EquationElem::set_class(Some(class)).wrap(); - let mut fragment = ctx.layout_into_fragment(elem.body(), styles.chain(&style))?; - fragment.set_class(class); - fragment.set_limits(Limits::for_class(class)); + let style = EquationElem::set_class(Some(elem.class)).wrap(); + let mut fragment = ctx.layout_into_fragment(&elem.body, styles.chain(&style))?; + fragment.set_class(elem.class); + fragment.set_limits(Limits::for_class(elem.class)); ctx.push(fragment); Ok(()) } @@ -663,7 +665,7 @@ fn layout_op( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let fragment = ctx.layout_into_fragment(elem.text(), styles)?; + let fragment = ctx.layout_into_fragment(&elem.text, styles)?; let italics = fragment.italics_correction(); let accent_attach = fragment.accent_attach(); let text_like = fragment.is_text_like(); @@ -689,7 +691,7 @@ fn layout_external( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult { - (ctx.engine.routines.layout_frame)( + crate::layout_frame( ctx.engine, content, ctx.locator.next(&content.span()), diff --git a/crates/typst-layout/src/math/root.rs b/crates/typst-layout/src/math/root.rs index 4e5d844f2..a6b5c03d0 100644 --- a/crates/typst-layout/src/math/root.rs +++ b/crates/typst-layout/src/math/root.rs @@ -18,7 +18,6 @@ pub fn layout_root( styles: StyleChain, ) -> SourceResult<()> { let index = elem.index(styles); - let radicand = elem.radicand(); let span = elem.span(); let gap = scaled!( @@ -36,7 +35,7 @@ pub fn layout_root( let radicand = { let cramped = style_cramped(); let styles = styles.chain(&cramped); - let run = ctx.layout_into_run(radicand, styles)?; + let run = ctx.layout_into_run(&elem.radicand, styles)?; let multiline = run.is_multiline(); let mut radicand = run.into_fragment(styles).into_frame(); if multiline { diff --git a/crates/typst-layout/src/math/stretch.rs b/crates/typst-layout/src/math/stretch.rs index 4bc5a9262..dafa8cbe8 100644 --- a/crates/typst-layout/src/math/stretch.rs +++ b/crates/typst-layout/src/math/stretch.rs @@ -10,6 +10,7 @@ use super::{ delimiter_alignment, GlyphFragment, MathContext, MathFragment, Scaled, VariantFragment, }; +use crate::modifiers::FrameModify; /// Maximum number of times extenders can be repeated. const MAX_REPEATS: usize = 1024; @@ -21,7 +22,7 @@ pub fn layout_stretch( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?; + let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?; stretch_fragment( ctx, styles, @@ -265,7 +266,7 @@ fn assemble( let mut frame = Frame::soft(size); let mut offset = Abs::zero(); frame.set_baseline(baseline); - frame.post_process_raw(base.dests, base.hidden); + frame.modify(&base.modifiers); for (fragment, advance) in selected { let pos = match axis { diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs index eb30373dd..9a64992aa 100644 --- a/crates/typst-layout/src/math/text.rs +++ b/crates/typst-layout/src/math/text.rs @@ -1,8 +1,8 @@ use std::f64::consts::SQRT_2; -use ecow::{eco_vec, EcoString}; +use ecow::EcoString; use typst_library::diag::SourceResult; -use typst_library::foundations::{Packed, StyleChain, StyleVec}; +use typst_library::foundations::{Packed, StyleChain, SymbolElem}; use typst_library::layout::{Abs, Size}; use typst_library::math::{EquationElem, MathSize, MathVariant}; use typst_library::text::{ @@ -20,56 +20,68 @@ pub fn layout_text( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let text = elem.text(); + let text = &elem.text; let span = elem.span(); - let mut chars = text.chars(); - let math_size = EquationElem::size_in(styles); - let mut dtls = ctx.dtls_table.is_some(); - let fragment: MathFragment = if let Some(mut glyph) = chars - .next() - .filter(|_| chars.next().is_none()) - .map(|c| dtls_char(c, &mut dtls)) - .map(|c| styled_char(styles, c, true)) - .and_then(|c| GlyphFragment::try_new(ctx, styles, c, span)) - { - // A single letter that is available in the math font. - if dtls { - glyph.make_dotless_form(ctx); - } + let fragment = if text.contains(is_newline) { + layout_text_lines(text.split(is_newline), span, ctx, styles)? + } else { + layout_inline_text(text, span, ctx, styles)? + }; + ctx.push(fragment); + Ok(()) +} - match math_size { - MathSize::Script => { - glyph.make_script_size(ctx); - } - MathSize::ScriptScript => { - glyph.make_script_script_size(ctx); - } - _ => (), +/// Layout multiple lines of text. +fn layout_text_lines<'a>( + lines: impl Iterator, + span: Span, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult { + let mut fragments = vec![]; + for (i, line) in lines.enumerate() { + if i != 0 { + fragments.push(MathFragment::Linebreak); } + if !line.is_empty() { + fragments.push(layout_inline_text(line, span, ctx, styles)?.into()); + } + } + let mut frame = MathRun::new(fragments).into_frame(styles); + let axis = scaled!(ctx, styles, axis_height); + frame.set_baseline(frame.height() / 2.0 + axis); + Ok(FrameFragment::new(styles, frame)) +} - if glyph.class == MathClass::Large { - let mut variant = if math_size == MathSize::Display { - let height = scaled!(ctx, styles, display_operator_min_height) - .max(SQRT_2 * glyph.height()); - glyph.stretch_vertical(ctx, height, Abs::zero()) - } else { - glyph.into_variant() - }; - // TeXbook p 155. Large operators are always vertically centered on the axis. - variant.center_on_axis(ctx); - variant.into() - } else { - glyph.into() - } - } else if text.chars().all(|c| c.is_ascii_digit() || c == '.') { - // Numbers aren't that difficult. +/// Layout the given text string into a [`FrameFragment`] after styling all +/// characters for the math font (without auto-italics). +fn layout_inline_text( + text: &str, + span: Span, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult { + 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 c in text.chars() { - let c = styled_char(styles, c, false); - fragments.push(GlyphFragment::new(ctx, styles, c, span).into()); + let is_single = text.chars().count() == 1; + for unstyled_c in text.chars() { + let c = styled_char(styles, unstyled_c, false); + let mut glyph = GlyphFragment::new(ctx, styles, c, span); + if is_single { + // Duplicate what `layout_glyph` does exactly even if it's + // probably incorrect here. + match EquationElem::size_in(styles) { + MathSize::Script => glyph.make_script_size(ctx), + MathSize::ScriptScript => glyph.make_script_script_size(ctx), + _ => {} + } + } + fragments.push(glyph.into()); } let frame = MathRun::new(fragments).into_frame(styles); - FrameFragment::new(styles, frame).with_text_like(true).into() + Ok(FrameFragment::new(styles, frame).with_text_like(true)) } else { let local = [ TextElem::set_top_edge(TopEdge::Metric(TopEdgeMetric::Bounds)), @@ -77,64 +89,97 @@ pub fn layout_text( ] .map(|p| p.wrap()); - // Anything else is handled by Typst's standard text layout. let styles = styles.chain(&local); - let text: EcoString = + let styled_text: EcoString = text.chars().map(|c| styled_char(styles, c, false)).collect(); - if text.contains(is_newline) { - let mut fragments = vec![]; - for (i, piece) in text.split(is_newline).enumerate() { - if i != 0 { - fragments.push(MathFragment::Linebreak); - } - if !piece.is_empty() { - fragments.push(layout_complex_text(piece, ctx, span, styles)?.into()); - } - } - let mut frame = MathRun::new(fragments).into_frame(styles); - let axis = scaled!(ctx, styles, axis_height); - frame.set_baseline(frame.height() / 2.0 + axis); - FrameFragment::new(styles, frame).into() - } else { - layout_complex_text(&text, ctx, span, styles)?.into() + + let spaced = styled_text.graphemes(true).nth(1).is_some(); + let elem = TextElem::packed(styled_text).spanned(span); + + // There isn't a natural width for a paragraph in a math environment; + // because it will be placed somewhere probably not at the left margin + // it will overflow. So emulate an `hbox` instead and allow the + // paragraph to extend as far as needed. + let frame = crate::inline::layout_inline( + ctx.engine, + &[(&elem, styles)], + &mut ctx.locator.next(&span).split(), + styles, + Size::splat(Abs::inf()), + false, + None, + )? + .into_frame(); + + Ok(FrameFragment::new(styles, frame) + .with_class(MathClass::Alphabetic) + .with_text_like(true) + .with_spaced(spaced)) + } +} + +/// Layout a single character in the math font with the correct styling applied +/// (includes auto-italics). +pub fn layout_symbol( + elem: &Packed, + ctx: &mut MathContext, + styles: StyleChain, +) -> SourceResult<()> { + // Switch dotless char to normal when we have the dtls OpenType feature. + // This should happen before the main styling pass. + let (unstyled_c, dtls) = match try_dotless(elem.text) { + Some(c) if ctx.dtls_table.is_some() => (c, true), + _ => (elem.text, false), + }; + let c = styled_char(styles, unstyled_c, true); + let fragment = match GlyphFragment::try_new(ctx, styles, c, elem.span()) { + Some(glyph) => layout_glyph(glyph, dtls, ctx, styles), + None => { + // Not in the math font, fallback to normal inline text layout. + layout_inline_text(c.encode_utf8(&mut [0; 4]), elem.span(), ctx, styles)? + .into() } }; - ctx.push(fragment); Ok(()) } -/// Layout the given text string into a [`FrameFragment`]. -fn layout_complex_text( - text: &str, +/// Layout a [`GlyphFragment`]. +fn layout_glyph( + mut glyph: GlyphFragment, + dtls: bool, ctx: &mut MathContext, - span: Span, styles: StyleChain, -) -> SourceResult { - // There isn't a natural width for a paragraph in a math environment; - // because it will be placed somewhere probably not at the left margin - // it will overflow. So emulate an `hbox` instead and allow the paragraph - // to extend as far as needed. - let spaced = text.graphemes(true).nth(1).is_some(); - let elem = TextElem::packed(text).spanned(span); - let frame = (ctx.engine.routines.layout_inline)( - ctx.engine, - &StyleVec::wrap(eco_vec![elem]), - ctx.locator.next(&span), - styles, - false, - Size::splat(Abs::inf()), - false, - )? - .into_frame(); +) -> MathFragment { + if dtls { + glyph.make_dotless_form(ctx); + } + let math_size = EquationElem::size_in(styles); + match math_size { + MathSize::Script => glyph.make_script_size(ctx), + MathSize::ScriptScript => glyph.make_script_script_size(ctx), + _ => {} + } - Ok(FrameFragment::new(styles, frame) - .with_class(MathClass::Alphabetic) - .with_text_like(true) - .with_spaced(spaced)) + if glyph.class == MathClass::Large { + let mut variant = if math_size == MathSize::Display { + let height = scaled!(ctx, styles, display_operator_min_height) + .max(SQRT_2 * glyph.height()); + glyph.stretch_vertical(ctx, height, Abs::zero()) + } else { + glyph.into_variant() + }; + // TeXbook p 155. Large operators are always vertically centered on the + // axis. + variant.center_on_axis(ctx); + variant.into() + } else { + glyph.into() + } } -/// Select the correct styled math letter. +/// Style the character by selecting the unicode codepoint for italic, bold, +/// caligraphic, etc. /// /// /// @@ -353,15 +398,12 @@ fn greek_exception( }) } -/// Switch dotless character to non dotless character for use of the dtls -/// OpenType feature. -pub fn dtls_char(c: char, dtls: &mut bool) -> char { - match (c, *dtls) { - ('ı', true) => 'i', - ('ȷ', true) => 'j', - _ => { - *dtls = false; - c - } +/// The non-dotless version of a dotless character that can be used with the +/// `dtls` OpenType feature. +pub fn try_dotless(c: char) -> Option { + match c { + 'ı' => Some('i'), + 'ȷ' => Some('j'), + _ => None, } } diff --git a/crates/typst-layout/src/math/underover.rs b/crates/typst-layout/src/math/underover.rs index e55996389..7b3617c3e 100644 --- a/crates/typst-layout/src/math/underover.rs +++ b/crates/typst-layout/src/math/underover.rs @@ -32,7 +32,7 @@ pub fn layout_underline( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - layout_underoverline(ctx, styles, elem.body(), elem.span(), Position::Under) + layout_underoverline(ctx, styles, &elem.body, elem.span(), Position::Under) } /// Lays out an [`OverlineElem`]. @@ -42,7 +42,7 @@ pub fn layout_overline( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - layout_underoverline(ctx, styles, elem.body(), elem.span(), Position::Over) + layout_underoverline(ctx, styles, &elem.body, elem.span(), Position::Over) } /// Lays out an [`UnderbraceElem`]. @@ -55,7 +55,7 @@ pub fn layout_underbrace( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⏟', BRACE_GAP, @@ -74,7 +74,7 @@ pub fn layout_overbrace( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⏞', BRACE_GAP, @@ -93,7 +93,7 @@ pub fn layout_underbracket( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⎵', BRACKET_GAP, @@ -112,7 +112,7 @@ pub fn layout_overbracket( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⎴', BRACKET_GAP, @@ -131,7 +131,7 @@ pub fn layout_underparen( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⏝', PAREN_GAP, @@ -150,7 +150,7 @@ pub fn layout_overparen( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⏜', PAREN_GAP, @@ -169,7 +169,7 @@ pub fn layout_undershell( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⏡', SHELL_GAP, @@ -188,7 +188,7 @@ pub fn layout_overshell( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⏠', SHELL_GAP, diff --git a/crates/typst-layout/src/modifiers.rs b/crates/typst-layout/src/modifiers.rs new file mode 100644 index 000000000..ac5f40b04 --- /dev/null +++ b/crates/typst-layout/src/modifiers.rs @@ -0,0 +1,110 @@ +use typst_library::foundations::StyleChain; +use typst_library::layout::{Fragment, Frame, FrameItem, HideElem, Point}; +use typst_library::model::{Destination, LinkElem}; + +/// Frame-level modifications resulting from styles that do not impose any +/// layout structure. +/// +/// These are always applied at the highest level of style uniformity. +/// Consequently, they must be applied by all layouters that manually manage +/// styles of their children (because they can produce children with varying +/// styles). This currently includes flow, inline, and math layout. +/// +/// Other layouters don't manually need to handle it because their parents that +/// result from realization will take care of it and the styles can only apply +/// to them as a whole, not part of it (since they don't manage styles). +/// +/// Currently existing frame modifiers are: +/// - `HideElem::hidden` +/// - `LinkElem::dests` +#[derive(Debug, Clone)] +pub struct FrameModifiers { + /// A destination to link to. + dest: Option, + /// Whether the contents of the frame should be hidden. + hidden: bool, +} + +impl FrameModifiers { + /// Retrieve all modifications that should be applied per-frame. + pub fn get_in(styles: StyleChain) -> Self { + Self { + dest: LinkElem::current_in(styles), + hidden: HideElem::hidden_in(styles), + } + } +} + +/// Applies [`FrameModifiers`]. +pub trait FrameModify { + /// Apply the modifiers in-place. + fn modify(&mut self, modifiers: &FrameModifiers); + + /// Apply the modifiers, and return the modified result. + fn modified(mut self, modifiers: &FrameModifiers) -> Self + where + Self: Sized, + { + self.modify(modifiers); + self + } +} + +impl FrameModify for Frame { + fn modify(&mut self, modifiers: &FrameModifiers) { + if let Some(dest) = &modifiers.dest { + let size = self.size(); + self.push(Point::zero(), FrameItem::Link(dest.clone(), size)); + } + + if modifiers.hidden { + self.hide(); + } + } +} + +impl FrameModify for Fragment { + fn modify(&mut self, modifiers: &FrameModifiers) { + for frame in self.iter_mut() { + frame.modify(modifiers); + } + } +} + +impl FrameModify for Result +where + T: FrameModify, +{ + fn modify(&mut self, props: &FrameModifiers) { + if let Ok(inner) = self { + inner.modify(props); + } + } +} + +/// Performs layout and modification in one step. +/// +/// This just runs `layout(styles).modified(&FrameModifiers::get_in(styles))`, +/// but with the additional step that redundant modifiers (which are already +/// applied here) are removed from the `styles` passed to `layout`. This is used +/// for the layout of containers like `block`. +pub fn layout_and_modify(styles: StyleChain, layout: F) -> R +where + F: FnOnce(StyleChain) -> R, + R: FrameModify, +{ + let modifiers = FrameModifiers::get_in(styles); + + // Disable the current link internally since it's already applied at this + // level of layout. This means we don't generate redundant nested links, + // which may bloat the output considerably. + let reset; + let outer = styles; + let mut styles = styles; + if modifiers.dest.is_some() { + reset = LinkElem::set_current(None).wrap(); + styles = outer.chain(&reset); + } + + layout(styles).modified(&modifiers) +} diff --git a/crates/typst-layout/src/pages/collect.rs b/crates/typst-layout/src/pages/collect.rs index 0bbae9f4c..8eab18a62 100644 --- a/crates/typst-layout/src/pages/collect.rs +++ b/crates/typst-layout/src/pages/collect.rs @@ -23,7 +23,7 @@ pub enum Item<'a> { /// things like tags and weak pagebreaks. pub fn collect<'a>( mut children: &'a mut [Pair<'a>], - mut locator: SplitLocator<'a>, + locator: &mut SplitLocator<'a>, mut initial: StyleChain<'a>, ) -> Vec> { // The collected page-level items. diff --git a/crates/typst-layout/src/pages/mod.rs b/crates/typst-layout/src/pages/mod.rs index 27002a6c9..14dc0f3fb 100644 --- a/crates/typst-layout/src/pages/mod.rs +++ b/crates/typst-layout/src/pages/mod.rs @@ -83,7 +83,7 @@ fn layout_document_impl( styles, )?; - let pages = layout_pages(&mut engine, &mut children, locator, styles)?; + let pages = layout_pages(&mut engine, &mut children, &mut locator, styles)?; let introspector = Introspector::paged(&pages); Ok(PagedDocument { pages, info, introspector }) @@ -93,7 +93,7 @@ fn layout_document_impl( fn layout_pages<'a>( engine: &mut Engine, children: &'a mut [Pair<'a>], - locator: SplitLocator<'a>, + locator: &mut SplitLocator<'a>, styles: StyleChain<'a>, ) -> SourceResult> { // Slice up the children into logical parts. diff --git a/crates/typst-layout/src/pages/run.rs b/crates/typst-layout/src/pages/run.rs index 79ff5ab05..6d2d29da5 100644 --- a/crates/typst-layout/src/pages/run.rs +++ b/crates/typst-layout/src/pages/run.rs @@ -19,7 +19,7 @@ use typst_library::visualize::Paint; use typst_library::World; use typst_utils::Numeric; -use crate::flow::layout_flow; +use crate::flow::{layout_flow, FlowMode}; /// A mostly finished layout for one page. Needs only knowledge of its exact /// page number to be finalized into a `Page`. (Because the margins can depend @@ -181,7 +181,7 @@ fn layout_page_run_impl( Regions::repeat(area, area.map(Abs::is_finite)), PageElem::columns_in(styles), ColumnsElem::gutter_in(styles), - true, + FlowMode::Root, )?; // Layouts a single marginal. diff --git a/crates/typst-layout/src/shapes.rs b/crates/typst-layout/src/shapes.rs index 7c56bf763..eb665f06a 100644 --- a/crates/typst-layout/src/shapes.rs +++ b/crates/typst-layout/src/shapes.rs @@ -62,7 +62,7 @@ pub fn layout_path( axes.resolve(styles).zip_map(region.size, Rel::relative_to).to_point() }; - let vertices = elem.vertices(); + let vertices = &elem.vertices; let points: Vec = vertices.iter().map(|c| resolve(c.vertex())).collect(); let mut size = Size::zero(); @@ -150,7 +150,7 @@ pub fn layout_curve( ) -> SourceResult { let mut builder = CurveBuilder::new(region, styles); - for item in elem.components() { + for item in &elem.components { match item { CurveComponent::Move(element) => { let relative = element.relative(styles); @@ -399,7 +399,7 @@ pub fn layout_polygon( region: Region, ) -> SourceResult { let points: Vec = elem - .vertices() + .vertices .iter() .map(|c| c.resolve(styles).zip_map(region.size, Rel::relative_to).to_point()) .collect(); diff --git a/crates/typst-layout/src/stack.rs b/crates/typst-layout/src/stack.rs index a3ebc9f36..c468945eb 100644 --- a/crates/typst-layout/src/stack.rs +++ b/crates/typst-layout/src/stack.rs @@ -27,7 +27,7 @@ pub fn layout_stack( let spacing = elem.spacing(styles); let mut deferred = None; - for child in elem.children() { + for child in &elem.children { match child { StackChild::Spacing(kind) => { layouter.layout_spacing(*kind); @@ -36,14 +36,14 @@ pub fn layout_stack( StackChild::Block(block) => { // Transparently handle `h`. if let (Axis::X, Some(h)) = (axis, block.to_packed::()) { - layouter.layout_spacing(*h.amount()); + layouter.layout_spacing(h.amount); deferred = None; continue; } // Transparently handle `v`. if let (Axis::Y, Some(v)) = (axis, block.to_packed::()) { - layouter.layout_spacing(*v.amount()); + layouter.layout_spacing(v.amount); deferred = None; continue; } diff --git a/crates/typst-layout/src/transforms.rs b/crates/typst-layout/src/transforms.rs index e0f29c4c2..f4526dd09 100644 --- a/crates/typst-layout/src/transforms.rs +++ b/crates/typst-layout/src/transforms.rs @@ -52,7 +52,7 @@ pub fn layout_rotate( region, size, styles, - elem.body(), + &elem.body, Transform::rotate(angle), align, elem.reflow(styles), @@ -81,7 +81,7 @@ pub fn layout_scale( region, size, styles, - elem.body(), + &elem.body, Transform::scale(scale.x, scale.y), elem.origin(styles).resolve(styles), elem.reflow(styles), @@ -169,7 +169,7 @@ pub fn layout_skew( region, size, styles, - elem.body(), + &elem.body, Transform::skew(ax, ay), align, elem.reflow(styles), diff --git a/crates/typst-library/src/foundations/args.rs b/crates/typst-library/src/foundations/args.rs index a60e6d7f2..430c4e9ad 100644 --- a/crates/typst-library/src/foundations/args.rs +++ b/crates/typst-library/src/foundations/args.rs @@ -1,4 +1,5 @@ use std::fmt::{self, Debug, Formatter}; +use std::ops::Add; use ecow::{eco_format, eco_vec, EcoString, EcoVec}; use typst_syntax::{Span, Spanned}; @@ -304,8 +305,6 @@ impl Args { /// ``` #[func(constructor)] pub fn construct( - /// The real arguments (the other argument is just for the docs). - /// The docs argument cannot be called `args`. args: &mut Args, /// The arguments to construct. #[external] @@ -366,7 +365,7 @@ impl Debug for Args { impl Repr for Args { fn repr(&self) -> EcoString { let pieces = self.items.iter().map(Arg::repr).collect::>(); - repr::pretty_array_like(&pieces, false).into() + eco_format!("arguments{}", repr::pretty_array_like(&pieces, false)) } } @@ -376,6 +375,21 @@ impl PartialEq for Args { } } +impl Add for Args { + type Output = Self; + + fn add(mut self, rhs: Self) -> Self::Output { + self.items.retain(|item| { + !item.name.as_ref().is_some_and(|name| { + rhs.items.iter().any(|a| a.name.as_ref() == Some(name)) + }) + }); + self.items.extend(rhs.items); + self.span = Span::detached(); + self + } +} + /// An argument to a function call: `12` or `draw: false`. #[derive(Clone, Hash)] #[allow(clippy::derived_hash_with_manual_eq)] diff --git a/crates/typst-library/src/foundations/array.rs b/crates/typst-library/src/foundations/array.rs index 4667ee765..aad7266bc 100644 --- a/crates/typst-library/src/foundations/array.rs +++ b/crates/typst-library/src/foundations/array.rs @@ -301,9 +301,7 @@ impl Array { #[func] pub fn find( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The function to apply to each item. Must return a boolean. searcher: Func, @@ -325,9 +323,7 @@ impl Array { #[func] pub fn position( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The function to apply to each item. Must return a boolean. searcher: Func, @@ -363,8 +359,6 @@ impl Array { /// ``` #[func] pub fn range( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The start of the range (inclusive). #[external] @@ -402,9 +396,7 @@ impl Array { #[func] pub fn filter( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The function to apply to each item. Must return a boolean. test: Func, @@ -427,9 +419,7 @@ impl Array { #[func] pub fn map( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The function to apply to each item. mapper: Func, @@ -481,8 +471,6 @@ impl Array { #[func] pub fn zip( self, - /// The real arguments (the `others` arguments are just for the docs, this - /// function is a bit involved, so we parse the positional arguments manually). args: &mut Args, /// Whether all arrays have to have the same length. /// For example, `{(1, 2).zip((1, 2, 3), exact: true)}` produces an @@ -569,9 +557,7 @@ impl Array { #[func] pub fn fold( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The initial value to start with. init: Value, @@ -631,9 +617,7 @@ impl Array { #[func] pub fn any( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The function to apply to each item. Must return a boolean. test: Func, @@ -651,9 +635,7 @@ impl Array { #[func] pub fn all( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The function to apply to each item. Must return a boolean. test: Func, @@ -831,11 +813,8 @@ impl Array { #[func] pub fn sorted( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, /// If given, applies this function to the elements in the array to /// determine the keys to sort by. @@ -881,9 +860,7 @@ impl Array { #[func(title = "Deduplicate")] pub fn dedup( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// If given, applies this function to the elements in the array to /// determine the keys to deduplicate by. @@ -967,9 +944,7 @@ impl Array { #[func] pub fn reduce( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The reducing function. Must have two parameters: One for the /// accumulated value and one for an item. @@ -1124,6 +1099,53 @@ impl FromValue for SmallVec<[T; N]> { } } +/// One element, or multiple provided as an array. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct OneOrMultiple(pub Vec); + +impl Reflect for OneOrMultiple { + fn input() -> CastInfo { + T::input() + Array::input() + } + + fn output() -> CastInfo { + T::output() + Array::output() + } + + fn castable(value: &Value) -> bool { + Array::castable(value) || T::castable(value) + } +} + +impl IntoValue for OneOrMultiple { + fn into_value(self) -> Value { + self.0.into_value() + } +} + +impl FromValue for OneOrMultiple { + fn from_value(value: Value) -> HintedStrResult { + if T::castable(&value) { + return Ok(Self(vec![T::from_value(value)?])); + } + if Array::castable(&value) { + return Ok(Self( + Array::from_value(value)? + .into_iter() + .map(|value| T::from_value(value)) + .collect::>()?, + )); + } + Err(Self::error(&value)) + } +} + +impl Default for OneOrMultiple { + fn default() -> Self { + Self(vec![]) + } +} + /// The error message when the array is empty. #[cold] fn array_is_empty() -> EcoString { diff --git a/crates/typst-library/src/foundations/bytes.rs b/crates/typst-library/src/foundations/bytes.rs index 05fe4763a..d633c99ad 100644 --- a/crates/typst-library/src/foundations/bytes.rs +++ b/crates/typst-library/src/foundations/bytes.rs @@ -1,6 +1,8 @@ -use std::borrow::Cow; +use std::any::Any; use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; use std::ops::{Add, AddAssign, Deref}; +use std::str::Utf8Error; use std::sync::Arc; use ecow::{eco_format, EcoString}; @@ -39,28 +41,75 @@ use crate::foundations::{cast, func, scope, ty, Array, Reflect, Repr, Str, Value /// #str(data.slice(1, 4)) /// ``` #[ty(scope, cast)] -#[derive(Clone, Hash, Eq, PartialEq)] -pub struct Bytes(Arc>>); +#[derive(Clone, Hash)] +#[allow(clippy::derived_hash_with_manual_eq)] +pub struct Bytes(Arc>); impl Bytes { - /// Create a buffer from a static byte slice. - pub fn from_static(slice: &'static [u8]) -> Self { - Self(Arc::new(LazyHash::new(Cow::Borrowed(slice)))) + /// Create `Bytes` from anything byte-like. + /// + /// The `data` type will directly back this bytes object. This means you can + /// e.g. pass `&'static [u8]` or `[u8; 8]` and no extra vector will be + /// allocated. + /// + /// If the type is `Vec` and the `Bytes` are unique (i.e. not cloned), + /// the vector will be reused when mutating to the `Bytes`. + /// + /// If your source type is a string, prefer [`Bytes::from_string`] to + /// directly use the UTF-8 encoded string data without any copying. + pub fn new(data: T) -> Self + where + T: AsRef<[u8]> + Send + Sync + 'static, + { + Self(Arc::new(LazyHash::new(data))) + } + + /// Create `Bytes` from anything string-like, implicitly viewing the UTF-8 + /// representation. + /// + /// The `data` type will directly back this bytes object. This means you can + /// e.g. pass `String` or `EcoString` without any copying. + pub fn from_string(data: T) -> Self + where + T: AsRef + Send + Sync + 'static, + { + Self(Arc::new(LazyHash::new(StrWrapper(data)))) } /// Return `true` if the length is 0. pub fn is_empty(&self) -> bool { - self.0.is_empty() + self.as_slice().is_empty() } - /// Return a view into the buffer. + /// Return a view into the bytes. pub fn as_slice(&self) -> &[u8] { self } - /// Return a copy of the buffer as a vector. + /// Try to view the bytes as an UTF-8 string. + /// + /// If these bytes were created via `Bytes::from_string`, UTF-8 validation + /// is skipped. + pub fn as_str(&self) -> Result<&str, Utf8Error> { + self.inner().as_str() + } + + /// Return a copy of the bytes as a vector. pub fn to_vec(&self) -> Vec { - self.0.to_vec() + self.as_slice().to_vec() + } + + /// Try to turn the bytes into a `Str`. + /// + /// - If these bytes were created via `Bytes::from_string::`, the + /// string is cloned directly. + /// - If these bytes were created via `Bytes::from_string`, but from a + /// different type of string, UTF-8 validation is still skipped. + pub fn to_str(&self) -> Result { + match self.inner().as_any().downcast_ref::() { + Some(string) => Ok(string.clone()), + None => self.as_str().map(Into::into), + } } /// Resolve an index or throw an out of bounds error. @@ -72,12 +121,15 @@ impl Bytes { /// /// `index == len` is considered in bounds. fn locate_opt(&self, index: i64) -> Option { + let len = self.as_slice().len(); let wrapped = - if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) }; + if index >= 0 { Some(index) } else { (len as i64).checked_add(index) }; + wrapped.and_then(|v| usize::try_from(v).ok()).filter(|&v| v <= len) + } - wrapped - .and_then(|v| usize::try_from(v).ok()) - .filter(|&v| v <= self.0.len()) + /// Access the inner `dyn Bytelike`. + fn inner(&self) -> &dyn Bytelike { + &**self.0 } } @@ -106,7 +158,7 @@ impl Bytes { /// The length in bytes. #[func(title = "Length")] pub fn len(&self) -> usize { - self.0.len() + self.as_slice().len() } /// Returns the byte at the specified index. Returns the default value if @@ -122,13 +174,13 @@ impl Bytes { default: Option, ) -> StrResult { self.locate_opt(index) - .and_then(|i| self.0.get(i).map(|&b| Value::Int(b.into()))) + .and_then(|i| self.as_slice().get(i).map(|&b| Value::Int(b.into()))) .or(default) .ok_or_else(|| out_of_bounds_no_default(index, self.len())) } - /// Extracts a subslice of the bytes. Fails with an error if the start or end - /// index is out of bounds. + /// Extracts a subslice of the bytes. Fails with an error if the start or + /// end index is out of bounds. #[func] pub fn slice( &self, @@ -148,9 +200,17 @@ impl Bytes { if end.is_none() { end = count.map(|c: i64| start + c); } + let start = self.locate(start)?; let end = self.locate(end.unwrap_or(self.len() as i64))?.max(start); - Ok(self.0[start..end].into()) + let slice = &self.as_slice()[start..end]; + + // We could hold a view into the original bytes here instead of + // making a copy, but it's unclear when that's worth it. Java + // originally did that for strings, but went back on it because a + // very small view into a very large buffer would be a sort of + // memory leak. + Ok(Bytes::new(slice.to_vec())) } } @@ -170,7 +230,15 @@ impl Deref for Bytes { type Target = [u8]; fn deref(&self) -> &Self::Target { - &self.0 + self.inner().as_bytes() + } +} + +impl Eq for Bytes {} + +impl PartialEq for Bytes { + fn eq(&self, other: &Self) -> bool { + self.0.eq(&other.0) } } @@ -180,18 +248,6 @@ impl AsRef<[u8]> for Bytes { } } -impl From<&[u8]> for Bytes { - fn from(slice: &[u8]) -> Self { - Self(Arc::new(LazyHash::new(slice.to_vec().into()))) - } -} - -impl From> for Bytes { - fn from(vec: Vec) -> Self { - Self(Arc::new(LazyHash::new(vec.into()))) - } -} - impl Add for Bytes { type Output = Self; @@ -207,10 +263,12 @@ impl AddAssign for Bytes { // Nothing to do } else if self.is_empty() { *self = rhs; - } else if Arc::strong_count(&self.0) == 1 && matches!(**self.0, Cow::Owned(_)) { - Arc::make_mut(&mut self.0).to_mut().extend_from_slice(&rhs); + } else if let Some(vec) = Arc::get_mut(&mut self.0) + .and_then(|unique| unique.as_any_mut().downcast_mut::>()) + { + vec.extend_from_slice(&rhs); } else { - *self = Self::from([self.as_slice(), rhs.as_slice()].concat()); + *self = Self::new([self.as_slice(), rhs.as_slice()].concat()); } } } @@ -228,20 +286,79 @@ impl Serialize for Bytes { } } +/// Any type that can back a byte buffer. +trait Bytelike: Send + Sync { + fn as_bytes(&self) -> &[u8]; + fn as_str(&self) -> Result<&str, Utf8Error>; + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; +} + +impl Bytelike for T +where + T: AsRef<[u8]> + Send + Sync + 'static, +{ + fn as_bytes(&self) -> &[u8] { + self.as_ref() + } + + fn as_str(&self) -> Result<&str, Utf8Error> { + std::str::from_utf8(self.as_ref()) + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + +impl Hash for dyn Bytelike { + fn hash(&self, state: &mut H) { + self.as_bytes().hash(state); + } +} + +/// Makes string-like objects usable with `Bytes`. +struct StrWrapper(T); + +impl Bytelike for StrWrapper +where + T: AsRef + Send + Sync + 'static, +{ + fn as_bytes(&self) -> &[u8] { + self.0.as_ref().as_bytes() + } + + fn as_str(&self) -> Result<&str, Utf8Error> { + Ok(self.0.as_ref()) + } + + fn as_any(&self) -> &dyn Any { + self + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + /// A value that can be cast to bytes. pub struct ToBytes(Bytes); cast! { ToBytes, - v: Str => Self(v.as_bytes().into()), + v: Str => Self(Bytes::from_string(v)), v: Array => Self(v.iter() .map(|item| match item { Value::Int(byte @ 0..=255) => Ok(*byte as u8), Value::Int(_) => bail!("number must be between 0 and 255"), value => Err(::error(value)), }) - .collect::, _>>()? - .into() + .collect::, _>>() + .map(Bytes::new)? ), v: Bytes => Self(v), } diff --git a/crates/typst-library/src/foundations/calc.rs b/crates/typst-library/src/foundations/calc.rs index fd4498e07..a8e0eaeb3 100644 --- a/crates/typst-library/src/foundations/calc.rs +++ b/crates/typst-library/src/foundations/calc.rs @@ -97,7 +97,6 @@ cast! { /// ``` #[func(title = "Power")] pub fn pow( - /// The callsite span. span: Span, /// The base of the power. /// @@ -159,7 +158,6 @@ pub fn pow( /// ``` #[func(title = "Exponential")] pub fn exp( - /// The callsite span. span: Span, /// The exponent of the power. exponent: Spanned, @@ -412,7 +410,6 @@ pub fn tanh( /// ``` #[func(title = "Logarithm")] pub fn log( - /// The callsite span. span: Span, /// The number whose logarithm to calculate. Must be strictly positive. value: Spanned, @@ -454,7 +451,6 @@ pub fn log( /// ``` #[func(title = "Natural Logarithm")] pub fn ln( - /// The callsite span. span: Span, /// The number whose logarithm to calculate. Must be strictly positive. value: Spanned, @@ -782,7 +778,6 @@ pub fn round( /// ``` #[func] pub fn clamp( - /// The callsite span. span: Span, /// The number to clamp. value: DecNum, @@ -815,7 +810,6 @@ pub fn clamp( /// ``` #[func(title = "Minimum")] pub fn min( - /// The callsite span. span: Span, /// The sequence of values from which to extract the minimum. /// Must not be empty. @@ -833,7 +827,6 @@ pub fn min( /// ``` #[func(title = "Maximum")] pub fn max( - /// The callsite span. span: Span, /// The sequence of values from which to extract the maximum. /// Must not be empty. @@ -911,7 +904,6 @@ pub fn odd( /// ``` #[func(title = "Remainder")] pub fn rem( - /// The span of the function call. span: Span, /// The dividend of the remainder. dividend: DecNum, @@ -950,7 +942,6 @@ pub fn rem( /// ``` #[func(title = "Euclidean Division")] pub fn div_euclid( - /// The callsite span. span: Span, /// The dividend of the division. dividend: DecNum, @@ -994,7 +985,6 @@ pub fn div_euclid( /// ``` #[func(title = "Euclidean Remainder", keywords = ["modulo", "modulus"])] pub fn rem_euclid( - /// The callsite span. span: Span, /// The dividend of the remainder. dividend: DecNum, @@ -1031,7 +1021,6 @@ pub fn rem_euclid( /// ``` #[func(title = "Quotient")] pub fn quo( - /// The span of the function call. span: Span, /// The dividend of the quotient. dividend: DecNum, diff --git a/crates/typst-library/src/foundations/cast.rs b/crates/typst-library/src/foundations/cast.rs index 84f38f36e..38f409c67 100644 --- a/crates/typst-library/src/foundations/cast.rs +++ b/crates/typst-library/src/foundations/cast.rs @@ -13,7 +13,9 @@ use typst_syntax::{Span, Spanned}; use unicode_math_class::MathClass; use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult}; -use crate::foundations::{array, repr, NativeElement, Packed, Repr, Str, Type, Value}; +use crate::foundations::{ + array, repr, Fold, NativeElement, Packed, Repr, Str, Type, Value, +}; /// Determine details of a type. /// @@ -497,3 +499,58 @@ cast! { /// An operator that can be both unary or binary like `+`. "vary" => MathClass::Vary, } + +/// A type that contains a user-visible source portion and something that is +/// derived from it, but not user-visible. +/// +/// An example usage would be `source` being a `DataSource` and `derived` a +/// TextMate theme parsed from it. With `Derived`, we can store both parts in +/// the `RawElem::theme` field and get automatic nice `Reflect` and `IntoValue` +/// impls. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Derived { + /// The source portion. + pub source: S, + /// The derived portion. + pub derived: D, +} + +impl Derived { + /// Create a new instance from the `source` and the `derived` data. + pub fn new(source: S, derived: D) -> Self { + Self { source, derived } + } +} + +impl Reflect for Derived { + fn input() -> CastInfo { + S::input() + } + + fn output() -> CastInfo { + S::output() + } + + fn castable(value: &Value) -> bool { + S::castable(value) + } + + fn error(found: &Value) -> HintedString { + S::error(found) + } +} + +impl IntoValue for Derived { + fn into_value(self) -> Value { + self.source.into_value() + } +} + +impl Fold for Derived { + fn fold(self, outer: Self) -> Self { + Self { + source: self.source.fold(outer.source), + derived: self.derived.fold(outer.derived), + } + } +} diff --git a/crates/typst-library/src/foundations/content.rs b/crates/typst-library/src/foundations/content.rs index ab2f68ac2..76cd6a222 100644 --- a/crates/typst-library/src/foundations/content.rs +++ b/crates/typst-library/src/foundations/content.rs @@ -9,7 +9,6 @@ use std::sync::Arc; use comemo::Tracked; use ecow::{eco_format, EcoString}; use serde::{Serialize, Serializer}; -use smallvec::smallvec; use typst_syntax::Span; use typst_utils::{fat, singleton, LazyHash, SmallBitSet}; @@ -500,7 +499,7 @@ impl Content { /// Link the content somewhere. pub fn linked(self, dest: Destination) -> Self { - self.styled(LinkElem::set_dests(smallvec![dest])) + self.styled(LinkElem::set_current(Some(dest))) } /// Set alignments for this content. diff --git a/crates/typst-library/src/foundations/datetime.rs b/crates/typst-library/src/foundations/datetime.rs index d15cd417a..2fc48a521 100644 --- a/crates/typst-library/src/foundations/datetime.rs +++ b/crates/typst-library/src/foundations/datetime.rs @@ -318,7 +318,6 @@ impl Datetime { /// ``` #[func] pub fn today( - /// The engine. engine: &mut Engine, /// An offset to apply to the current UTC date. If set to `{auto}`, the /// offset will be the local offset. diff --git a/crates/typst-library/src/foundations/float.rs b/crates/typst-library/src/foundations/float.rs index c3d4e0e73..fcc46b034 100644 --- a/crates/typst-library/src/foundations/float.rs +++ b/crates/typst-library/src/foundations/float.rs @@ -163,18 +163,14 @@ impl f64 { size: u32, ) -> StrResult { Ok(match size { - 8 => match endian { + 8 => Bytes::new(match endian { Endianness::Little => self.to_le_bytes(), Endianness::Big => self.to_be_bytes(), - } - .as_slice() - .into(), - 4 => match endian { + }), + 4 => Bytes::new(match endian { Endianness::Little => (self as f32).to_le_bytes(), Endianness::Big => (self as f32).to_be_bytes(), - } - .as_slice() - .into(), + }), _ => bail!("size must be either 4 or 8"), }) } diff --git a/crates/typst-library/src/foundations/func.rs b/crates/typst-library/src/foundations/func.rs index 40c826df9..cb3eba161 100644 --- a/crates/typst-library/src/foundations/func.rs +++ b/crates/typst-library/src/foundations/func.rs @@ -334,8 +334,6 @@ impl Func { #[func] pub fn with( self, - /// The real arguments (the other argument is just for the docs). - /// The docs argument cannot be called `args`. args: &mut Args, /// The arguments to apply to the function. #[external] @@ -361,8 +359,6 @@ impl Func { #[func] pub fn where_( self, - /// The real arguments (the other argument is just for the docs). - /// The docs argument cannot be called `args`. args: &mut Args, /// The fields to filter for. #[variadic] diff --git a/crates/typst-library/src/foundations/int.rs b/crates/typst-library/src/foundations/int.rs index bddffada3..83a89bf8a 100644 --- a/crates/typst-library/src/foundations/int.rs +++ b/crates/typst-library/src/foundations/int.rs @@ -1,6 +1,7 @@ use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError}; use ecow::{eco_format, EcoString}; +use smallvec::SmallVec; use crate::diag::{bail, StrResult}; use crate::foundations::{ @@ -322,7 +323,7 @@ impl i64 { Endianness::Little => self.to_le_bytes(), }; - let mut buf = vec![0u8; size]; + let mut buf = SmallVec::<[u8; 8]>::from_elem(0, size); match endian { Endianness::Big => { // Copy the bytes from the array to the buffer, starting from @@ -339,7 +340,7 @@ impl i64 { } } - Bytes::from(buf) + Bytes::new(buf) } } diff --git a/crates/typst-library/src/foundations/mod.rs b/crates/typst-library/src/foundations/mod.rs index d960a666c..2c3730d53 100644 --- a/crates/typst-library/src/foundations/mod.rs +++ b/crates/typst-library/src/foundations/mod.rs @@ -266,7 +266,6 @@ impl assert { /// ``` #[func(title = "Evaluate")] pub fn eval( - /// The engine. engine: &mut Engine, /// A string of Typst code to evaluate. source: Spanned, diff --git a/crates/typst-library/src/foundations/ops.rs b/crates/typst-library/src/foundations/ops.rs index 8d12966bf..7dbdde8ff 100644 --- a/crates/typst-library/src/foundations/ops.rs +++ b/crates/typst-library/src/foundations/ops.rs @@ -6,7 +6,9 @@ use ecow::eco_format; use typst_utils::Numeric; use crate::diag::{bail, HintedStrResult, StrResult}; -use crate::foundations::{format_str, Datetime, IntoValue, Regex, Repr, Value}; +use crate::foundations::{ + format_str, Datetime, IntoValue, Regex, Repr, SymbolElem, Value, +}; use crate::layout::{Alignment, Length, Rel}; use crate::text::TextElem; use crate::visualize::Stroke; @@ -30,12 +32,13 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult { (Symbol(a), Str(b)) => Str(format_str!("{a}{b}")), (Bytes(a), Bytes(b)) => Bytes(a + b), (Content(a), Content(b)) => Content(a + b), - (Content(a), Symbol(b)) => Content(a + TextElem::packed(b.get())), + (Content(a), Symbol(b)) => Content(a + SymbolElem::packed(b.get())), (Content(a), Str(b)) => Content(a + TextElem::packed(b)), (Str(a), Content(b)) => Content(TextElem::packed(a) + b), - (Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b), + (Symbol(a), Content(b)) => Content(SymbolElem::packed(a.get()) + b), (Array(a), Array(b)) => Array(a + b), (Dict(a), Dict(b)) => Dict(a + b), + (Args(a), Args(b)) => Args(a + b), (a, b) => mismatch!("cannot join {} with {}", a, b), }) } @@ -129,13 +132,14 @@ pub fn add(lhs: Value, rhs: Value) -> HintedStrResult { (Symbol(a), Str(b)) => Str(format_str!("{a}{b}")), (Bytes(a), Bytes(b)) => Bytes(a + b), (Content(a), Content(b)) => Content(a + b), - (Content(a), Symbol(b)) => Content(a + TextElem::packed(b.get())), + (Content(a), Symbol(b)) => Content(a + SymbolElem::packed(b.get())), (Content(a), Str(b)) => Content(a + TextElem::packed(b)), (Str(a), Content(b)) => Content(TextElem::packed(a) + b), - (Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b), + (Symbol(a), Content(b)) => Content(SymbolElem::packed(a.get()) + b), (Array(a), Array(b)) => Array(a + b), (Dict(a), Dict(b)) => Dict(a + b), + (Args(a), Args(b)) => Args(a + b), (Color(color), Length(thickness)) | (Length(thickness), Color(color)) => { Stroke::from_pair(color, thickness).into_value() diff --git a/crates/typst-library/src/foundations/plugin.rs b/crates/typst-library/src/foundations/plugin.rs index f57257a45..d41261edc 100644 --- a/crates/typst-library/src/foundations/plugin.rs +++ b/crates/typst-library/src/foundations/plugin.rs @@ -9,7 +9,7 @@ use wasmi::{AsContext, AsContextMut}; use crate::diag::{bail, At, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{func, repr, scope, ty, Bytes}; -use crate::World; +use crate::loading::{DataSource, Load}; /// A WebAssembly plugin. /// @@ -152,17 +152,14 @@ impl Plugin { /// Creates a new plugin from a WebAssembly file. #[func(constructor)] pub fn construct( - /// The engine. engine: &mut Engine, - /// Path to a WebAssembly file. + /// A path to a WebAssembly file or raw WebAssembly bytes. /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// For more details about paths, see the [Paths section]($syntax/#paths). + source: Spanned, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - Plugin::new(data).at(span) + let data = source.load(engine.world)?; + Plugin::new(data).at(source.span) } } @@ -293,7 +290,7 @@ impl Plugin { _ => bail!("plugin did not respect the protocol"), }; - Ok(output.into()) + Ok(Bytes::new(output)) } /// An iterator over all the function names defined by the plugin. diff --git a/crates/typst-library/src/foundations/str.rs b/crates/typst-library/src/foundations/str.rs index 8ac99ac63..551ac04f5 100644 --- a/crates/typst-library/src/foundations/str.rs +++ b/crates/typst-library/src/foundations/str.rs @@ -425,9 +425,7 @@ impl Str { #[func] pub fn replace( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The pattern to search for. pattern: StrPattern, @@ -577,9 +575,10 @@ impl Str { /// of the resulting parts. /// /// When the empty string is used as a separator, it separates every - /// character in the string, along with the beginning and end of the - /// string. In practice, this means that the resulting list of parts - /// will contain the empty string at the start and end of the list. + /// character (i.e., Unicode code point) in the string, along with the + /// beginning and end of the string. In practice, this means that the + /// resulting list of parts will contain the empty string at the start + /// and end of the list. #[func] pub fn split( &self, @@ -783,11 +782,7 @@ cast! { v: f64 => Self::Str(repr::display_float(v).into()), v: Decimal => Self::Str(format_str!("{}", v)), v: Version => Self::Str(format_str!("{}", v)), - v: Bytes => Self::Str( - std::str::from_utf8(&v) - .map_err(|_| "bytes are not valid utf-8")? - .into() - ), + v: Bytes => Self::Str(v.to_str().map_err(|_| "bytes are not valid utf-8")?), v: Label => Self::Str(v.resolve().as_str().into()), v: Type => Self::Str(v.long_name().into()), v: Str => Self::Str(v), diff --git a/crates/typst-library/src/foundations/styles.rs b/crates/typst-library/src/foundations/styles.rs index 7354719e9..983803300 100644 --- a/crates/typst-library/src/foundations/styles.rs +++ b/crates/typst-library/src/foundations/styles.rs @@ -12,7 +12,8 @@ use typst_utils::LazyHash; use crate::diag::{SourceResult, Trace, Tracepoint}; use crate::engine::Engine; use crate::foundations::{ - cast, ty, Content, Context, Element, Func, NativeElement, Repr, Selector, + cast, ty, Content, Context, Element, Func, NativeElement, OneOrMultiple, Repr, + Selector, }; use crate::text::{FontFamily, FontList, TextElem}; @@ -775,107 +776,6 @@ impl<'a> Iterator for Links<'a> { } } -/// A sequence of elements with associated styles. -#[derive(Clone, PartialEq, Hash)] -pub struct StyleVec { - /// The elements themselves. - elements: EcoVec, - /// A run-length encoded list of style lists. - /// - /// Each element is a (styles, count) pair. Any elements whose - /// style falls after the end of this list is considered to - /// have an empty style list. - styles: EcoVec<(Styles, usize)>, -} - -impl StyleVec { - /// Create a style vector from an unstyled vector content. - pub fn wrap(elements: EcoVec) -> Self { - Self { elements, styles: EcoVec::new() } - } - - /// Create a `StyleVec` from a list of content with style chains. - pub fn create<'a>(buf: &[(&'a Content, StyleChain<'a>)]) -> (Self, StyleChain<'a>) { - let trunk = StyleChain::trunk(buf.iter().map(|&(_, s)| s)).unwrap_or_default(); - let depth = trunk.links().count(); - - let mut elements = EcoVec::with_capacity(buf.len()); - let mut styles = EcoVec::<(Styles, usize)>::new(); - let mut last: Option<(StyleChain<'a>, usize)> = None; - - for &(element, chain) in buf { - elements.push(element.clone()); - - if let Some((prev, run)) = &mut last { - if chain == *prev { - *run += 1; - } else { - styles.push((prev.suffix(depth), *run)); - last = Some((chain, 1)); - } - } else { - last = Some((chain, 1)); - } - } - - if let Some((last, run)) = last { - let skippable = styles.is_empty() && last == trunk; - if !skippable { - styles.push((last.suffix(depth), run)); - } - } - - (StyleVec { elements, styles }, trunk) - } - - /// Whether there are no elements. - pub fn is_empty(&self) -> bool { - self.elements.is_empty() - } - - /// The number of elements. - pub fn len(&self) -> usize { - self.elements.len() - } - - /// Iterate over the contained content and style chains. - pub fn iter<'a>( - &'a self, - outer: &'a StyleChain<'_>, - ) -> impl Iterator)> { - static EMPTY: Styles = Styles::new(); - self.elements - .iter() - .zip( - self.styles - .iter() - .flat_map(|(local, count)| std::iter::repeat(local).take(*count)) - .chain(std::iter::repeat(&EMPTY)), - ) - .map(|(element, local)| (element, outer.chain(local))) - } - - /// Get a style property, but only if it is the same for all children of the - /// style vector. - pub fn shared_get( - &self, - styles: StyleChain<'_>, - getter: fn(StyleChain) -> T, - ) -> Option { - let value = getter(styles); - self.styles - .iter() - .all(|(local, _)| getter(styles.chain(local)) == value) - .then_some(value) - } -} - -impl Debug for StyleVec { - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - f.debug_list().entries(&self.elements).finish() - } -} - /// A property that is resolved with other properties from the style chain. pub trait Resolve { /// The type of the resolved output. @@ -939,6 +839,13 @@ impl Fold for SmallVec<[T; N]> { } } +impl Fold for OneOrMultiple { + fn fold(self, mut outer: Self) -> Self { + outer.0.extend(self.0); + outer + } +} + /// A variant of fold for foldable optional (`Option`) values where an inner /// `None` value isn't respected (contrary to `Option`'s usual `Fold` /// implementation, with which folding with an inner `None` always returns diff --git a/crates/typst-library/src/foundations/symbol.rs b/crates/typst-library/src/foundations/symbol.rs index 72800f311..8a80506fe 100644 --- a/crates/typst-library/src/foundations/symbol.rs +++ b/crates/typst-library/src/foundations/symbol.rs @@ -9,7 +9,10 @@ use typst_syntax::{is_ident, Span, Spanned}; use typst_utils::hash128; use crate::diag::{bail, SourceResult, StrResult}; -use crate::foundations::{cast, func, scope, ty, Array, Func, NativeFunc, Repr as _}; +use crate::foundations::{ + cast, elem, func, scope, ty, Array, Content, Func, NativeElement, NativeFunc, Packed, + PlainText, Repr as _, +}; /// A Unicode symbol. /// @@ -187,7 +190,6 @@ impl Symbol { /// ``` #[func(constructor)] pub fn construct( - /// The callsite span. span: Span, /// The variants of the symbol. /// @@ -426,3 +428,31 @@ fn parts(modifiers: &str) -> impl Iterator { fn contained(modifiers: &str, m: &str) -> bool { parts(modifiers).any(|part| part == m) } + +/// A single character. +#[elem(Repr, PlainText)] +pub struct SymbolElem { + /// The symbol's character. + #[required] + pub text: char, // This is called `text` for consistency with `TextElem`. +} + +impl SymbolElem { + /// Create a new packed symbol element. + pub fn packed(text: impl Into) -> Content { + Self::new(text.into()).pack() + } +} + +impl PlainText for Packed { + fn plain_text(&self, text: &mut EcoString) { + text.push(self.text); + } +} + +impl crate::foundations::Repr for SymbolElem { + /// Use a custom repr that matches normal content. + fn repr(&self) -> EcoString { + eco_format!("[{}]", self.text) + } +} diff --git a/crates/typst-library/src/foundations/target.rs b/crates/typst-library/src/foundations/target.rs index b743ea1ab..5841552e4 100644 --- a/crates/typst-library/src/foundations/target.rs +++ b/crates/typst-library/src/foundations/target.rs @@ -30,9 +30,6 @@ pub struct TargetElem { /// Returns the current compilation target. #[func(contextual)] -pub fn target( - /// The callsite context. - context: Tracked, -) -> HintedStrResult { +pub fn target(context: Tracked) -> HintedStrResult { Ok(TargetElem::target_in(context.styles()?)) } diff --git a/crates/typst-library/src/foundations/value.rs b/crates/typst-library/src/foundations/value.rs index eb0d6eedc..8d9f59332 100644 --- a/crates/typst-library/src/foundations/value.rs +++ b/crates/typst-library/src/foundations/value.rs @@ -16,7 +16,7 @@ use crate::foundations::{ fields, ops, repr, Args, Array, AutoValue, Bytes, CastInfo, Content, Datetime, Decimal, Dict, Duration, Fold, FromValue, Func, IntoValue, Label, Module, NativeElement, NativeType, NoneValue, Plugin, Reflect, Repr, Resolve, Scope, Str, - Styles, Symbol, Type, Version, + Styles, Symbol, SymbolElem, Type, Version, }; use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel}; use crate::text::{RawContent, RawElem, TextElem}; @@ -209,7 +209,7 @@ impl Value { Self::Decimal(v) => TextElem::packed(eco_format!("{v}")), Self::Str(v) => TextElem::packed(v), Self::Version(v) => TextElem::packed(eco_format!("{v}")), - Self::Symbol(v) => TextElem::packed(v.get()), + Self::Symbol(v) => SymbolElem::packed(v.get()), Self::Content(v) => v, Self::Module(module) => module.content(), _ => RawElem::new(RawContent::Text(self.repr())) @@ -459,15 +459,15 @@ impl<'de> Visitor<'de> for ValueVisitor { } fn visit_bytes(self, v: &[u8]) -> Result { - Ok(Bytes::from(v).into_value()) + Ok(Bytes::new(v.to_vec()).into_value()) } fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result { - Ok(Bytes::from(v).into_value()) + Ok(Bytes::new(v.to_vec()).into_value()) } fn visit_byte_buf(self, v: Vec) -> Result { - Ok(Bytes::from(v).into_value()) + Ok(Bytes::new(v).into_value()) } fn visit_none(self) -> Result { @@ -656,7 +656,7 @@ primitive! { Duration: "duration", Duration } primitive! { Content: "content", Content, None => Content::empty(), - Symbol(v) => TextElem::packed(v.get()), + Symbol(v) => SymbolElem::packed(v.get()), Str(v) => TextElem::packed(v) } primitive! { Styles: "styles", Styles } diff --git a/crates/typst-library/src/html/dom.rs b/crates/typst-library/src/html/dom.rs index 5b6eab4d6..1b725d543 100644 --- a/crates/typst-library/src/html/dom.rs +++ b/crates/typst-library/src/html/dom.rs @@ -210,7 +210,10 @@ impl HtmlAttr { /// Creates a compile-time constant `HtmlAttr`. /// - /// Should only be used in const contexts because it can panic. + /// 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() { @@ -472,17 +475,55 @@ pub mod tag { wbr } + /// Whether this is a void tag whose associated element may not have a + /// 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::param + | 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. - /// - /// If this is true, then pretty-printing can insert spaces around such - /// nodes and around the contents of such nodes. - /// - /// However, when users change the properties of such tags via CSS, the - /// insertion of whitespace may actually impact the visual output; for - /// example, shows how - /// adding CSS rules to `

` can make it sensitive to whitespace. In such - /// cases, users should disable pretty-printing. pub fn is_block_by_default(tag: HtmlTag) -> bool { matches!( tag, @@ -569,42 +610,29 @@ pub mod tag { ) } - /// Whether this is a void tag whose associated element may not have a - /// children. - pub fn is_void(tag: HtmlTag) -> bool { + /// 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::area - | self::base - | self::br + self::table + | self::thead + | self::tbody + | self::tfoot + | self::tr + | self::th + | self::td + | self::caption | self::col - | self::embed - | self::hr - | self::img - | self::input - | self::link - | self::meta - | self::param - | self::source - | self::track - | self::wbr + | self::colgroup ) } - - /// 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) - } } /// Predefined constants for HTML attributes. /// /// Note: These are very incomplete. +#[allow(non_upper_case_globals)] pub mod attr { use super::HtmlAttr; @@ -619,13 +647,18 @@ pub mod attr { attrs! { charset + cite + colspan content href name - value + reversed role + rowspan + start + style + value } - #[allow(non_upper_case_globals)] pub const aria_level: HtmlAttr = HtmlAttr::constant("aria-level"); } diff --git a/crates/typst-library/src/introspection/counter.rs b/crates/typst-library/src/introspection/counter.rs index e189103d9..5432df238 100644 --- a/crates/typst-library/src/introspection/counter.rs +++ b/crates/typst-library/src/introspection/counter.rs @@ -428,11 +428,8 @@ impl Counter { #[func(contextual)] pub fn get( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, ) -> SourceResult { let loc = context.location().at(span)?; @@ -444,11 +441,8 @@ impl Counter { #[func(contextual)] pub fn display( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The call span of the display. span: Span, /// A [numbering pattern or a function]($numbering), which specifies how /// to display the counter. If given a function, that function receives @@ -482,11 +476,8 @@ impl Counter { #[func(contextual)] pub fn at( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, /// The place at which the counter's value should be retrieved. selector: LocatableSelector, @@ -500,11 +491,8 @@ impl Counter { #[func(contextual)] pub fn final_( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, ) -> SourceResult { context.introspect().at(span)?; @@ -528,7 +516,6 @@ impl Counter { #[func] pub fn step( self, - /// The call span of the update. span: Span, /// The depth at which to step the counter. Defaults to `{1}`. #[named] @@ -545,7 +532,6 @@ impl Counter { #[func] pub fn update( self, - /// The call span of the update. span: Span, /// If given an integer or array of integers, sets the counter to that /// value. If given a function, that function receives the previous @@ -800,7 +786,7 @@ impl ManualPageCounter { let Some(elem) = elem.to_packed::() else { continue; }; - if *elem.key() == CounterKey::Page { + if elem.key == CounterKey::Page { let mut state = CounterState(smallvec![self.logical]); state.update(engine, elem.update.clone())?; self.logical = state.first(); diff --git a/crates/typst-library/src/introspection/here.rs b/crates/typst-library/src/introspection/here.rs index 9d6133816..510093247 100644 --- a/crates/typst-library/src/introspection/here.rs +++ b/crates/typst-library/src/introspection/here.rs @@ -44,9 +44,6 @@ use crate::introspection::Location; /// ``` /// Refer to the [`selector`] type for more details on before/after selectors. #[func(contextual)] -pub fn here( - /// The callsite context. - context: Tracked, -) -> HintedStrResult { +pub fn here(context: Tracked) -> HintedStrResult { context.location() } diff --git a/crates/typst-library/src/introspection/locate.rs b/crates/typst-library/src/introspection/locate.rs index f6631b021..50f217851 100644 --- a/crates/typst-library/src/introspection/locate.rs +++ b/crates/typst-library/src/introspection/locate.rs @@ -24,9 +24,7 @@ use crate::introspection::Location; /// ``` #[func(contextual)] pub fn locate( - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// A selector that should match exactly one element. This element will be /// located. diff --git a/crates/typst-library/src/introspection/query.rs b/crates/typst-library/src/introspection/query.rs index f616208c5..b742ac010 100644 --- a/crates/typst-library/src/introspection/query.rs +++ b/crates/typst-library/src/introspection/query.rs @@ -136,9 +136,7 @@ use crate::foundations::{func, Array, Context, LocatableSelector, Value}; /// ``` #[func(contextual)] pub fn query( - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// Can be /// - an element function like a `heading` or `figure`, diff --git a/crates/typst-library/src/introspection/state.rs b/crates/typst-library/src/introspection/state.rs index 7e019e6c7..cc3f566b5 100644 --- a/crates/typst-library/src/introspection/state.rs +++ b/crates/typst-library/src/introspection/state.rs @@ -245,7 +245,7 @@ impl State { for elem in introspector.query(&self.selector()) { let elem = elem.to_packed::().unwrap(); - match elem.update() { + match &elem.update { StateUpdate::Set(value) => state = value.clone(), StateUpdate::Func(func) => { state = func.call(&mut engine, Context::none().track(), [state])? @@ -289,11 +289,8 @@ impl State { #[func(contextual)] pub fn get( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, ) -> SourceResult { let loc = context.location().at(span)?; @@ -309,11 +306,8 @@ impl State { #[func(contextual)] pub fn at( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, /// The place at which the state's value should be retrieved. selector: LocatableSelector, @@ -326,11 +320,8 @@ impl State { #[func(contextual)] pub fn final_( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, ) -> SourceResult { context.introspect().at(span)?; @@ -349,7 +340,6 @@ impl State { #[func] pub fn update( self, - /// The span of the `update` call. span: Span, /// If given a non function-value, sets the state to that value. If /// given a function, that function receives the previous state and has diff --git a/crates/typst-library/src/layout/align.rs b/crates/typst-library/src/layout/align.rs index e8ba4d7c3..5604d6831 100644 --- a/crates/typst-library/src/layout/align.rs +++ b/crates/typst-library/src/layout/align.rs @@ -100,7 +100,7 @@ pub struct AlignElem { 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(styles))) + Ok(self.body.clone().aligned(self.alignment(styles))) } } diff --git a/crates/typst-library/src/layout/container.rs b/crates/typst-library/src/layout/container.rs index 266d1d88f..725f177b7 100644 --- a/crates/typst-library/src/layout/container.rs +++ b/crates/typst-library/src/layout/container.rs @@ -14,9 +14,9 @@ use crate::visualize::{Paint, Stroke}; /// An inline-level container that sizes content. /// /// All elements except inline math, text, and boxes are block-level and cannot -/// occur inside of a paragraph. The box function can be used to integrate such -/// elements into a paragraph. Boxes take the size of their contents by default -/// but can also be sized explicitly. +/// occur inside of a [paragraph]($par). The box function can be used to +/// integrate such elements into a paragraph. Boxes take the size of their +/// contents by default but can also be sized explicitly. /// /// # Example /// ```example @@ -166,7 +166,7 @@ impl Packed { styles: StyleChain, region: Size, ) -> SourceResult> { - self.body().call(engine, locator, styles, region) + self.body.call(engine, locator, styles, region) } } @@ -184,6 +184,10 @@ pub enum InlineItem { /// Such a container can be used to separate content, size it, and give it a /// background or border. /// +/// Blocks are also the primary way to control whether text becomes part of a +/// paragraph or not. See [the paragraph documentation]($par/#what-becomes-a-paragraph) +/// for more details. +/// /// # Examples /// With a block, you can give a background to content while still allowing it /// to break across multiple pages. diff --git a/crates/typst-library/src/layout/frame.rs b/crates/typst-library/src/layout/frame.rs index e57eb27e8..a26a7d0ef 100644 --- a/crates/typst-library/src/layout/frame.rs +++ b/crates/typst-library/src/layout/frame.rs @@ -4,16 +4,13 @@ use std::fmt::{self, Debug, Formatter}; use std::num::NonZeroUsize; use std::sync::Arc; -use smallvec::SmallVec; use typst_syntax::Span; use typst_utils::{LazyHash, Numeric}; -use crate::foundations::{cast, dict, Dict, Label, StyleChain, Value}; +use crate::foundations::{cast, dict, Dict, Label, Value}; use crate::introspection::{Location, Tag}; -use crate::layout::{ - Abs, Axes, FixedAlignment, HideElem, Length, Point, Size, Transform, -}; -use crate::model::{Destination, LinkElem}; +use crate::layout::{Abs, Axes, FixedAlignment, Length, Point, Size, Transform}; +use crate::model::Destination; use crate::text::TextItem; use crate::visualize::{Color, Curve, FixedStroke, Geometry, Image, Paint, Shape}; @@ -304,49 +301,6 @@ impl Frame { } } - /// Apply late-stage properties from the style chain to this frame. This - /// includes: - /// - `HideElem::hidden` - /// - `LinkElem::dests` - /// - /// This must be called on all frames produced by elements - /// that manually handle styles (because their children can have varying - /// styles). This currently includes flow, par, and equation. - /// - /// Other elements don't manually need to handle it because their parents - /// that result from realization will take care of it and the styles can - /// only apply to them as a whole, not part of it (because they don't manage - /// styles). - pub fn post_processed(mut self, styles: StyleChain) -> Self { - self.post_process(styles); - self - } - - /// Post process in place. - pub fn post_process(&mut self, styles: StyleChain) { - if !self.is_empty() { - self.post_process_raw( - LinkElem::dests_in(styles), - HideElem::hidden_in(styles), - ); - } - } - - /// Apply raw late-stage properties from the raw data. - pub fn post_process_raw(&mut self, dests: SmallVec<[Destination; 1]>, hide: bool) { - if !self.is_empty() { - let size = self.size; - self.push_multiple( - dests - .into_iter() - .map(|dest| (Point::zero(), FrameItem::Link(dest, size))), - ); - if hide { - self.hide(); - } - } - } - /// Hide all content in the frame, but keep metadata. pub fn hide(&mut self) { Arc::make_mut(&mut self.items).retain_mut(|(_, item)| match item { diff --git a/crates/typst-library/src/layout/grid.rs b/crates/typst-library/src/layout/grid/mod.rs similarity index 99% rename from crates/typst-library/src/layout/grid.rs rename to crates/typst-library/src/layout/grid/mod.rs index 2e1e9abc4..6616c3311 100644 --- a/crates/typst-library/src/layout/grid.rs +++ b/crates/typst-library/src/layout/grid/mod.rs @@ -1,3 +1,5 @@ +pub mod resolve; + use std::num::NonZeroUsize; use std::sync::Arc; @@ -747,7 +749,7 @@ cast! { impl Show for Packed { fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult { - show_grid_cell(self.body().clone(), self.inset(styles), self.align(styles)) + show_grid_cell(self.body.clone(), self.inset(styles), self.align(styles)) } } diff --git a/crates/typst-layout/src/grid/cells.rs b/crates/typst-library/src/layout/grid/resolve.rs similarity index 77% rename from crates/typst-layout/src/grid/cells.rs rename to crates/typst-library/src/layout/grid/resolve.rs index 175e21833..f6df57a37 100644 --- a/crates/typst-layout/src/grid/cells.rs +++ b/crates/typst-library/src/layout/grid/resolve.rs @@ -2,19 +2,463 @@ use std::num::NonZeroUsize; use std::sync::Arc; use ecow::eco_format; -use typst_library::diag::{bail, At, Hint, HintedStrResult, HintedString, SourceResult}; +use typst_library::diag::{ + bail, At, Hint, HintedStrResult, HintedString, SourceResult, Trace, Tracepoint, +}; use typst_library::engine::Engine; -use typst_library::foundations::{Content, Smart, StyleChain}; +use typst_library::foundations::{Content, Fold, Packed, Smart, StyleChain}; use typst_library::introspection::Locator; use typst_library::layout::{ - Abs, Alignment, Axes, Celled, Fragment, Length, Regions, Rel, ResolvedCelled, Sides, - Sizing, + Abs, Alignment, Axes, Celled, GridCell, GridChild, GridElem, GridItem, Length, + OuterHAlignment, OuterVAlignment, Rel, ResolvedCelled, Sides, Sizing, }; +use typst_library::model::{TableCell, TableChild, TableElem, TableItem}; +use typst_library::text::TextElem; use typst_library::visualize::{Paint, Stroke}; +use typst_library::Dir; + use typst_syntax::Span; use typst_utils::NonZeroExt; -use super::{Footer, Header, Line, Repeatable}; +/// Convert a grid to a cell grid. +#[typst_macros::time(span = elem.span())] +pub fn grid_to_cellgrid<'a>( + elem: &Packed, + engine: &mut Engine, + locator: Locator<'a>, + styles: StyleChain, +) -> SourceResult> { + let inset = elem.inset(styles); + let align = elem.align(styles); + let columns = elem.columns(styles); + let rows = elem.rows(styles); + let column_gutter = elem.column_gutter(styles); + let row_gutter = elem.row_gutter(styles); + let fill = elem.fill(styles); + let stroke = elem.stroke(styles); + + let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); + let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice()); + // Use trace to link back to the grid when a specific cell errors + let tracepoint = || Tracepoint::Call(Some(eco_format!("grid"))); + let resolve_item = |item: &GridItem| grid_item_to_resolvable(item, styles); + let children = elem.children.iter().map(|child| match child { + GridChild::Header(header) => ResolvableGridChild::Header { + repeat: header.repeat(styles), + span: header.span(), + items: header.children.iter().map(resolve_item), + }, + GridChild::Footer(footer) => ResolvableGridChild::Footer { + repeat: footer.repeat(styles), + span: footer.span(), + items: footer.children.iter().map(resolve_item), + }, + GridChild::Item(item) => { + ResolvableGridChild::Item(grid_item_to_resolvable(item, styles)) + } + }); + CellGrid::resolve( + tracks, + gutter, + locator, + children, + fill, + align, + &inset, + &stroke, + engine, + styles, + elem.span(), + ) + .trace(engine.world, tracepoint, elem.span()) +} + +/// Convert a table to a cell grid. +#[typst_macros::time(span = elem.span())] +pub fn table_to_cellgrid<'a>( + elem: &Packed, + engine: &mut Engine, + locator: Locator<'a>, + styles: StyleChain, +) -> SourceResult> { + let inset = elem.inset(styles); + let align = elem.align(styles); + let columns = elem.columns(styles); + let rows = elem.rows(styles); + let column_gutter = elem.column_gutter(styles); + let row_gutter = elem.row_gutter(styles); + let fill = elem.fill(styles); + let stroke = elem.stroke(styles); + + let tracks = Axes::new(columns.0.as_slice(), rows.0.as_slice()); + let gutter = Axes::new(column_gutter.0.as_slice(), row_gutter.0.as_slice()); + // Use trace to link back to the table when a specific cell errors + let tracepoint = || Tracepoint::Call(Some(eco_format!("table"))); + let resolve_item = |item: &TableItem| table_item_to_resolvable(item, styles); + let children = elem.children.iter().map(|child| match child { + TableChild::Header(header) => ResolvableGridChild::Header { + repeat: header.repeat(styles), + span: header.span(), + items: header.children.iter().map(resolve_item), + }, + TableChild::Footer(footer) => ResolvableGridChild::Footer { + repeat: footer.repeat(styles), + span: footer.span(), + items: footer.children.iter().map(resolve_item), + }, + TableChild::Item(item) => { + ResolvableGridChild::Item(table_item_to_resolvable(item, styles)) + } + }); + CellGrid::resolve( + tracks, + gutter, + locator, + children, + fill, + align, + &inset, + &stroke, + engine, + styles, + elem.span(), + ) + .trace(engine.world, tracepoint, elem.span()) +} + +fn grid_item_to_resolvable( + item: &GridItem, + styles: StyleChain, +) -> ResolvableGridItem> { + match item { + GridItem::HLine(hline) => ResolvableGridItem::HLine { + y: hline.y(styles), + start: hline.start(styles), + end: hline.end(styles), + stroke: hline.stroke(styles), + span: hline.span(), + position: match hline.position(styles) { + OuterVAlignment::Top => LinePosition::Before, + OuterVAlignment::Bottom => LinePosition::After, + }, + }, + GridItem::VLine(vline) => ResolvableGridItem::VLine { + x: vline.x(styles), + start: vline.start(styles), + end: vline.end(styles), + stroke: vline.stroke(styles), + span: vline.span(), + position: match vline.position(styles) { + OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => { + LinePosition::After + } + OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => { + LinePosition::Before + } + OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before, + OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After, + }, + }, + GridItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()), + } +} + +fn table_item_to_resolvable( + item: &TableItem, + styles: StyleChain, +) -> ResolvableGridItem> { + match item { + TableItem::HLine(hline) => ResolvableGridItem::HLine { + y: hline.y(styles), + start: hline.start(styles), + end: hline.end(styles), + stroke: hline.stroke(styles), + span: hline.span(), + position: match hline.position(styles) { + OuterVAlignment::Top => LinePosition::Before, + OuterVAlignment::Bottom => LinePosition::After, + }, + }, + TableItem::VLine(vline) => ResolvableGridItem::VLine { + x: vline.x(styles), + start: vline.start(styles), + end: vline.end(styles), + stroke: vline.stroke(styles), + span: vline.span(), + position: match vline.position(styles) { + OuterHAlignment::Left if TextElem::dir_in(styles) == Dir::RTL => { + LinePosition::After + } + OuterHAlignment::Right if TextElem::dir_in(styles) == Dir::RTL => { + LinePosition::Before + } + OuterHAlignment::Start | OuterHAlignment::Left => LinePosition::Before, + OuterHAlignment::End | OuterHAlignment::Right => LinePosition::After, + }, + }, + TableItem::Cell(cell) => ResolvableGridItem::Cell(cell.clone()), + } +} + +impl ResolvableCell for Packed { + fn resolve_cell<'a>( + mut self, + x: usize, + y: usize, + fill: &Option, + align: Smart, + inset: Sides>>, + stroke: Sides>>>>, + breakable: bool, + locator: Locator<'a>, + styles: StyleChain, + ) -> Cell<'a> { + let cell = &mut *self; + let colspan = cell.colspan(styles); + let rowspan = cell.rowspan(styles); + let breakable = cell.breakable(styles).unwrap_or(breakable); + let fill = cell.fill(styles).unwrap_or_else(|| fill.clone()); + + let cell_stroke = cell.stroke(styles); + let stroke_overridden = + cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_)))); + + // Using a typical 'Sides' fold, an unspecified side loses to a + // specified side. Additionally, when both are specified, an inner + // None wins over the outer Some, and vice-versa. When both are + // specified and Some, fold occurs, which, remarkably, leads to an Arc + // clone. + // + // In the end, we flatten because, for layout purposes, an unspecified + // cell stroke is the same as specifying 'none', so we equate the two + // concepts. + let stroke = cell_stroke.fold(stroke).map(Option::flatten); + cell.push_x(Smart::Custom(x)); + cell.push_y(Smart::Custom(y)); + cell.push_fill(Smart::Custom(fill.clone())); + cell.push_align(match align { + Smart::Custom(align) => { + Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align))) + } + // Don't fold if the table is using outer alignment. Use the + // cell's alignment instead (which, in the end, will fold with + // the outer alignment when it is effectively displayed). + Smart::Auto => cell.align(styles), + }); + cell.push_inset(Smart::Custom( + cell.inset(styles).map_or(inset, |inner| inner.fold(inset)), + )); + cell.push_stroke( + // Here we convert the resolved stroke to a regular stroke, however + // with resolved units (that is, 'em' converted to absolute units). + // We also convert any stroke unspecified by both the cell and the + // outer stroke ('None' in the folded stroke) to 'none', that is, + // all sides are present in the resulting Sides object accessible + // by show rules on table cells. + stroke.as_ref().map(|side| { + Some(side.as_ref().map(|cell_stroke| { + Arc::new((**cell_stroke).clone().map(Length::from)) + })) + }), + ); + cell.push_breakable(Smart::Custom(breakable)); + Cell { + body: self.pack(), + locator, + fill, + colspan, + rowspan, + stroke, + stroke_overridden, + breakable, + } + } + + fn x(&self, styles: StyleChain) -> Smart { + (**self).x(styles) + } + + fn y(&self, styles: StyleChain) -> Smart { + (**self).y(styles) + } + + fn colspan(&self, styles: StyleChain) -> NonZeroUsize { + (**self).colspan(styles) + } + + fn rowspan(&self, styles: StyleChain) -> NonZeroUsize { + (**self).rowspan(styles) + } + + fn span(&self) -> Span { + Packed::span(self) + } +} + +impl ResolvableCell for Packed { + fn resolve_cell<'a>( + mut self, + x: usize, + y: usize, + fill: &Option, + align: Smart, + inset: Sides>>, + stroke: Sides>>>>, + breakable: bool, + locator: Locator<'a>, + styles: StyleChain, + ) -> Cell<'a> { + let cell = &mut *self; + let colspan = cell.colspan(styles); + let rowspan = cell.rowspan(styles); + let breakable = cell.breakable(styles).unwrap_or(breakable); + let fill = cell.fill(styles).unwrap_or_else(|| fill.clone()); + + let cell_stroke = cell.stroke(styles); + let stroke_overridden = + cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_)))); + + // Using a typical 'Sides' fold, an unspecified side loses to a + // specified side. Additionally, when both are specified, an inner + // None wins over the outer Some, and vice-versa. When both are + // specified and Some, fold occurs, which, remarkably, leads to an Arc + // clone. + // + // In the end, we flatten because, for layout purposes, an unspecified + // cell stroke is the same as specifying 'none', so we equate the two + // concepts. + let stroke = cell_stroke.fold(stroke).map(Option::flatten); + cell.push_x(Smart::Custom(x)); + cell.push_y(Smart::Custom(y)); + cell.push_fill(Smart::Custom(fill.clone())); + cell.push_align(match align { + Smart::Custom(align) => { + Smart::Custom(cell.align(styles).map_or(align, |inner| inner.fold(align))) + } + // Don't fold if the grid is using outer alignment. Use the + // cell's alignment instead (which, in the end, will fold with + // the outer alignment when it is effectively displayed). + Smart::Auto => cell.align(styles), + }); + cell.push_inset(Smart::Custom( + cell.inset(styles).map_or(inset, |inner| inner.fold(inset)), + )); + cell.push_stroke( + // Here we convert the resolved stroke to a regular stroke, however + // with resolved units (that is, 'em' converted to absolute units). + // We also convert any stroke unspecified by both the cell and the + // outer stroke ('None' in the folded stroke) to 'none', that is, + // all sides are present in the resulting Sides object accessible + // by show rules on grid cells. + stroke.as_ref().map(|side| { + Some(side.as_ref().map(|cell_stroke| { + Arc::new((**cell_stroke).clone().map(Length::from)) + })) + }), + ); + cell.push_breakable(Smart::Custom(breakable)); + Cell { + body: self.pack(), + locator, + fill, + colspan, + rowspan, + stroke, + stroke_overridden, + breakable, + } + } + + fn x(&self, styles: StyleChain) -> Smart { + (**self).x(styles) + } + + fn y(&self, styles: StyleChain) -> Smart { + (**self).y(styles) + } + + fn colspan(&self, styles: StyleChain) -> NonZeroUsize { + (**self).colspan(styles) + } + + fn rowspan(&self, styles: StyleChain) -> NonZeroUsize { + (**self).rowspan(styles) + } + + fn span(&self) -> Span { + Packed::span(self) + } +} + +/// Represents an explicit grid line (horizontal or vertical) specified by the +/// user. +pub struct Line { + /// The index of the track after this line. This will be the index of the + /// row a horizontal line is above of, or of the column right after a + /// vertical line. + /// + /// Must be within `0..=tracks.len()` (where `tracks` is either `grid.cols` + /// or `grid.rows`, ignoring gutter tracks, as appropriate). + pub index: usize, + /// The index of the track at which this line starts being drawn. + /// This is the first column a horizontal line appears in, or the first row + /// a vertical line appears in. + /// + /// Must be within `0..tracks.len()` minus gutter tracks. + pub start: usize, + /// The index after the last track through which the line is drawn. + /// Thus, the line is drawn through tracks `start..end` (note that `end` is + /// exclusive). + /// + /// Must be within `1..=tracks.len()` minus gutter tracks. + /// `None` indicates the line should go all the way to the end. + pub end: Option, + /// The line's stroke. This is `None` when the line is explicitly used to + /// override a previously specified line. + pub stroke: Option>>, + /// The line's position in relation to the track with its index. + pub position: LinePosition, +} + +/// A repeatable grid header. Starts at the first row. +pub struct Header { + /// The index after the last row included in this header. + pub end: usize, +} + +/// A repeatable grid footer. Stops at the last row. +pub struct Footer { + /// The first row included in this footer. + pub start: usize, +} + +/// A possibly repeatable grid object. +/// It still exists even when not repeatable, but must not have additional +/// considerations by grid layout, other than for consistency (such as making +/// a certain group of rows unbreakable). +pub enum Repeatable { + Repeated(T), + NotRepeated(T), +} + +impl Repeatable { + /// Gets the value inside this repeatable, regardless of whether + /// it repeats. + pub fn unwrap(&self) -> &T { + match self { + Self::Repeated(repeated) => repeated, + Self::NotRepeated(not_repeated) => not_repeated, + } + } + + /// Returns `Some` if the value is repeated, `None` otherwise. + pub fn as_repeated(&self) -> Option<&T> { + match self { + Self::Repeated(repeated) => Some(repeated), + Self::NotRepeated(_) => None, + } + } +} /// Used for cell-like elements which are aware of their final properties in /// the table, and may have property overrides. @@ -131,26 +575,6 @@ impl<'a> Cell<'a> { breakable: true, } } - - /// Layout the cell into the given regions. - /// - /// The `disambiguator` indicates which instance of this cell this should be - /// layouted as. For normal cells, it is always `0`, but for headers and - /// footers, it indicates the index of the header/footer among all. See the - /// [`Locator`] docs for more details on the concepts behind this. - pub fn layout( - &self, - engine: &mut Engine, - disambiguator: usize, - styles: StyleChain, - regions: Regions, - ) -> SourceResult { - let mut locator = self.locator.relayout(); - if disambiguator > 0 { - locator = locator.split().next_inner(disambiguator as u128); - } - crate::layout_fragment(engine, &self.body, locator, styles, regions) - } } /// Indicates whether the line should be drawn before or after the track with @@ -178,7 +602,7 @@ pub enum Entry<'a> { impl<'a> Entry<'a> { /// Obtains the cell inside this entry, if this is not a merged cell. - fn as_cell(&self) -> Option<&Cell<'a>> { + pub fn as_cell(&self) -> Option<&Cell<'a>> { match self { Self::Cell(cell) => Some(cell), Self::Merged { .. } => None, diff --git a/crates/typst-library/src/layout/hide.rs b/crates/typst-library/src/layout/hide.rs index 1b8b9bd57..eca33471a 100644 --- a/crates/typst-library/src/layout/hide.rs +++ b/crates/typst-library/src/layout/hide.rs @@ -29,6 +29,6 @@ pub struct HideElem { impl Show for Packed { #[typst_macros::time(name = "hide", span = self.span())] fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(HideElem::set_hidden(true))) + Ok(self.body.clone().styled(HideElem::set_hidden(true))) } } diff --git a/crates/typst-library/src/layout/layout.rs b/crates/typst-library/src/layout/layout.rs index c3d112e16..cde3187d3 100644 --- a/crates/typst-library/src/layout/layout.rs +++ b/crates/typst-library/src/layout/layout.rs @@ -54,7 +54,6 @@ use crate::layout::{BlockElem, Size}; /// corresponding page dimension is set to `{auto}`. #[func] pub fn layout( - /// The call span of this function. span: Span, /// A function to call with the outer container's size. Its return value is /// displayed in the document. @@ -89,7 +88,7 @@ impl Show for Packed { let loc = elem.location().unwrap(); let context = Context::new(Some(loc), Some(styles)); let result = elem - .func() + .func .call( engine, context.track(), diff --git a/crates/typst-library/src/layout/measure.rs b/crates/typst-library/src/layout/measure.rs index 0c6071eb0..93c48ad40 100644 --- a/crates/typst-library/src/layout/measure.rs +++ b/crates/typst-library/src/layout/measure.rs @@ -43,11 +43,8 @@ use crate::layout::{Abs, Axes, Length, Region, Size}; /// `height`, both of type [`length`]. #[func(contextual)] pub fn measure( - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, /// The width available to layout the content. /// diff --git a/crates/typst-library/src/layout/mod.rs b/crates/typst-library/src/layout/mod.rs index b54d6906e..574a2830a 100644 --- a/crates/typst-library/src/layout/mod.rs +++ b/crates/typst-library/src/layout/mod.rs @@ -12,7 +12,7 @@ mod em; mod fr; mod fragment; mod frame; -mod grid; +pub mod grid; mod hide; #[path = "layout.rs"] mod layout_; diff --git a/crates/typst-library/src/layout/repeat.rs b/crates/typst-library/src/layout/repeat.rs index e423410ab..9579f1856 100644 --- a/crates/typst-library/src/layout/repeat.rs +++ b/crates/typst-library/src/layout/repeat.rs @@ -10,7 +10,7 @@ use crate::layout::{BlockElem, Length}; /// Space may be inserted between the instances of the body parameter, so be /// sure to adjust the [`justify`]($repeat.justify) parameter accordingly. /// -/// Errors if there no bounds on the available space, as it would create +/// Errors if there are no bounds on the available space, as it would create /// infinite content. /// /// # Example diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs index 87b2fcb44..2ea77eaa5 100644 --- a/crates/typst-library/src/lib.rs +++ b/crates/typst-library/src/lib.rs @@ -21,6 +21,7 @@ pub mod layout; pub mod loading; pub mod math; pub mod model; +pub mod pdf; pub mod routines; pub mod symbols; pub mod text; @@ -249,6 +250,7 @@ fn global(math: Module, inputs: Dict, features: &Features) -> Module { self::introspection::define(&mut global); self::loading::define(&mut global); self::symbols::define(&mut global); + self::pdf::define(&mut global); global.reset_category(); if features.is_enabled(Feature::Html) { global.define_module(self::html::module()); diff --git a/crates/typst-library/src/loading/cbor.rs b/crates/typst-library/src/loading/cbor.rs index 977059c3d..2bdeb80ef 100644 --- a/crates/typst-library/src/loading/cbor.rs +++ b/crates/typst-library/src/loading/cbor.rs @@ -1,10 +1,10 @@ -use ecow::{eco_format, EcoString}; +use ecow::eco_format; use typst_syntax::Spanned; use crate::diag::{At, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Bytes, Value}; -use crate::World; +use crate::loading::{DataSource, Load}; /// Reads structured data from a CBOR file. /// @@ -19,31 +19,31 @@ use crate::World; /// floating point numbers, which may result in an approximative value. #[func(scope, title = "CBOR")] pub fn cbor( - /// The engine. engine: &mut Engine, - /// Path to a CBOR file. + /// A path to a CBOR file or raw CBOR bytes. /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// For more details about paths, see the [Paths section]($syntax/#paths). + source: Spanned, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - cbor::decode(Spanned::new(data, span)) + let data = source.load(engine.world)?; + ciborium::from_reader(data.as_slice()) + .map_err(|err| eco_format!("failed to parse CBOR ({err})")) + .at(source.span) } #[scope] impl cbor { /// Reads structured data from CBOR bytes. + /// + /// This function is deprecated. The [`cbor`] function now accepts bytes + /// directly. #[func(title = "Decode CBOR")] pub fn decode( - /// cbor data. + engine: &mut Engine, + /// CBOR data. data: Spanned, ) -> SourceResult { - let Spanned { v: data, span } = data; - ciborium::from_reader(data.as_slice()) - .map_err(|err| eco_format!("failed to parse CBOR ({err})")) - .at(span) + cbor(engine, data.map(DataSource::Bytes)) } /// Encode structured data into CBOR bytes. @@ -55,7 +55,7 @@ impl cbor { let Spanned { v: value, span } = value; let mut res = Vec::new(); ciborium::into_writer(&value, &mut res) - .map(|_| res.into()) + .map(|_| Bytes::new(res)) .map_err(|err| eco_format!("failed to encode value as CBOR ({err})")) .at(span) } diff --git a/crates/typst-library/src/loading/csv.rs b/crates/typst-library/src/loading/csv.rs index 6822505d3..1cf656ae2 100644 --- a/crates/typst-library/src/loading/csv.rs +++ b/crates/typst-library/src/loading/csv.rs @@ -4,8 +4,7 @@ use typst_syntax::Spanned; use crate::diag::{bail, At, SourceResult}; use crate::engine::Engine; use crate::foundations::{cast, func, scope, Array, Dict, IntoValue, Type, Value}; -use crate::loading::Readable; -use crate::World; +use crate::loading::{DataSource, Load, Readable}; /// Reads structured data from a CSV file. /// @@ -26,12 +25,11 @@ use crate::World; /// ``` #[func(scope, title = "CSV")] pub fn csv( - /// The engine. engine: &mut Engine, - /// Path to a CSV file. + /// Path to a CSV file or raw CSV bytes. /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// For more details about paths, see the [Paths section]($syntax/#paths). + source: Spanned, /// The delimiter that separates columns in the CSV file. /// Must be a single ASCII character. #[named] @@ -48,17 +46,62 @@ pub fn csv( #[default(RowType::Array)] row_type: RowType, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - self::csv::decode(Spanned::new(Readable::Bytes(data), span), delimiter, row_type) + let data = source.load(engine.world)?; + + let mut builder = ::csv::ReaderBuilder::new(); + let has_headers = row_type == RowType::Dict; + builder.has_headers(has_headers); + builder.delimiter(delimiter.0 as u8); + + // Counting lines from 1 by default. + let mut line_offset: usize = 1; + let mut reader = builder.from_reader(data.as_slice()); + let mut headers: Option<::csv::StringRecord> = None; + + if has_headers { + // Counting lines from 2 because we have a header. + line_offset += 1; + headers = Some( + reader + .headers() + .map_err(|err| format_csv_error(err, 1)) + .at(source.span)? + .clone(), + ); + } + + let mut array = Array::new(); + for (line, result) in reader.records().enumerate() { + // Original solution was to use line from error, but that is + // incorrect with `has_headers` set to `false`. See issue: + // https://github.com/BurntSushi/rust-csv/issues/184 + let line = line + line_offset; + let row = result.map_err(|err| format_csv_error(err, line)).at(source.span)?; + let item = if let Some(headers) = &headers { + let mut dict = Dict::new(); + for (field, value) in headers.iter().zip(&row) { + dict.insert(field.into(), value.into_value()); + } + dict.into_value() + } else { + let sub = row.into_iter().map(|field| field.into_value()).collect(); + Value::Array(sub) + }; + array.push(item); + } + + Ok(array) } #[scope] impl csv { /// Reads structured data from a CSV string/bytes. + /// + /// This function is deprecated. The [`csv`] function now accepts bytes + /// directly. #[func(title = "Decode CSV")] pub fn decode( + engine: &mut Engine, /// CSV data. data: Spanned, /// The delimiter that separates columns in the CSV file. @@ -77,51 +120,7 @@ impl csv { #[default(RowType::Array)] row_type: RowType, ) -> SourceResult { - let Spanned { v: data, span } = data; - let has_headers = row_type == RowType::Dict; - - let mut builder = ::csv::ReaderBuilder::new(); - builder.has_headers(has_headers); - builder.delimiter(delimiter.0 as u8); - - // Counting lines from 1 by default. - let mut line_offset: usize = 1; - let mut reader = builder.from_reader(data.as_slice()); - let mut headers: Option<::csv::StringRecord> = None; - - if has_headers { - // Counting lines from 2 because we have a header. - line_offset += 1; - headers = Some( - reader - .headers() - .map_err(|err| format_csv_error(err, 1)) - .at(span)? - .clone(), - ); - } - - let mut array = Array::new(); - for (line, result) in reader.records().enumerate() { - // Original solution was to use line from error, but that is - // incorrect with `has_headers` set to `false`. See issue: - // https://github.com/BurntSushi/rust-csv/issues/184 - let line = line + line_offset; - let row = result.map_err(|err| format_csv_error(err, line)).at(span)?; - let item = if let Some(headers) = &headers { - let mut dict = Dict::new(); - for (field, value) in headers.iter().zip(&row) { - dict.insert(field.into(), value.into_value()); - } - dict.into_value() - } else { - let sub = row.into_iter().map(|field| field.into_value()).collect(); - Value::Array(sub) - }; - array.push(item); - } - - Ok(array) + csv(engine, data.map(Readable::into_source), delimiter, row_type) } } @@ -137,18 +136,10 @@ impl Default for Delimiter { cast! { Delimiter, self => self.0.into_value(), - v: EcoString => { - let mut chars = v.chars(); - let first = chars.next().ok_or("delimiter must not be empty")?; - if chars.next().is_some() { - bail!("delimiter must be a single character"); - } - - if !first.is_ascii() { - bail!("delimiter must be an ASCII character"); - } - - Self(first) + c: char => if c.is_ascii() { + Self(c) + } else { + bail!("delimiter must be an ASCII character") }, } diff --git a/crates/typst-library/src/loading/json.rs b/crates/typst-library/src/loading/json.rs index 597cf4cc6..035c5e4a7 100644 --- a/crates/typst-library/src/loading/json.rs +++ b/crates/typst-library/src/loading/json.rs @@ -1,11 +1,10 @@ -use ecow::{eco_format, EcoString}; +use ecow::eco_format; use typst_syntax::Spanned; use crate::diag::{At, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Str, Value}; -use crate::loading::Readable; -use crate::World; +use crate::loading::{DataSource, Load, Readable}; /// Reads structured data from a JSON file. /// @@ -51,31 +50,31 @@ use crate::World; /// ``` #[func(scope, title = "JSON")] pub fn json( - /// The engine. engine: &mut Engine, - /// Path to a JSON file. + /// Path to a JSON file or raw JSON bytes. /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// For more details about paths, see the [Paths section]($syntax/#paths). + source: Spanned, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - json::decode(Spanned::new(Readable::Bytes(data), span)) + let data = source.load(engine.world)?; + serde_json::from_slice(data.as_slice()) + .map_err(|err| eco_format!("failed to parse JSON ({err})")) + .at(source.span) } #[scope] impl json { /// Reads structured data from a JSON string/bytes. + /// + /// This function is deprecated. The [`json`] function now accepts bytes + /// directly. #[func(title = "Decode JSON")] pub fn decode( + engine: &mut Engine, /// JSON data. data: Spanned, ) -> SourceResult { - let Spanned { v: data, span } = data; - serde_json::from_slice(data.as_slice()) - .map_err(|err| eco_format!("failed to parse JSON ({err})")) - .at(span) + json(engine, data.map(Readable::into_source)) } /// Encodes structured data into a JSON string. diff --git a/crates/typst-library/src/loading/mod.rs b/crates/typst-library/src/loading/mod.rs index ae74df864..171ae651a 100644 --- a/crates/typst-library/src/loading/mod.rs +++ b/crates/typst-library/src/loading/mod.rs @@ -15,6 +15,10 @@ mod xml_; #[path = "yaml.rs"] mod yaml_; +use comemo::Tracked; +use ecow::EcoString; +use typst_syntax::Spanned; + pub use self::cbor_::*; pub use self::csv_::*; pub use self::json_::*; @@ -23,7 +27,10 @@ pub use self::toml_::*; pub use self::xml_::*; pub use self::yaml_::*; +use crate::diag::{At, SourceResult}; +use crate::foundations::OneOrMultiple; use crate::foundations::{cast, category, Bytes, Category, Scope, Str}; +use crate::World; /// Data loading from external files. /// @@ -44,6 +51,76 @@ pub(super) fn define(global: &mut Scope) { global.define_func::(); } +/// Something we can retrieve byte data from. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum DataSource { + /// A path to a file. + Path(EcoString), + /// Raw bytes. + Bytes(Bytes), +} + +cast! { + DataSource, + self => match self { + Self::Path(v) => v.into_value(), + Self::Bytes(v) => v.into_value(), + }, + v: EcoString => Self::Path(v), + v: Bytes => Self::Bytes(v), +} + +/// Loads data from a path or provided bytes. +pub trait Load { + /// Bytes or a list of bytes (if there are multiple sources). + type Output; + + /// Load the bytes. + fn load(&self, world: Tracked) -> SourceResult; +} + +impl Load for Spanned { + type Output = Bytes; + + fn load(&self, world: Tracked) -> SourceResult { + self.as_ref().load(world) + } +} + +impl Load for Spanned<&DataSource> { + type Output = Bytes; + + fn load(&self, world: Tracked) -> SourceResult { + match &self.v { + DataSource::Path(path) => { + let file_id = self.span.resolve_path(path).at(self.span)?; + world.file(file_id).at(self.span) + } + DataSource::Bytes(bytes) => Ok(bytes.clone()), + } + } +} + +impl Load for Spanned> { + type Output = Vec; + + fn load(&self, world: Tracked) -> SourceResult> { + self.as_ref().load(world) + } +} + +impl Load for Spanned<&OneOrMultiple> { + type Output = Vec; + + fn load(&self, world: Tracked) -> SourceResult> { + self.v + .0 + .iter() + .map(|source| Spanned::new(source, self.span).load(world)) + .collect() + } +} + /// A value that can be read from a file. #[derive(Debug, Clone, PartialEq, Hash)] pub enum Readable { @@ -54,18 +131,15 @@ pub enum Readable { } impl Readable { - pub fn as_slice(&self) -> &[u8] { + pub fn into_bytes(self) -> Bytes { match self { - Readable::Bytes(v) => v, - Readable::Str(v) => v.as_bytes(), + Self::Bytes(v) => v, + Self::Str(v) => Bytes::from_string(v), } } - pub fn as_str(&self) -> Option<&str> { - match self { - Readable::Str(v) => Some(v.as_str()), - Readable::Bytes(v) => std::str::from_utf8(v).ok(), - } + pub fn into_source(self) -> DataSource { + DataSource::Bytes(self.into_bytes()) } } @@ -78,12 +152,3 @@ cast! { v: Str => Self::Str(v), v: Bytes => Self::Bytes(v), } - -impl From for Bytes { - fn from(value: Readable) -> Self { - match value { - Readable::Bytes(v) => v, - Readable::Str(v) => v.as_bytes().into(), - } - } -} diff --git a/crates/typst-library/src/loading/read.rs b/crates/typst-library/src/loading/read.rs index 23e6e27e7..32dadc799 100644 --- a/crates/typst-library/src/loading/read.rs +++ b/crates/typst-library/src/loading/read.rs @@ -1,7 +1,7 @@ use ecow::EcoString; use typst_syntax::Spanned; -use crate::diag::{At, SourceResult}; +use crate::diag::{At, FileError, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, Cast}; use crate::loading::Readable; @@ -24,7 +24,6 @@ use crate::World; /// ``` #[func] pub fn read( - /// The engine. engine: &mut Engine, /// Path to a file. /// @@ -42,12 +41,9 @@ pub fn read( let data = engine.world.file(id).at(span)?; Ok(match encoding { None => Readable::Bytes(data), - Some(Encoding::Utf8) => Readable::Str( - std::str::from_utf8(&data) - .map_err(|_| "file is not valid utf-8") - .at(span)? - .into(), - ), + Some(Encoding::Utf8) => { + Readable::Str(data.to_str().map_err(FileError::from).at(span)?) + } }) } diff --git a/crates/typst-library/src/loading/toml.rs b/crates/typst-library/src/loading/toml.rs index 5167703ef..402207b02 100644 --- a/crates/typst-library/src/loading/toml.rs +++ b/crates/typst-library/src/loading/toml.rs @@ -1,11 +1,10 @@ use ecow::{eco_format, EcoString}; use typst_syntax::{is_newline, Spanned}; -use crate::diag::{At, SourceResult}; +use crate::diag::{At, FileError, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Str, Value}; -use crate::loading::Readable; -use crate::World; +use crate::loading::{DataSource, Load, Readable}; /// Reads structured data from a TOML file. /// @@ -29,34 +28,32 @@ use crate::World; /// ``` #[func(scope, title = "TOML")] pub fn toml( - /// The engine. engine: &mut Engine, - /// Path to a TOML file. + /// A path to a TOML file or raw TOML bytes. /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// For more details about paths, see the [Paths section]($syntax/#paths). + source: Spanned, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - toml::decode(Spanned::new(Readable::Bytes(data), span)) + let data = source.load(engine.world)?; + let raw = data.as_str().map_err(FileError::from).at(source.span)?; + ::toml::from_str(raw) + .map_err(|err| format_toml_error(err, raw)) + .at(source.span) } #[scope] impl toml { /// Reads structured data from a TOML string/bytes. + /// + /// This function is deprecated. The [`toml`] function now accepts bytes + /// directly. #[func(title = "Decode TOML")] pub fn decode( + engine: &mut Engine, /// TOML data. data: Spanned, ) -> SourceResult { - let Spanned { v: data, span } = data; - let raw = std::str::from_utf8(data.as_slice()) - .map_err(|_| "file is not valid utf-8") - .at(span)?; - ::toml::from_str(raw) - .map_err(|err| format_toml_error(err, raw)) - .at(span) + toml(engine, data.map(Readable::into_source)) } /// Encodes structured data into a TOML string. diff --git a/crates/typst-library/src/loading/xml.rs b/crates/typst-library/src/loading/xml.rs index 3b1a9674b..ca467c238 100644 --- a/crates/typst-library/src/loading/xml.rs +++ b/crates/typst-library/src/loading/xml.rs @@ -5,8 +5,7 @@ use typst_syntax::Spanned; use crate::diag::{format_xml_like_error, At, FileError, SourceResult}; use crate::engine::Engine; use crate::foundations::{dict, func, scope, Array, Dict, IntoValue, Str, Value}; -use crate::loading::Readable; -use crate::World; +use crate::loading::{DataSource, Load, Readable}; /// Reads structured data from an XML file. /// @@ -58,38 +57,36 @@ use crate::World; /// ``` #[func(scope, title = "XML")] pub fn xml( - /// The engine. engine: &mut Engine, - /// Path to an XML file. + /// A path to an XML file or raw XML bytes. /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// For more details about paths, see the [Paths section]($syntax/#paths). + source: Spanned, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - xml::decode(Spanned::new(Readable::Bytes(data), span)) + let data = source.load(engine.world)?; + let text = data.as_str().map_err(FileError::from).at(source.span)?; + let document = roxmltree::Document::parse_with_options( + text, + ParsingOptions { allow_dtd: true, ..Default::default() }, + ) + .map_err(format_xml_error) + .at(source.span)?; + Ok(convert_xml(document.root())) } #[scope] impl xml { /// Reads structured data from an XML string/bytes. + /// + /// This function is deprecated. The [`xml`] function now accepts bytes + /// directly. #[func(title = "Decode XML")] pub fn decode( + engine: &mut Engine, /// XML data. data: Spanned, ) -> SourceResult { - let Spanned { v: data, span } = data; - let text = std::str::from_utf8(data.as_slice()) - .map_err(FileError::from) - .at(span)?; - let document = roxmltree::Document::parse_with_options( - text, - ParsingOptions { allow_dtd: true, ..Default::default() }, - ) - .map_err(format_xml_error) - .at(span)?; - Ok(convert_xml(document.root())) + xml(engine, data.map(Readable::into_source)) } } diff --git a/crates/typst-library/src/loading/yaml.rs b/crates/typst-library/src/loading/yaml.rs index 0e8ca3fb0..5767cb640 100644 --- a/crates/typst-library/src/loading/yaml.rs +++ b/crates/typst-library/src/loading/yaml.rs @@ -1,11 +1,10 @@ -use ecow::{eco_format, EcoString}; +use ecow::eco_format; use typst_syntax::Spanned; use crate::diag::{At, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Str, Value}; -use crate::loading::Readable; -use crate::World; +use crate::loading::{DataSource, Load, Readable}; /// Reads structured data from a YAML file. /// @@ -41,31 +40,31 @@ use crate::World; /// ``` #[func(scope, title = "YAML")] pub fn yaml( - /// The engine. engine: &mut Engine, - /// Path to a YAML file. + /// A path to a YAML file or raw YAML bytes. /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// For more details about paths, see the [Paths section]($syntax/#paths). + source: Spanned, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - yaml::decode(Spanned::new(Readable::Bytes(data), span)) + let data = source.load(engine.world)?; + serde_yaml::from_slice(data.as_slice()) + .map_err(|err| eco_format!("failed to parse YAML ({err})")) + .at(source.span) } #[scope] impl yaml { /// Reads structured data from a YAML string/bytes. + /// + /// This function is deprecated. The [`yaml`] function now accepts bytes + /// directly. #[func(title = "Decode YAML")] pub fn decode( + engine: &mut Engine, /// YAML data. data: Spanned, ) -> SourceResult { - let Spanned { v: data, span } = data; - serde_yaml::from_slice(data.as_slice()) - .map_err(|err| eco_format!("failed to parse YAML ({err})")) - .at(span) + yaml(engine, data.map(Readable::into_source)) } /// Encode structured data into a YAML string. diff --git a/crates/typst-library/src/math/accent.rs b/crates/typst-library/src/math/accent.rs index fee705ee4..b162c52b1 100644 --- a/crates/typst-library/src/math/accent.rs +++ b/crates/typst-library/src/math/accent.rs @@ -1,8 +1,7 @@ use crate::diag::bail; -use crate::foundations::{cast, elem, func, Content, NativeElement, Value}; +use crate::foundations::{cast, elem, func, Content, NativeElement, SymbolElem}; use crate::layout::{Length, Rel}; use crate::math::Mathy; -use crate::text::TextElem; /// Attaches an accent to a base. /// @@ -142,8 +141,8 @@ cast! { Accent, self => self.0.into_value(), v: char => Self::new(v), - v: Content => match v.to_packed::() { - Some(elem) => Value::Str(elem.text().clone().into()).cast()?, - None => bail!("expected text"), + v: Content => match v.to_packed::() { + Some(elem) => Self::new(elem.text), + None => bail!("expected a symbol"), }, } diff --git a/crates/typst-library/src/math/attach.rs b/crates/typst-library/src/math/attach.rs index e1f577272..d526aba57 100644 --- a/crates/typst-library/src/math/attach.rs +++ b/crates/typst-library/src/math/attach.rs @@ -47,9 +47,9 @@ impl Packed { /// base AttachElem where possible. pub fn merge_base(&self) -> Option { // Extract from an EquationElem. - let mut base = self.base(); + let mut base = &self.base; while let Some(equation) = base.to_packed::() { - base = equation.body(); + base = &equation.body; } // Move attachments from elem into base where possible. diff --git a/crates/typst-library/src/math/equation.rs b/crates/typst-library/src/math/equation.rs index a9173c433..32be216a4 100644 --- a/crates/typst-library/src/math/equation.rs +++ b/crates/typst-library/src/math/equation.rs @@ -20,7 +20,9 @@ use crate::text::{FontFamily, FontList, FontWeight, LocalName, TextElem}; /// A mathematical equation. /// -/// Can be displayed inline with text or as a separate block. +/// Can be displayed inline with text or as a separate block. An equation +/// becomes block-level through the presence of at least one space after the +/// opening dollar sign and one space before the closing dollar sign. /// /// # Example /// ```example @@ -229,35 +231,20 @@ impl Refable for Packed { } impl Outlinable for Packed { - fn outline( - &self, - engine: &mut Engine, - styles: StyleChain, - ) -> SourceResult> { - if !self.block(StyleChain::default()) { - return Ok(None); - } - let Some(numbering) = self.numbering() else { - return Ok(None); - }; - - // After synthesis, this should always be custom content. - let mut supplement = match (**self).supplement(StyleChain::default()) { - Smart::Custom(Some(Supplement::Content(content))) => content, - _ => Content::empty(), - }; + fn outlined(&self) -> bool { + self.block(StyleChain::default()) && self.numbering().is_some() + } + fn prefix(&self, numbers: Content) -> Content { + let supplement = self.supplement(); if !supplement.is_empty() { - supplement += TextElem::packed("\u{a0}"); + supplement + TextElem::packed('\u{a0}') + numbers + } else { + numbers } + } - let numbers = self.counter().display_at_loc( - engine, - self.location().unwrap(), - styles, - numbering, - )?; - - Ok(Some(supplement + numbers)) + fn body(&self) -> Content { + Content::empty() } } diff --git a/crates/typst-library/src/math/lr.rs b/crates/typst-library/src/math/lr.rs index 965f53516..7558717af 100644 --- a/crates/typst-library/src/math/lr.rs +++ b/crates/typst-library/src/math/lr.rs @@ -1,7 +1,6 @@ -use crate::foundations::{elem, func, Content, NativeElement}; +use crate::foundations::{elem, func, Content, NativeElement, SymbolElem}; use crate::layout::{Length, Rel}; use crate::math::Mathy; -use crate::text::TextElem; /// Scales delimiters. /// @@ -19,7 +18,7 @@ pub struct LrElem { #[parse( let mut arguments = args.all::()?.into_iter(); let mut body = arguments.next().unwrap_or_default(); - arguments.for_each(|arg| body += TextElem::packed(',') + arg); + arguments.for_each(|arg| body += SymbolElem::packed(',') + arg); body )] pub body: Content, @@ -125,9 +124,9 @@ fn delimited( ) -> Content { let span = body.span(); let mut elem = LrElem::new(Content::sequence([ - TextElem::packed(left), + SymbolElem::packed(left), body, - TextElem::packed(right), + SymbolElem::packed(right), ])); // Push size only if size is provided if let Some(size) = size { diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs index 5a83c854f..3b4b133d9 100644 --- a/crates/typst-library/src/math/mod.rs +++ b/crates/typst-library/src/math/mod.rs @@ -82,8 +82,9 @@ use crate::text::TextElem; /// - Within them, Typst is still in "math mode". Thus, you can write math /// directly into them, but need to use hash syntax to pass code expressions /// (except for strings, which are available in the math syntax). -/// - They support positional and named arguments, but don't support trailing -/// content blocks and argument spreading. +/// - They support positional and named arguments, as well as argument +/// spreading. +/// - They don't support trailing content blocks. /// - They provide additional syntax for 2-dimensional argument lists. The /// semicolon (`;`) merges preceding arguments separated by commas into an /// array argument. @@ -92,6 +93,7 @@ use crate::text::TextElem; /// $ frac(a^2, 2) $ /// $ vec(1, 2, delim: "[") $ /// $ mat(1, 2; 3, 4) $ +/// $ mat(..#range(1, 5).chunks(2)) $ /// $ lim_x = /// op("lim", limits: #true)_x $ /// ``` diff --git a/crates/typst-library/src/math/op.rs b/crates/typst-library/src/math/op.rs index ef24705a7..55696e534 100644 --- a/crates/typst-library/src/math/op.rs +++ b/crates/typst-library/src/math/op.rs @@ -1,6 +1,6 @@ use ecow::EcoString; -use crate::foundations::{elem, Content, NativeElement, Scope}; +use crate::foundations::{elem, Content, NativeElement, Scope, SymbolElem}; use crate::layout::HElem; use crate::math::{upright, Mathy, THIN}; use crate::text::TextElem; @@ -17,9 +17,9 @@ use crate::text::TextElem; /// # Predefined Operators { #predefined } /// Typst predefines the operators `arccos`, `arcsin`, `arctan`, `arg`, `cos`, /// `cosh`, `cot`, `coth`, `csc`, `csch`, `ctg`, `deg`, `det`, `dim`, `exp`, -/// `gcd`, `hom`, `id`, `im`, `inf`, `ker`, `lg`, `lim`, `liminf`, `limsup`, -/// `ln`, `log`, `max`, `min`, `mod`, `Pr`, `sec`, `sech`, `sin`, `sinc`, -/// `sinh`, `sup`, `tan`, `tanh`, `tg` and `tr`. +/// `gcd`, `lcm`, `hom`, `id`, `im`, `inf`, `ker`, `lg`, `lim`, `liminf`, +/// `limsup`, `ln`, `log`, `max`, `min`, `mod`, `Pr`, `sec`, `sech`, `sin`, +/// `sinc`, `sinh`, `sup`, `tan`, `tanh`, `tg` and `tr`. #[elem(title = "Text Operator", Mathy)] pub struct OpElem { /// The operator's text. @@ -38,6 +38,7 @@ macro_rules! ops { let operator = EcoString::from(ops!(@name $name $(: $value)?)); math.define( stringify!($name), + // Latex also uses their equivalent of `TextElem` here. OpElem::new(TextElem::new(operator).into()) .with_limits(ops!(@limit $($tts)*)) .pack() @@ -46,7 +47,7 @@ macro_rules! ops { let dif = |d| { HElem::new(THIN.into()).with_weak(true).pack() - + upright(TextElem::packed(d)) + + upright(SymbolElem::packed(d)) }; math.define("dif", dif('d')); math.define("Dif", dif('D')); @@ -75,6 +76,7 @@ ops! { dim, exp, gcd (limits), + lcm (limits), hom, id, im, diff --git a/crates/typst-library/src/math/root.rs b/crates/typst-library/src/math/root.rs index e25c6d423..ad111700b 100644 --- a/crates/typst-library/src/math/root.rs +++ b/crates/typst-library/src/math/root.rs @@ -10,7 +10,6 @@ use crate::math::Mathy; /// ``` #[func(title = "Square Root")] pub fn sqrt( - /// The call span of this function. span: Span, /// The expression to take the square root of. radicand: Content, diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index 280ac4a42..a391e5804 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -1,7 +1,7 @@ +use std::any::TypeId; use std::collections::HashMap; use std::ffi::OsStr; use std::fmt::{self, Debug, Formatter}; -use std::hash::{Hash, Hasher}; use std::num::NonZeroUsize; use std::path::Path; use std::sync::{Arc, LazyLock}; @@ -12,33 +12,34 @@ use hayagriva::archive::ArchivedStyle; use hayagriva::io::BibLaTeXError; use hayagriva::{ citationberg, BibliographyDriver, BibliographyRequest, CitationItem, CitationRequest, - SpecificLocator, + Library, SpecificLocator, }; use indexmap::IndexMap; use smallvec::{smallvec, SmallVec}; -use typed_arena::Arena; use typst_syntax::{Span, Spanned}; -use typst_utils::{LazyHash, NonZeroExt, PicoStr}; +use typst_utils::{Get, ManuallyHash, NonZeroExt, PicoStr}; use crate::diag::{bail, error, At, FileError, HintedStrResult, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, ty, Args, Array, Bytes, CastInfo, Content, FromValue, IntoValue, Label, - NativeElement, Packed, Reflect, Repr, Scope, Show, ShowSet, Smart, Str, StyleChain, - Styles, Synthesize, Type, Value, + elem, Bytes, CastInfo, Content, Derived, FromValue, IntoValue, Label, NativeElement, + OneOrMultiple, Packed, Reflect, Scope, Show, ShowSet, Smart, StyleChain, Styles, + Synthesize, Value, }; use crate::introspection::{Introspector, Locatable, Location}; use crate::layout::{ BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem, - Sizing, TrackSizings, VElem, + Sides, Sizing, TrackSizings, }; +use crate::loading::{DataSource, Load}; use crate::model::{ CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem, Url, }; use crate::routines::{EvalMode, Routines}; use crate::text::{ - FontStyle, Lang, LocalName, Region, SubElem, SuperElem, TextElem, WeightDelta, + FontStyle, Lang, LocalName, Region, Smallcaps, SubElem, SuperElem, TextElem, + WeightDelta, }; use crate::World; @@ -86,13 +87,20 @@ use crate::World; /// ``` #[elem(Locatable, Synthesize, Show, ShowSet, LocalName)] pub struct BibliographyElem { - /// Path(s) to Hayagriva `.yml` and/or BibLaTeX `.bib` files. + /// One or multiple paths to or raw bytes for Hayagriva `.yml` and/or + /// BibLaTeX `.bib` files. + /// + /// This can be a: + /// - A path string to load a bibliography file from the given path. For + /// more details about paths, see the [Paths section]($syntax/#paths). + /// - Raw bytes from which the bibliography should be decoded. + /// - An array where each item is one the above. #[required] #[parse( - let (paths, bibliography) = Bibliography::parse(engine, args)?; - paths + let sources = args.expect("sources")?; + Bibliography::load(engine.world, sources)? )] - pub path: BibliographyPaths, + pub sources: Derived, Bibliography>, /// The title of the bibliography. /// @@ -116,19 +124,22 @@ pub struct BibliographyElem { /// The bibliography style. /// - /// Should be either one of the built-in styles (see below) or a path to - /// a [CSL file](https://citationstyles.org/). Some of the styles listed - /// below appear twice, once with their full name and once with a short - /// alias. - #[parse(CslStyle::parse(engine, args)?)] - #[default(CslStyle::from_name("ieee").unwrap())] - pub style: CslStyle, - - /// The loaded bibliography. - #[internal] - #[required] - #[parse(bibliography)] - pub bibliography: Bibliography, + /// This can be: + /// - A string with the name of one of the built-in styles (see below). Some + /// of the styles listed below appear twice, once with their full name and + /// once with a short alias. + /// - A path string to a [CSL file](https://citationstyles.org/). For more + /// details about paths, see the [Paths section]($syntax/#paths). + /// - Raw bytes from which a CSL style should be decoded. + #[parse(match args.named::>("style")? { + Some(source) => Some(CslStyle::load(engine.world, source)?), + None => None, + })] + #[default({ + let default = ArchivedStyle::InstituteOfElectricalAndElectronicsEngineers; + Derived::new(CslSource::Named(default), CslStyle::from_archived(default)) + })] + pub style: Derived, /// The language setting where the bibliography is. #[internal] @@ -141,17 +152,6 @@ pub struct BibliographyElem { pub region: Option, } -/// A list of bibliography file paths. -#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] -pub struct BibliographyPaths(Vec); - -cast! { - BibliographyPaths, - self => self.0.into_value(), - v: EcoString => Self(vec![v]), - v: Array => Self(v.into_iter().map(Value::cast).collect::>()?), -} - impl BibliographyElem { /// Find the document's bibliography. pub fn find(introspector: Tracked) -> StrResult> { @@ -169,13 +169,12 @@ impl BibliographyElem { } /// Whether the bibliography contains the given key. - pub fn has(engine: &Engine, key: impl Into) -> bool { - let key = key.into(); + pub fn has(engine: &Engine, key: Label) -> bool { engine .introspector .query(&Self::elem().select()) .iter() - .any(|elem| elem.to_packed::().unwrap().bibliography().has(key)) + .any(|elem| elem.to_packed::().unwrap().sources.derived.has(key)) } /// Find all bibliography keys. @@ -183,9 +182,9 @@ impl BibliographyElem { let mut vec = vec![]; for elem in introspector.query(&Self::elem().select()).iter() { let this = elem.to_packed::().unwrap(); - for (key, entry) in this.bibliography().iter() { + for (key, entry) in this.sources.derived.iter() { let detail = entry.title().map(|title| title.value.to_str().into()); - vec.push((Label::new(key), detail)) + vec.push((key, detail)) } } vec @@ -207,19 +206,20 @@ impl Show for Packed { 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(styles).unwrap_or_else(|| { - Some(TextElem::packed(Self::local_name_in(styles)).spanned(self.span())) + Some(TextElem::packed(Self::local_name_in(styles)).spanned(span)) }) { seq.push( HeadingElem::new(title) .with_depth(NonZeroUsize::ONE) .pack() - .spanned(self.span()), + .spanned(span), ); } - let span = self.span(); let works = Works::generate(engine).at(span)?; let references = works .references @@ -227,10 +227,9 @@ impl Show for Packed { .ok_or("CSL style is not suitable for bibliographies") .at(span)?; - let row_gutter = ParElem::spacing_in(styles); - let row_gutter_elem = VElem::new(row_gutter.into()).with_weak(true).pack(); - if references.iter().any(|(prefix, _)| prefix.is_some()) { + let row_gutter = ParElem::spacing_in(styles); + let mut cells = vec![]; for (prefix, reference) in references { cells.push(GridChild::Item(GridItem::Cell( @@ -247,23 +246,27 @@ impl Show for Packed { .with_column_gutter(TrackSizings(smallvec![COLUMN_GUTTER.into()])) .with_row_gutter(TrackSizings(smallvec![row_gutter.into()])) .pack() - .spanned(self.span()), + .spanned(span), ); } else { - for (i, (_, reference)) in references.iter().enumerate() { - if i > 0 { - seq.push(row_gutter_elem.clone()); - } - seq.push(reference.clone()); + 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(TextElem::dir_in(styles).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)); } } - let mut content = Content::sequence(seq); - if works.hanging_indent { - content = content.styled(ParElem::set_hanging_indent(INDENT.into())); - } - - Ok(content) + Ok(Content::sequence(seq)) } } @@ -282,63 +285,35 @@ impl LocalName for Packed { } /// A loaded bibliography. -#[derive(Clone, PartialEq)] -pub struct Bibliography { - map: Arc>, - hash: u128, -} +#[derive(Clone, PartialEq, Hash)] +pub struct Bibliography(Arc>>); impl Bibliography { - /// Parse the bibliography argument. - fn parse( - engine: &mut Engine, - args: &mut Args, - ) -> SourceResult<(BibliographyPaths, Bibliography)> { - let Spanned { v: paths, span } = - args.expect::>("path to bibliography file")?; - - // Load bibliography files. - let data = paths - .0 - .iter() - .map(|path| { - let id = span.resolve_path(path).at(span)?; - engine.world.file(id).at(span) - }) - .collect::>>()?; - - // Parse. - let bibliography = Self::load(&paths, &data).at(span)?; - - Ok((paths, bibliography)) + /// Load a bibliography from data sources. + fn load( + world: Tracked, + sources: Spanned>, + ) -> SourceResult, Self>> { + let data = sources.load(world)?; + let bibliography = Self::decode(&sources.v, &data).at(sources.span)?; + Ok(Derived::new(sources.v, bibliography)) } - /// Load bibliography entries from paths. + /// Decode a bibliography from loaded data sources. #[comemo::memoize] #[typst_macros::time(name = "load bibliography")] - fn load(paths: &BibliographyPaths, data: &[Bytes]) -> StrResult { + fn decode( + sources: &OneOrMultiple, + data: &[Bytes], + ) -> StrResult { let mut map = IndexMap::new(); let mut duplicates = Vec::::new(); // We might have multiple bib/yaml files - for (path, bytes) in paths.0.iter().zip(data) { - let src = std::str::from_utf8(bytes).map_err(FileError::from)?; - - let ext = Path::new(path.as_str()) - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default(); - - let library = match ext.to_lowercase().as_str() { - "yml" | "yaml" => hayagriva::io::from_yaml_str(src) - .map_err(|err| eco_format!("failed to parse YAML ({err})"))?, - "bib" => hayagriva::io::from_biblatex_str(src) - .map_err(|errors| format_biblatex_error(path, src, errors))?, - _ => bail!("unknown bibliography format (must be .yml/.yaml or .bib)"), - }; - + for (source, data) in sources.0.iter().zip(data) { + let library = decode_library(source, data)?; for entry in library { - match map.entry(PicoStr::intern(entry.key())) { + match map.entry(Label::new(PicoStr::intern(entry.key()))) { indexmap::map::Entry::Vacant(vacant) => { vacant.insert(entry); } @@ -353,182 +328,210 @@ impl Bibliography { bail!("duplicate bibliography keys: {}", duplicates.join(", ")); } - Ok(Bibliography { - map: Arc::new(map), - hash: typst_utils::hash128(data), - }) + Ok(Bibliography(Arc::new(ManuallyHash::new(map, typst_utils::hash128(data))))) } - fn has(&self, key: impl Into) -> bool { - self.map.contains_key(&key.into()) + fn has(&self, key: Label) -> bool { + self.0.contains_key(&key) } - fn iter(&self) -> impl Iterator { - self.map.iter().map(|(&k, v)| (k, v)) + fn get(&self, key: Label) -> Option<&hayagriva::Entry> { + self.0.get(&key) + } + + fn iter(&self) -> impl Iterator { + self.0.iter().map(|(&k, v)| (k, v)) } } impl Debug for Bibliography { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.debug_set().entries(self.map.keys()).finish() + f.debug_set().entries(self.0.keys()).finish() } } -impl Hash for Bibliography { - fn hash(&self, state: &mut H) { - self.hash.hash(state); +/// Decode on library from one data source. +fn decode_library(source: &DataSource, data: &Bytes) -> StrResult { + let src = data.as_str().map_err(FileError::from)?; + + if let DataSource::Path(path) = source { + // If we got a path, use the extension to determine whether it is + // YAML or BibLaTeX. + let ext = Path::new(path.as_str()) + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default(); + + match ext.to_lowercase().as_str() { + "yml" | "yaml" => hayagriva::io::from_yaml_str(src) + .map_err(|err| eco_format!("failed to parse YAML ({err})")), + "bib" => hayagriva::io::from_biblatex_str(src) + .map_err(|errors| format_biblatex_error(src, Some(path), errors)), + _ => bail!("unknown bibliography format (must be .yml/.yaml or .bib)"), + } + } else { + // If we just got bytes, we need to guess. If it can be decoded as + // hayagriva YAML, we'll use that. + let haya_err = match hayagriva::io::from_yaml_str(src) { + Ok(library) => return Ok(library), + Err(err) => err, + }; + + // If it can be decoded as BibLaTeX, we use that isntead. + let bib_errs = match hayagriva::io::from_biblatex_str(src) { + Ok(library) => return Ok(library), + Err(err) => err, + }; + + // If neither decoded correctly, check whether `:` or `{` appears + // more often to guess whether it's more likely to be YAML or BibLaTeX + // and emit the more appropriate error. + let mut yaml = 0; + let mut biblatex = 0; + for c in src.chars() { + match c { + ':' => yaml += 1, + '{' => biblatex += 1, + _ => {} + } + } + + if yaml > biblatex { + bail!("failed to parse YAML ({haya_err})") + } else { + Err(format_biblatex_error(src, None, bib_errs)) + } } } /// Format a BibLaTeX loading error. -fn format_biblatex_error(path: &str, src: &str, errors: Vec) -> EcoString { +fn format_biblatex_error( + src: &str, + path: Option<&str>, + errors: Vec, +) -> EcoString { let Some(error) = errors.first() else { - return eco_format!("failed to parse BibLaTeX file ({path})"); + return match path { + Some(path) => eco_format!("failed to parse BibLaTeX file ({path})"), + None => eco_format!("failed to parse BibLaTeX"), + }; }; let (span, msg) = match error { BibLaTeXError::Parse(error) => (&error.span, error.kind.to_string()), BibLaTeXError::Type(error) => (&error.span, error.kind.to_string()), }; + let line = src.get(..span.start).unwrap_or_default().lines().count(); - eco_format!("failed to parse BibLaTeX file ({path}:{line}: {msg})") + match path { + Some(path) => eco_format!("failed to parse BibLaTeX file ({path}:{line}: {msg})"), + None => eco_format!("failed to parse BibLaTeX ({line}: {msg})"), + } } /// A loaded CSL style. -#[ty(cast)] #[derive(Debug, Clone, PartialEq, Hash)] -pub struct CslStyle { - name: Option, - style: Arc>, -} +pub struct CslStyle(Arc>); impl CslStyle { - /// Parse the style argument. - pub fn parse(engine: &mut Engine, args: &mut Args) -> SourceResult> { - let Some(Spanned { v: string, span }) = - args.named::>("style")? - else { - return Ok(None); - }; - - Ok(Some(Self::parse_impl(engine, &string, span).at(span)?)) - } - - /// Parse the style argument with `Smart`. - pub fn parse_smart( - engine: &mut Engine, - args: &mut Args, - ) -> SourceResult>> { - let Some(Spanned { v: smart, span }) = - args.named::>>("style")? - else { - return Ok(None); - }; - - Ok(Some(match smart { - Smart::Auto => Smart::Auto, - Smart::Custom(string) => { - Smart::Custom(Self::parse_impl(engine, &string, span).at(span)?) + /// Load a CSL style from a data source. + pub fn load( + world: Tracked, + Spanned { v: source, span }: Spanned, + ) -> SourceResult> { + let style = match &source { + CslSource::Named(style) => Self::from_archived(*style), + CslSource::Normal(source) => { + let data = Spanned::new(source, span).load(world)?; + Self::from_data(data).at(span)? } - })) - } - - /// Parse internally. - fn parse_impl(engine: &mut Engine, string: &str, span: Span) -> StrResult { - let ext = Path::new(string) - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default() - .to_lowercase(); - - if ext == "csl" { - let id = span.resolve_path(string)?; - let data = engine.world.file(id)?; - CslStyle::from_data(&data) - } else { - CslStyle::from_name(string) - } + }; + Ok(Derived::new(source, style)) } /// Load a built-in CSL style. #[comemo::memoize] - pub fn from_name(name: &str) -> StrResult { - match hayagriva::archive::ArchivedStyle::by_name(name).map(ArchivedStyle::get) { - Some(citationberg::Style::Independent(style)) => Ok(Self { - name: Some(name.into()), - style: Arc::new(LazyHash::new(style)), - }), - _ => bail!("unknown style: `{name}`"), + pub fn from_archived(archived: ArchivedStyle) -> CslStyle { + match archived.get() { + citationberg::Style::Independent(style) => Self(Arc::new(ManuallyHash::new( + style, + typst_utils::hash128(&(TypeId::of::(), archived)), + ))), + // Ensured by `test_bibliography_load_builtin_styles`. + _ => unreachable!("archive should not contain dependant styles"), } } /// Load a CSL style from file contents. #[comemo::memoize] - pub fn from_data(data: &Bytes) -> StrResult { - let text = std::str::from_utf8(data.as_slice()).map_err(FileError::from)?; + pub fn from_data(data: Bytes) -> StrResult { + let text = data.as_str().map_err(FileError::from)?; citationberg::IndependentStyle::from_xml(text) - .map(|style| Self { name: None, style: Arc::new(LazyHash::new(style)) }) + .map(|style| { + Self(Arc::new(ManuallyHash::new( + style, + typst_utils::hash128(&(TypeId::of::(), data)), + ))) + }) .map_err(|err| eco_format!("failed to load CSL style ({err})")) } /// Get the underlying independent style. pub fn get(&self) -> &citationberg::IndependentStyle { - self.style.as_ref() + self.0.as_ref() } } -// This Reflect impl is technically a bit wrong because it doesn't say what -// FromValue and IntoValue really do. Instead, it says what the `style` argument -// on `bibliography` and `cite` expect (through manual parsing). -impl Reflect for CslStyle { +/// Source for a CSL style. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum CslSource { + /// A predefined named style. + Named(ArchivedStyle), + /// A normal data source. + Normal(DataSource), +} + +impl Reflect for CslSource { #[comemo::memoize] fn input() -> CastInfo { - let ty = std::iter::once(CastInfo::Type(Type::of::())); - let options = hayagriva::archive::ArchivedStyle::all().iter().map(|name| { + let source = std::iter::once(DataSource::input()); + let names = ArchivedStyle::all().iter().map(|name| { CastInfo::Value(name.names()[0].into_value(), name.display_name()) }); - CastInfo::Union(ty.chain(options).collect()) + CastInfo::Union(source.into_iter().chain(names).collect()) } fn output() -> CastInfo { - EcoString::output() + DataSource::output() } fn castable(value: &Value) -> bool { - if let Value::Dyn(dynamic) = &value { - if dynamic.is::() { - return true; - } - } - - false + DataSource::castable(value) } } -impl FromValue for CslStyle { +impl FromValue for CslSource { fn from_value(value: Value) -> HintedStrResult { - if let Value::Dyn(dynamic) = &value { - if let Some(concrete) = dynamic.downcast::() { - return Ok(concrete.clone()); + if EcoString::castable(&value) { + let string = EcoString::from_value(value.clone())?; + if Path::new(string.as_str()).extension().is_none() { + let style = ArchivedStyle::by_name(&string) + .ok_or_else(|| eco_format!("unknown style: {}", string))?; + return Ok(CslSource::Named(style)); } } - Err(::error(&value)) + DataSource::from_value(value).map(CslSource::Normal) } } -impl IntoValue for CslStyle { +impl IntoValue for CslSource { fn into_value(self) -> Value { - Value::dynamic(self) - } -} - -impl Repr for CslStyle { - fn repr(&self) -> EcoString { - self.name - .as_ref() - .map(|name| name.repr()) - .unwrap_or_else(|| "..".into()) + match self { + // We prefer the shorter names which are at the back of the array. + Self::Named(v) => v.names().last().unwrap().into_value(), + Self::Normal(v) => v.into_value(), + } } } @@ -632,16 +635,15 @@ impl<'a> Generator<'a> { static LOCALES: LazyLock> = LazyLock::new(hayagriva::archive::locales); - let database = self.bibliography.bibliography(); - let bibliography_style = self.bibliography.style(StyleChain::default()); - let styles = Arena::new(); + let database = &self.bibliography.sources.derived; + let bibliography_style = &self.bibliography.style(StyleChain::default()).derived; // Process all citation groups. let mut driver = BibliographyDriver::new(); for elem in &self.groups { let group = elem.to_packed::().unwrap(); let location = elem.location().unwrap(); - let children = group.children(); + let children = &group.children; // Groups should never be empty. let Some(first) = children.first() else { continue }; @@ -653,12 +655,11 @@ impl<'a> Generator<'a> { // Create infos and items for each child in the group. for child in children { - let key = *child.key(); - let Some(entry) = database.map.get(&key.into_inner()) else { + let Some(entry) = database.get(child.key) else { errors.push(error!( child.span(), "key `{}` does not exist in the bibliography", - key.resolve() + child.key.resolve() )); continue; }; @@ -685,7 +686,7 @@ impl<'a> Generator<'a> { }; normal &= special_form.is_none(); - subinfos.push(CiteInfo { key, supplement, hidden }); + subinfos.push(CiteInfo { key: child.key, supplement, hidden }); items.push(CitationItem::new(entry, locator, None, hidden, special_form)); } @@ -695,8 +696,8 @@ impl<'a> Generator<'a> { } let style = match first.style(StyleChain::default()) { - Smart::Auto => &bibliography_style.style, - Smart::Custom(style) => styles.alloc(style.style), + Smart::Auto => bibliography_style.get(), + Smart::Custom(style) => style.derived.get(), }; self.infos.push(GroupInfo { @@ -727,7 +728,7 @@ impl<'a> Generator<'a> { // Add hidden items for everything if we should print the whole // bibliography. if self.bibliography.full(StyleChain::default()) { - for entry in database.map.values() { + for (_, entry) in database.iter() { driver.citation(CitationRequest::new( vec![CitationItem::new(entry, None, None, true, None)], bibliography_style.get(), @@ -1050,7 +1051,8 @@ fn apply_formatting(mut content: Content, format: &hayagriva::Formatting) -> Con match format.font_variant { citationberg::FontVariant::Normal => {} citationberg::FontVariant::SmallCaps => { - content = content.styled(TextElem::set_smallcaps(true)); + content = + content.styled(TextElem::set_smallcaps(Some(Smallcaps::Minuscules))); } } @@ -1097,3 +1099,15 @@ fn locale(lang: Lang, region: Option) -> citationberg::LocaleCode { } citationberg::LocaleCode(value) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bibliography_load_builtin_styles() { + for &archived in ArchivedStyle::all() { + let _ = CslStyle::from_archived(archived); + } + } +} diff --git a/crates/typst-library/src/model/cite.rs b/crates/typst-library/src/model/cite.rs index ac0cfa790..29497993d 100644 --- a/crates/typst-library/src/model/cite.rs +++ b/crates/typst-library/src/model/cite.rs @@ -1,11 +1,14 @@ +use typst_syntax::Spanned; + use crate::diag::{error, At, HintedString, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, Cast, Content, Label, Packed, Show, Smart, StyleChain, Synthesize, + cast, elem, Cast, Content, Derived, Label, Packed, Show, Smart, StyleChain, + Synthesize, }; use crate::introspection::Locatable; use crate::model::bibliography::Works; -use crate::model::CslStyle; +use crate::model::{CslSource, CslStyle}; use crate::text::{Lang, Region, TextElem}; /// Cite a work from the bibliography. @@ -87,15 +90,24 @@ pub struct CiteElem { /// The citation style. /// - /// Should be either `{auto}`, one of the built-in styles (see below) or a - /// path to a [CSL file](https://citationstyles.org/). Some of the styles - /// listed below appear twice, once with their full name and once with a - /// short alias. - /// - /// When set to `{auto}`, automatically use the - /// [bibliography's style]($bibliography.style) for the citations. - #[parse(CslStyle::parse_smart(engine, args)?)] - pub style: Smart, + /// This can be: + /// - `{auto}` to automatically use the + /// [bibliography's style]($bibliography.style) for citations. + /// - A string with the name of one of the built-in styles (see below). Some + /// of the styles listed below appear twice, once with their full name and + /// once with a short alias. + /// - A path string to a [CSL file](https://citationstyles.org/). For more + /// details about paths, see the [Paths section]($syntax/#paths). + /// - Raw bytes from which a CSL style should be decoded. + #[parse(match args.named::>>("style")? { + Some(Spanned { v: Smart::Custom(source), span }) => Some(Smart::Custom( + CslStyle::load(engine.world, Spanned::new(source, span))? + )), + Some(Spanned { v: Smart::Auto, .. }) => Some(Smart::Auto), + None => None, + })] + #[borrowed] + pub style: Smart>, /// The text language setting where the citation is. #[internal] diff --git a/crates/typst-library/src/model/document.rs b/crates/typst-library/src/model/document.rs index 5124b2487..1bce6b357 100644 --- a/crates/typst-library/src/model/document.rs +++ b/crates/typst-library/src/model/document.rs @@ -3,8 +3,8 @@ use ecow::EcoString; use crate::diag::{bail, HintedStrResult, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, Args, Array, Construct, Content, Datetime, Fields, Smart, StyleChain, - Styles, Value, + cast, elem, Args, Array, Construct, Content, Datetime, Fields, OneOrMultiple, Smart, + StyleChain, Styles, Value, }; /// The root element of a document and its metadata. @@ -35,7 +35,7 @@ pub struct DocumentElem { /// The document's authors. #[ghost] - pub author: Author, + pub author: OneOrMultiple, /// The document's description. #[ghost] @@ -43,7 +43,7 @@ pub struct DocumentElem { /// The document's keywords. #[ghost] - pub keywords: Keywords, + pub keywords: OneOrMultiple, /// The document's creation date. /// @@ -93,7 +93,7 @@ cast! { pub struct DocumentInfo { /// The document's title. pub title: Option, - /// The document's author. + /// The document's author(s). pub author: Vec, /// The document's description. pub description: Option, diff --git a/crates/typst-library/src/model/enum.rs b/crates/typst-library/src/model/enum.rs index eb3c2ea45..a4126e72c 100644 --- a/crates/typst-library/src/model/enum.rs +++ b/crates/typst-library/src/model/enum.rs @@ -9,9 +9,11 @@ use crate::foundations::{ cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain, Styles, TargetElem, }; -use crate::html::{attr, tag, HtmlAttr, HtmlElem}; +use crate::html::{attr, tag, HtmlElem}; use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem}; -use crate::model::{ListItemLike, ListLike, Numbering, NumberingPattern, ParElem}; +use crate::model::{ + ListItemLike, ListLike, Numbering, NumberingPattern, ParElem, ParbreakElem, +}; /// A numbered list. /// @@ -226,22 +228,29 @@ impl EnumElem { impl Show for Packed { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { + let tight = self.tight(styles); + if TargetElem::target_in(styles).is_html() { let mut elem = HtmlElem::new(tag::ol); if self.reversed(styles) { - elem = - elem.with_attr(const { HtmlAttr::constant("reversed") }, "reversed"); + elem = elem.with_attr(attr::reversed, "reversed"); } - return Ok(elem - .with_body(Some(Content::sequence(self.children.iter().map(|item| { - let mut li = HtmlElem::new(tag::li); - if let Some(nr) = item.number(styles) { - li = li.with_attr(attr::value, eco_format!("{nr}")); - } - li.with_body(Some(item.body.clone())).pack().spanned(item.span()) - })))) - .pack() - .spanned(self.span())); + if let Some(n) = self.start(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(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 = @@ -249,7 +258,7 @@ impl Show for Packed { .pack() .spanned(self.span()); - if self.tight(styles) { + if tight { let leading = ParElem::leading_in(styles); let spacing = VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); diff --git a/crates/typst-library/src/model/figure.rs b/crates/typst-library/src/model/figure.rs index fd843ee53..78a79a8e2 100644 --- a/crates/typst-library/src/model/figure.rs +++ b/crates/typst-library/src/model/figure.rs @@ -19,7 +19,9 @@ use crate::layout::{ AlignElem, Alignment, BlockBody, BlockElem, Em, HAlignment, Length, OuterVAlignment, PlaceElem, PlacementScope, VAlignment, VElem, }; -use crate::model::{Numbering, NumberingPattern, Outlinable, Refable, Supplement}; +use crate::model::{ + Numbering, NumberingPattern, Outlinable, ParbreakElem, Refable, Supplement, +}; use crate::text::{Lang, Region, TextElem}; use crate::visualize::ImageElem; @@ -156,6 +158,7 @@ pub struct FigureElem { pub scope: PlacementScope, /// The figure's caption. + #[borrowed] pub caption: Option>, /// The kind of figure this is. @@ -257,7 +260,7 @@ impl Synthesize for Packed { // Determine the figure's kind. let kind = elem.kind(styles).unwrap_or_else(|| { - elem.body() + elem.body .query_first(&Selector::can::()) .map(|elem| FigureKind::Elem(elem.func())) .unwrap_or_else(|| FigureKind::Elem(ImageElem::elem())) @@ -288,14 +291,13 @@ impl Synthesize for Packed { // Resolve the supplement with the first descendant of the kind or // just the body, if none was found. let descendant = match kind { - FigureKind::Elem(func) => elem - .body() - .query_first(&Selector::Elem(func, None)) - .map(Cow::Owned), + FigureKind::Elem(func) => { + elem.body.query_first(&Selector::Elem(func, None)).map(Cow::Owned) + } FigureKind::Name(_) => None, }; - let target = descendant.unwrap_or_else(|| Cow::Borrowed(elem.body())); + let target = descendant.unwrap_or_else(|| Cow::Borrowed(&elem.body)); Some(supplement.resolve(engine, styles, [target])?) } }; @@ -306,7 +308,7 @@ impl Synthesize for Packed { )); // Fill the figure's caption. - let mut caption = elem.caption(styles); + let mut caption = elem.caption(styles).clone(); if let Some(caption) = &mut caption { caption.synthesize(engine, styles)?; caption.push_kind(kind.clone()); @@ -328,11 +330,12 @@ 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 = TargetElem::target_in(styles); let mut realized = self.body.clone(); // Build the caption, if any. - if let Some(caption) = self.caption(styles) { + if let Some(caption) = self.caption(styles).clone() { let (first, second) = match caption.position(styles) { OuterVAlignment::Top => (caption.pack(), realized), OuterVAlignment::Bottom => (realized, caption.pack()), @@ -341,24 +344,27 @@ impl Show for Packed { seq.push(first); if !target.is_html() { let v = VElem::new(self.gap(styles).into()).with_weak(true); - seq.push(v.pack().spanned(self.span())) + 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(self.span())); + .spanned(span)); } // Wrap the contents in a block. realized = BlockElem::new() .with_body(Some(BlockBody::Content(realized))) .pack() - .spanned(self.span()); + .spanned(span); // Wrap in a float. if let Some(align) = self.placement(styles) { @@ -367,10 +373,10 @@ impl Show for Packed { .with_scope(self.scope(styles)) .with_float(true) .pack() - .spanned(self.span()); + .spanned(span); } else if self.scope(styles) == PlacementScope::Parent { bail!( - self.span(), + span, "parent-scoped placement is only available for floating figures"; hint: "you can enable floating placement with `figure(placement: auto, ..)`" ); @@ -424,46 +430,26 @@ impl Refable for Packed { } impl Outlinable for Packed { - fn outline( - &self, - engine: &mut Engine, - styles: StyleChain, - ) -> SourceResult> { - if !self.outlined(StyleChain::default()) { - return Ok(None); + fn outlined(&self) -> bool { + (**self).outlined(StyleChain::default()) + && (self.caption(StyleChain::default()).is_some() + || self.numbering().is_some()) + } + + fn prefix(&self, numbers: Content) -> Content { + let supplement = self.supplement(); + if !supplement.is_empty() { + supplement + TextElem::packed('\u{a0}') + numbers + } else { + numbers } + } - let Some(caption) = self.caption(StyleChain::default()) else { - return Ok(None); - }; - - let mut realized = caption.body().clone(); - if let ( - Smart::Custom(Some(Supplement::Content(mut supplement))), - Some(Some(counter)), - Some(numbering), - ) = ( - (**self).supplement(StyleChain::default()).clone(), - (**self).counter(), - self.numbering(), - ) { - let numbers = counter.display_at_loc( - engine, - self.location().unwrap(), - styles, - numbering, - )?; - - if !supplement.is_empty() { - supplement += TextElem::packed('\u{a0}'); - } - - let separator = caption.get_separator(StyleChain::default()); - - realized = supplement + numbers + separator + caption.body(); - } - - Ok(Some(realized)) + fn body(&self) -> Content { + self.caption(StyleChain::default()) + .as_ref() + .map(|caption| caption.body.clone()) + .unwrap_or_default() } } @@ -604,7 +590,7 @@ 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(); + let mut realized = self.body.clone(); if let ( Some(Some(mut supplement)), @@ -624,14 +610,17 @@ impl Show for Packed { realized = supplement + numbers + self.get_separator(styles) + realized; } - if TargetElem::target_in(styles).is_html() { - return Ok(HtmlElem::new(tag::figcaption) + Ok(if TargetElem::target_in(styles).is_html() { + HtmlElem::new(tag::figcaption) .with_body(Some(realized)) .pack() - .spanned(self.span())); - } - - Ok(realized) + .spanned(self.span()) + } else { + BlockElem::new() + .with_body(Some(BlockBody::Content(realized))) + .pack() + .spanned(self.span()) + }) } } diff --git a/crates/typst-library/src/model/footnote.rs b/crates/typst-library/src/model/footnote.rs index ffc78ea05..dfa3933bb 100644 --- a/crates/typst-library/src/model/footnote.rs +++ b/crates/typst-library/src/model/footnote.rs @@ -105,12 +105,12 @@ impl FootnoteElem { /// Tests if this footnote is a reference to another footnote. pub fn is_ref(&self) -> bool { - matches!(self.body(), FootnoteBody::Reference(_)) + matches!(self.body, FootnoteBody::Reference(_)) } /// Returns the content of the body of this footnote if it is not a ref. pub fn body_content(&self) -> Option<&Content> { - match self.body() { + match &self.body { FootnoteBody::Content(content) => Some(content), _ => None, } @@ -120,9 +120,9 @@ impl FootnoteElem { impl Packed { /// Returns the location of the definition of this footnote. pub fn declaration_location(&self, engine: &Engine) -> StrResult { - match self.body() { + match self.body { FootnoteBody::Reference(label) => { - let element = engine.introspector.query_label(*label)?; + let element = engine.introspector.query_label(label)?; let footnote = element .to_packed::() .ok_or("referenced element should be a footnote")?; @@ -281,12 +281,11 @@ 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 note = self.note(); let number_gap = Em::new(0.05); let default = StyleChain::default(); - let numbering = note.numbering(default); + let numbering = self.note.numbering(default); let counter = Counter::of(FootnoteElem::elem()); - let Some(loc) = note.location() else { + 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" @@ -304,18 +303,16 @@ impl Show for Packed { HElem::new(self.indent(styles).into()).pack(), sup, HElem::new(number_gap.into()).with_weak(true).pack(), - note.body_content().unwrap().clone(), + self.note.body_content().unwrap().clone(), ])) } } impl ShowSet for Packed { fn show_set(&self, _: StyleChain) -> Styles { - let text_size = Em::new(0.85); - let leading = Em::new(0.5); let mut out = Styles::new(); - out.set(ParElem::set_leading(leading.into())); - out.set(TextElem::set_size(TextSize(text_size.into()))); + out.set(ParElem::set_leading(Em::new(0.5).into())); + out.set(TextElem::set_size(TextSize(Em::new(0.85).into()))); out } } diff --git a/crates/typst-library/src/model/heading.rs b/crates/typst-library/src/model/heading.rs index ec9cf4e99..00931c815 100644 --- a/crates/typst-library/src/model/heading.rs +++ b/crates/typst-library/src/model/heading.rs @@ -1,7 +1,7 @@ use std::num::NonZeroUsize; use ecow::eco_format; -use typst_utils::NonZeroExt; +use typst_utils::{Get, NonZeroExt}; use crate::diag::{warning, SourceResult}; use crate::engine::Engine; @@ -13,8 +13,8 @@ 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}; -use crate::model::{Numbering, Outlinable, ParElem, Refable, Supplement}; +use crate::layout::{Abs, Axes, BlockBody, BlockElem, Em, HElem, Length, Region, Sides}; +use crate::model::{Numbering, Outlinable, Refable, Supplement}; use crate::text::{FontWeight, LocalName, SpaceElem, TextElem, TextSize}; /// A section heading. @@ -223,7 +223,7 @@ impl Show for Packed { const SPACING_TO_NUMBERING: Em = Em::new(0.3); let span = self.span(); - let mut realized = self.body().clone(); + let mut realized = self.body.clone(); let hanging_indent = self.hanging_indent(styles); let mut indent = match hanging_indent { @@ -264,10 +264,6 @@ impl Show for Packed { realized = numbering + spacing + realized; } - if indent != Abs::zero() && !html { - realized = realized.styled(ParElem::set_hanging_indent(indent.into())); - } - 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 @@ -294,8 +290,17 @@ impl Show for Packed { HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span) } } else { - let realized = BlockBody::Content(realized); - BlockElem::new().with_body(Some(realized)).pack().spanned(span) + let block = if indent != Abs::zero() { + let body = HElem::new((-indent).into()).pack() + realized; + let inset = Sides::default() + .with(TextElem::dir_in(styles).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) }) } } @@ -351,32 +356,21 @@ impl Refable for Packed { } impl Outlinable for Packed { - fn outline( - &self, - engine: &mut Engine, - styles: StyleChain, - ) -> SourceResult> { - if !self.outlined(StyleChain::default()) { - return Ok(None); - } - - let mut content = self.body().clone(); - if let Some(numbering) = (**self).numbering(StyleChain::default()).as_ref() { - let numbers = Counter::of(HeadingElem::elem()).display_at_loc( - engine, - self.location().unwrap(), - styles, - numbering, - )?; - content = numbers + SpaceElem::shared().clone() + content; - }; - - Ok(Some(content)) + fn outlined(&self) -> bool { + (**self).outlined(StyleChain::default()) } fn level(&self) -> NonZeroUsize { (**self).resolve_level(StyleChain::default()) } + + fn prefix(&self, numbers: Content) -> Content { + numbers + } + + fn body(&self) -> Content { + self.body.clone() + } } impl LocalName for Packed { diff --git a/crates/typst-library/src/model/link.rs b/crates/typst-library/src/model/link.rs index 8ab129fdd..24b746b7e 100644 --- a/crates/typst-library/src/model/link.rs +++ b/crates/typst-library/src/model/link.rs @@ -1,13 +1,12 @@ use std::ops::Deref; use ecow::{eco_format, EcoString}; -use smallvec::SmallVec; use crate::diag::{bail, warning, At, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, Content, Label, NativeElement, Packed, Repr, Show, Smart, StyleChain, - TargetElem, + cast, elem, Content, Label, NativeElement, Packed, Repr, Show, ShowSet, Smart, + StyleChain, Styles, TargetElem, }; use crate::html::{attr, tag, HtmlElem}; use crate::introspection::Location; @@ -16,7 +15,7 @@ use crate::text::{Hyphenate, TextElem}; /// Links to a URL or a location in the document. /// -/// By default, links are not styled any different from normal text. However, +/// By default, links do not look any different from normal text. However, /// you can easily apply a style of your choice with a show rule. /// /// # Example @@ -31,6 +30,11 @@ use crate::text::{Hyphenate, TextElem}; /// ] /// ``` /// +/// # Hyphenation +/// If you enable hyphenation or justification, by default, it will not apply to +/// links to prevent unwanted hyphenation in URLs. You can opt out of this +/// default via `{show link: set text(hyphenate: true)}`. +/// /// # Syntax /// This function also has dedicated syntax: Text that starts with `http://` or /// `https://` is automatically turned into a link. @@ -85,10 +89,10 @@ pub struct LinkElem { })] pub body: Content, - /// This style is set on the content contained in the `link` element. + /// A destination style that should be applied to elements. #[internal] #[ghost] - pub dests: SmallVec<[Destination; 1]>, + pub current: Option, } impl LinkElem { @@ -102,11 +106,10 @@ 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(); - let dest = self.dest(); + let body = self.body.clone(); Ok(if TargetElem::target_in(styles).is_html() { - if let LinkTarget::Dest(Destination::Url(url)) = dest { + 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)) @@ -120,25 +123,31 @@ impl Show for Packed { body } } else { - let linked = match self.dest() { + match &self.dest { LinkTarget::Dest(dest) => body.linked(dest.clone()), LinkTarget::Label(label) => { let elem = engine.introspector.query_label(*label).at(self.span())?; let dest = Destination::Location(elem.location().unwrap()); body.clone().linked(dest) } - }; - - linked.styled(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))) + } }) } } -fn body_from_url(url: &Url) -> Content { - let mut text = url.as_str(); - for prefix in ["mailto:", "tel:"] { - text = text.trim_start_matches(prefix); +impl ShowSet for Packed { + fn show_set(&self, _: StyleChain) -> Styles { + let mut out = Styles::new(); + out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))); + out } +} + +fn body_from_url(url: &Url) -> Content { + let text = ["mailto:", "tel:"] + .into_iter() + .find_map(|prefix| url.strip_prefix(prefix)) + .unwrap_or(url); let shorter = text.len() < url.len(); TextElem::packed(if shorter { text.into() } else { (**url).clone() }) } diff --git a/crates/typst-library/src/model/list.rs b/crates/typst-library/src/model/list.rs index 1e369d541..d93ec9172 100644 --- a/crates/typst-library/src/model/list.rs +++ b/crates/typst-library/src/model/list.rs @@ -8,7 +8,7 @@ use crate::foundations::{ }; use crate::html::{tag, HtmlElem}; use crate::layout::{BlockElem, Em, Length, VElem}; -use crate::model::ParElem; +use crate::model::{ParElem, ParbreakElem}; use crate::text::TextElem; /// A bullet list. @@ -141,11 +141,18 @@ impl ListElem { impl Show for Packed { fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { + let tight = self.tight(styles); + if TargetElem::target_in(styles).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(item.body.clone())) + .with_body(Some(body)) .pack() .spanned(item.span()) })))) @@ -158,7 +165,7 @@ impl Show for Packed { .pack() .spanned(self.span()); - if self.tight(styles) { + if tight { let leading = ParElem::leading_in(styles); let spacing = VElem::new(leading.into()).with_weak(true).with_attach(true).pack(); diff --git a/crates/typst-library/src/model/numbering.rs b/crates/typst-library/src/model/numbering.rs index 4e2fe4579..150506758 100644 --- a/crates/typst-library/src/model/numbering.rs +++ b/crates/typst-library/src/model/numbering.rs @@ -53,9 +53,7 @@ use crate::text::Case; /// ``` #[func] pub fn numbering( - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// Defines how the numbering works. /// diff --git a/crates/typst-library/src/model/outline.rs b/crates/typst-library/src/model/outline.rs index e8d32a540..f413189ba 100644 --- a/crates/typst-library/src/model/outline.rs +++ b/crates/typst-library/src/model/outline.rs @@ -1,50 +1,61 @@ use std::num::NonZeroUsize; use std::str::FromStr; -use comemo::Track; +use comemo::{Track, Tracked}; +use smallvec::SmallVec; use typst_syntax::Span; -use typst_utils::NonZeroExt; +use typst_utils::{Get, NonZeroExt}; -use crate::diag::{bail, At, SourceResult}; +use crate::diag::{bail, error, At, HintedStrResult, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, select_where, Content, Context, Func, LocatableSelector, - NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles, + cast, elem, func, scope, select_where, Args, Construct, Content, Context, Func, + LocatableSelector, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain, + Styles, +}; +use crate::introspection::{ + Counter, CounterKey, Introspector, Locatable, Location, Locator, LocatorLink, }; -use crate::introspection::{Counter, CounterKey, Locatable}; use crate::layout::{ - BoxElem, Dir, Em, Fr, HElem, HideElem, Length, Rel, RepeatElem, Spacing, + Abs, Axes, BlockBody, BlockElem, BoxElem, Dir, Em, Fr, HElem, Length, Region, Rel, + RepeatElem, Sides, }; -use crate::model::{ - Destination, HeadingElem, NumberingPattern, ParElem, ParbreakElem, Refable, -}; -use crate::text::{LinebreakElem, LocalName, SpaceElem, TextElem}; +use crate::math::EquationElem; +use crate::model::{Destination, HeadingElem, NumberingPattern, ParElem, Refable}; +use crate::text::{LocalName, SpaceElem, TextElem}; /// A table of contents, figures, or other elements. /// /// This function generates a list of all occurrences of an element in the -/// document, up to a given depth. The element's numbering and page number will -/// be displayed in the outline alongside its title or caption. By default this -/// generates a table of contents. +/// document, up to a given [`depth`]($outline.depth). The element's numbering +/// and page number will be displayed in the outline alongside its title or +/// caption. /// /// # Example /// ```example +/// #set heading(numbering: "1.") /// #outline() /// /// = Introduction /// #lorem(5) /// -/// = Prior work +/// = Methods +/// == Setup /// #lorem(10) /// ``` /// /// # Alternative outlines +/// In its default configuration, this function generates a table of contents. /// By setting the `target` parameter, the outline can be used to generate a -/// list of other kinds of elements than headings. In the example below, we list -/// all figures containing images by setting `target` to `{figure.where(kind: -/// image)}`. We could have also set it to just `figure`, but then the list -/// would also include figures containing tables or other material. For more -/// details on the `where` selector, [see here]($function.where). +/// list of other kinds of elements than headings. +/// +/// In the example below, we list all figures containing images by setting +/// `target` to `{figure.where(kind: image)}`. Just the same, we could have set +/// it to `{figure.where(kind: table)}` to generate a list of tables. +/// +/// We could also set it to just `figure`, without using a [`where`]($function.where) +/// selector, but then the list would contain _all_ figures, be it ones +/// containing images, tables, or other material. /// /// ```example /// #outline( @@ -59,16 +70,89 @@ use crate::text::{LinebreakElem, LocalName, SpaceElem, TextElem}; /// ``` /// /// # Styling the outline -/// The outline element has several options for customization, such as its -/// `title` and `indent` parameters. If desired, however, it is possible to have -/// more control over the outline's look and style through the -/// [`outline.entry`]($outline.entry) element. -#[elem(scope, keywords = ["Table of Contents"], Show, ShowSet, LocalName)] +/// At the most basic level, you can style the outline by setting properties on +/// it and its entries. This way, you can customize the outline's +/// [title]($outline.title), how outline entries are +/// [indented]($outline.indent), and how the space between an entry's text and +/// its page number should be [filled]($outline.entry.fill). +/// +/// Richer customization is possible through configuration of the outline's +/// [entries]($outline.entry). The outline generates one entry for each outlined +/// element. +/// +/// ## Spacing the entries { #entry-spacing } +/// Outline entries are [blocks]($block), so you can adjust the spacing between +/// them with normal block-spacing rules: +/// +/// ```example +/// #show outline.entry.where( +/// level: 1 +/// ): set block(above: 1.2em) +/// +/// #outline() +/// +/// = About ACME Corp. +/// == History +/// === Origins +/// = Products +/// == ACME Tools +/// ``` +/// +/// ## Building an outline entry from its parts { #building-an-entry } +/// For full control, you can also write a transformational show rule on +/// `outline.entry`. However, the logic for properly formatting and indenting +/// outline entries is quite complex and the outline entry itself only contains +/// two fields: The level and the outlined element. +/// +/// For this reason, various helper functions are provided. You can mix and +/// match these to compose an entry from just the parts you like. +/// +/// The default show rule for an outline entry looks like this[^1]: +/// ```typ +/// #show outline.entry: it => link( +/// it.element.location(), +/// it.indented(it.prefix(), it.inner()), +/// ) +/// ``` +/// +/// - The [`indented`]($outline.entry.indented) function takes an optional +/// prefix and inner content and automatically applies the proper indentation +/// to it, such that different entries align nicely and long headings wrap +/// properly. +/// +/// - The [`prefix`]($outline.entry.prefix) function formats the element's +/// numbering (if any). It also appends a supplement for certain elements. +/// +/// - The [`inner`]($outline.entry.inner) function combines the element's +/// [`body`]($outline.entry.body), the filler, and the +/// [`page` number]($outline.entry.page). +/// +/// You can use these individual functions to format the outline entry in +/// different ways. Let's say, you'd like to fully remove the filler and page +/// numbers. To achieve this, you could write a show rule like this: +/// +/// ```example +/// #show outline.entry: it => link( +/// it.element.location(), +/// // Keep just the body, dropping +/// // the fill and the page. +/// it.indented(it.prefix(), it.body()), +/// ) +/// +/// #outline() +/// +/// = About ACME Corp. +/// == History +/// ``` +/// +/// [^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)] pub struct OutlineElem { /// The title of the outline. /// /// - When set to `{auto}`, an appropriate title for the - /// [text language]($text.lang) will be used. This is the default. + /// [text language]($text.lang) will be used. /// - When set to `{none}`, the outline will not have a title. /// - A custom title can be set by passing content. /// @@ -79,8 +163,10 @@ pub struct OutlineElem { /// The type of element to include in the outline. /// - /// To list figures containing a specific kind of element, like a table, you - /// can write `{figure.where(kind: table)}`. + /// To list figures containing a specific kind of element, like an image or + /// a table, you can specify the desired kind in a [`where`]($function.where) + /// selector. See the section on [alternative outlines]($outline/#alternative-outlines) + /// for more details. /// /// ```example /// #outline( @@ -97,7 +183,7 @@ pub struct OutlineElem { /// caption: [Experiment results], /// ) /// ``` - #[default(LocatableSelector(select_where!(HeadingElem, Outlined => true)))] + #[default(LocatableSelector(HeadingElem::elem().select()))] #[borrowed] pub target: LocatableSelector, @@ -121,21 +207,22 @@ pub struct OutlineElem { /// How to indent the outline's entries. /// - /// - `{none}`: No indent - /// - `{auto}`: Indents the numbering of the nested entry with the title of - /// its parent entry. This only has an effect if the entries are numbered - /// (e.g., via [heading numbering]($heading.numbering)). - /// - [Relative length]($relative): Indents the item by this length - /// multiplied by its nesting level. Specifying `{2em}`, for instance, - /// would indent top-level headings (not nested) by `{0em}`, second level + /// - `{auto}`: Indents the numbering/prefix of a nested entry with the + /// title of its parent entry. If the entries are not numbered (e.g., via + /// [heading numbering]($heading.numbering)), this instead simply inserts + /// a fixed amount of `{1.2em}` indent per level. + /// + /// - [Relative length]($relative): Indents the entry by the specified + /// length per nesting level. Specifying `{2em}`, for instance, would + /// indent top-level headings by `{0em}` (not nested), second level /// headings by `{2em}` (nested once), third-level headings by `{4em}` /// (nested twice) and so on. - /// - [Function]($function): You can completely customize this setting with - /// a function. That function receives the nesting level as a parameter - /// (starting at 0 for top-level headings/elements) and can return a - /// relative length or content making up the indent. For example, - /// `{n => n * 2em}` would be equivalent to just specifying `{2em}`, while - /// `{n => [→ ] * n}` would indent with one arrow per nesting level. + /// + /// - [Function]($function): You can further customize this setting with a + /// function. That function receives the nesting level as a parameter + /// (starting at 0 for top-level headings/elements) and should return a + /// (relative) length. For example, `{n => n * 2em}` would be equivalent + /// to just specifying `{2em}`. /// /// ```example /// #set heading(numbering: "1.a.") @@ -150,11 +237,6 @@ pub struct OutlineElem { /// indent: 2em, /// ) /// - /// #outline( - /// title: [Contents (Function)], - /// indent: n => [→ ] * n, - /// ) - /// /// = About ACME Corp. /// == History /// === Origins @@ -163,20 +245,7 @@ pub struct OutlineElem { /// == Products /// #lorem(10) /// ``` - #[default(None)] - #[borrowed] - pub indent: Option>, - - /// Content to fill the space between the title and the page number. Can be - /// set to `{none}` to disable filling. - /// - /// ```example - /// #outline(fill: line(length: 100%)) - /// - /// = A New Beginning - /// ``` - #[default(Some(RepeatElem::new(TextElem::packed(".")).pack()))] - pub fill: Option, + pub indent: Smart, } #[scope] @@ -188,80 +257,51 @@ impl OutlineElem { impl Show for Packed { #[typst_macros::time(name = "outline", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let mut seq = vec![ParbreakElem::shared().clone()]; + let span = self.span(); + // Build the outline title. + let mut seq = vec![]; if let Some(title) = self.title(styles).unwrap_or_else(|| { - Some(TextElem::packed(Self::local_name_in(styles)).spanned(self.span())) + Some(TextElem::packed(Self::local_name_in(styles)).spanned(span)) }) { seq.push( HeadingElem::new(title) .with_depth(NonZeroUsize::ONE) .pack() - .spanned(self.span()), + .spanned(span), ); } - let indent = self.indent(styles); - let depth = self.depth(styles).unwrap_or(NonZeroUsize::new(usize::MAX).unwrap()); - - let mut ancestors: Vec<&Content> = vec![]; let elems = engine.introspector.query(&self.target(styles).0); + let depth = self.depth(styles).unwrap_or(NonZeroUsize::MAX); - for elem in &elems { - let Some(entry) = OutlineEntry::from_outlinable( - engine, - self.span(), - elem.clone(), - self.fill(styles), - styles, - )? - else { - continue; + // Build the outline entries. + for elem in elems { + let Some(outlinable) = elem.with::() else { + bail!(span, "cannot outline {}", elem.func().name()); }; - let level = entry.level(); - if depth < *level { - continue; + let level = outlinable.level(); + if outlinable.outlined() && level <= depth { + let entry = OutlineEntry::new(level, elem); + seq.push(entry.pack().spanned(span)); } - - // Deals with the ancestors of the current element. - // This is only applicable for elements with a hierarchy/level. - while ancestors - .last() - .and_then(|ancestor| ancestor.with::()) - .is_some_and(|last| last.level() >= *level) - { - ancestors.pop(); - } - - OutlineIndent::apply( - indent, - engine, - &ancestors, - &mut seq, - styles, - self.span(), - )?; - - // Add the overridable outline entry, followed by a line break. - seq.push(entry.pack().spanned(self.span())); - seq.push(LinebreakElem::shared().clone()); - - ancestors.push(elem); } - seq.push(ParbreakElem::shared().clone()); - Ok(Content::sequence(seq)) } } impl ShowSet for Packed { - fn show_set(&self, _: StyleChain) -> Styles { + fn show_set(&self, styles: StyleChain) -> Styles { let mut out = Styles::new(); out.set(HeadingElem::set_outlined(false)); out.set(HeadingElem::set_numbering(None)); - out.set(ParElem::set_first_line_indent(Em::new(0.0).into())); + out.set(ParElem::set_justify(false)); + out.set(BlockElem::set_above(Smart::Custom(ParElem::leading_in(styles).into()))); + // Makes the outline itself available to its entries. Should be + // superseded by a proper ancestry mechanism in the future. + out.set(OutlineEntry::set_parent(Some(self.clone()))); out } } @@ -270,93 +310,29 @@ impl LocalName for Packed { const KEY: &'static str = "outline"; } -/// Marks an element as being able to be outlined. This is used to implement the -/// `#outline()` element. -pub trait Outlinable: Refable { - /// Produce an outline item for this element. - fn outline( - &self, - engine: &mut Engine, - - styles: StyleChain, - ) -> SourceResult>; - - /// Returns the nesting level of this element. - fn level(&self) -> NonZeroUsize { - NonZeroUsize::ONE - } -} - /// Defines how an outline is indented. #[derive(Debug, Clone, PartialEq, Hash)] pub enum OutlineIndent { - Rel(Rel), + /// Indents by the specified length per level. + Rel(Rel), + /// Resolve the indent for a specific level through the given function. Func(Func), } impl OutlineIndent { - fn apply( - indent: &Option>, + /// Resolve the indent for an entry with the given level. + fn resolve( + &self, engine: &mut Engine, - ancestors: &Vec<&Content>, - seq: &mut Vec, - styles: StyleChain, + context: Tracked, + level: NonZeroUsize, span: Span, - ) -> SourceResult<()> { - match indent { - // 'none' | 'false' => no indenting - None => {} - - // 'auto' | 'true' => use numbering alignment for indenting - Some(Smart::Auto) => { - // Add hidden ancestors numberings to realize the indent. - let mut hidden = Content::empty(); - for ancestor in ancestors { - let ancestor_outlinable = ancestor.with::().unwrap(); - - if let Some(numbering) = ancestor_outlinable.numbering() { - let numbers = ancestor_outlinable.counter().display_at_loc( - engine, - ancestor.location().unwrap(), - styles, - numbering, - )?; - - hidden += numbers + SpaceElem::shared().clone(); - }; - } - - if !ancestors.is_empty() { - seq.push(HideElem::new(hidden).pack().spanned(span)); - seq.push(SpaceElem::shared().clone().spanned(span)); - } - } - - // Length => indent with some fixed spacing per level - Some(Smart::Custom(OutlineIndent::Rel(length))) => { - seq.push( - HElem::new(Spacing::Rel(*length)) - .pack() - .spanned(span) - .repeat(ancestors.len()), - ); - } - - // Function => call function with the current depth and take - // the returned content - Some(Smart::Custom(OutlineIndent::Func(func))) => { - let depth = ancestors.len(); - let LengthOrContent(content) = func - .call(engine, Context::new(None, Some(styles)).track(), [depth])? - .cast() - .at(span)?; - if !content.is_empty() { - seq.push(content); - } - } - }; - - Ok(()) + ) -> SourceResult { + let depth = level.get() - 1; + match self { + Self::Rel(length) => Ok(*length * depth as f64), + Self::Func(func) => func.call(engine, context, [depth])?.cast().at(span), + } } } @@ -366,46 +342,33 @@ cast! { Self::Rel(v) => v.into_value(), Self::Func(v) => v.into_value() }, - v: Rel => OutlineIndent::Rel(v), - v: Func => OutlineIndent::Func(v), + v: Rel => Self::Rel(v), + v: Func => Self::Func(v), } -struct LengthOrContent(Content); +/// Marks an element as being able to be outlined. +pub trait Outlinable: Refable { + /// Whether this element should be included in the outline. + fn outlined(&self) -> bool; -cast! { - LengthOrContent, - v: Rel => Self(HElem::new(Spacing::Rel(v)).pack()), - v: Content => Self(v), + /// The nesting level of this element. + fn level(&self) -> NonZeroUsize { + NonZeroUsize::ONE + } + + /// Constructs the default prefix given the formatted numbering. + fn prefix(&self, numbers: Content) -> Content; + + /// The body of the entry. + fn body(&self) -> Content; } -/// Represents each entry line in an outline, including the reference to the -/// outlined element, its page number, and the filler content between both. +/// Represents an entry line in an outline. /// -/// This element is intended for use with show rules to control the appearance -/// of outlines. To customize an entry's line, you can build it from scratch by -/// accessing the `level`, `element`, `body`, `fill` and `page` fields on the -/// entry. -/// -/// ```example -/// #set heading(numbering: "1.") -/// -/// #show outline.entry.where( -/// level: 1 -/// ): it => { -/// v(12pt, weak: true) -/// strong(it) -/// } -/// -/// #outline(indent: auto) -/// -/// = Introduction -/// = Background -/// == History -/// == State of the Art -/// = Analysis -/// == Setup -/// ``` -#[elem(name = "entry", title = "Outline Entry", Show)] +/// 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", Show)] pub struct OutlineEntry { /// The nesting level of this outline entry. Starts at `{1}` for top-level /// entries. @@ -413,90 +376,206 @@ pub struct OutlineEntry { pub level: NonZeroUsize, /// The element this entry refers to. Its location will be available - /// through the [`location`]($content.location) method on content + /// through the [`location`]($content.location) method on the content /// and can be [linked]($link) to. #[required] pub element: Content, - /// The content which is displayed in place of the referred element at its - /// entry in the outline. For a heading, this would be its number followed - /// by the heading's title, for example. - #[required] - pub body: Content, - - /// The content used to fill the space between the element's outline and - /// its page number, as defined by the outline element this entry is - /// located in. When `{none}`, empty space is inserted in that gap instead. + /// Content to fill the space between the title and the page number. Can be + /// set to `{none}` to disable filling. /// - /// Note that, when using show rules to override outline entries, it is - /// recommended to wrap the filling content in a [`box`] with fractional - /// width. For example, `{box(width: 1fr, repeat[-])}` would show precisely - /// as many `-` characters as necessary to fill a particular gap. - #[required] + /// The `fill` will be placed into a fractionally sized box that spans the + /// space between the entry's body and the page number. When using show + /// rules to override outline entries, it is thus recommended to wrap the + /// fill in a [`box`] with fractional width, i.e. + /// `{box(width: 1fr, it.fill}`. + /// + /// When using [`repeat`], the [`gap`]($repeat.gap) property can be useful + /// to tweak the visual weight of the fill. + /// + /// ```example + /// #set outline.entry(fill: line(length: 100%)) + /// #outline() + /// + /// = A New Beginning + /// ``` + #[borrowed] + #[default(Some( + RepeatElem::new(TextElem::packed(".")) + .with_gap(Em::new(0.15).into()) + .pack() + ))] pub fill: Option, - /// The page number of the element this entry links to, formatted with the - /// numbering set for the referenced page. - #[required] - pub page: Content, -} - -impl OutlineEntry { - /// Generates an OutlineEntry from the given element, if possible (errors if - /// the element does not implement `Outlinable`). If the element should not - /// be outlined (e.g. heading with 'outlined: false'), does not generate an - /// entry instance (returns `Ok(None)`). - fn from_outlinable( - engine: &mut Engine, - span: Span, - elem: Content, - fill: Option, - styles: StyleChain, - ) -> SourceResult> { - let Some(outlinable) = elem.with::() else { - bail!(span, "cannot outline {}", elem.func().name()); - }; - - let Some(body) = outlinable.outline(engine, styles)? else { - return Ok(None); - }; - - let location = elem.location().unwrap(); - let page_numbering = engine - .introspector - .page_numbering(location) - .cloned() - .unwrap_or_else(|| NumberingPattern::from_str("1").unwrap().into()); - - let page = Counter::new(CounterKey::Page).display_at_loc( - engine, - location, - styles, - &page_numbering, - )?; - - Ok(Some(Self::new(outlinable.level(), elem, body, fill, page))) - } + /// Lets outline entries access the outline they are part of. This is a bit + /// of a hack and should be superseded by a proper ancestry mechanism. + #[ghost] + #[internal] + pub parent: Option>, } impl Show for Packed { #[typst_macros::time(name = "outline.entry", span = self.span())] - fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let mut seq = vec![]; - let elem = self.element(); + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { + let span = self.span(); + let context = Context::new(None, Some(styles)); + let context = context.track(); - // In case a user constructs an outline entry with an arbitrary element. - let Some(location) = elem.location() else { - if elem.can::() && elem.can::() { - bail!( - self.span(), "{} must have a location", elem.func().name(); - hint: "try using a query or a show rule to customize the outline.entry instead", - ) - } else { - bail!(self.span(), "cannot outline {}", elem.func().name()) + let prefix = self.prefix(engine, context, span)?; + let inner = self.inner(engine, context, span)?; + 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))) + } +} + +#[scope] +impl OutlineEntry { + /// A helper function for producing an indented entry layout: Lays out a + /// prefix and the rest of the entry in an indent-aware way. + /// + /// If the parent outline's [`indent`]($outline.indent) is `{auto}`, the + /// inner content of all entries at level `N` is aligned with the prefix of + /// all entries at level `N + 1`, leaving at least `gap` space between the + /// prefix and inner parts. Furthermore, the `inner` contents of all entries + /// at the same level are aligned. + /// + /// If the outline's indent is a fixed value or a function, the prefixes are + /// indented, but the inner contents are simply inset from the prefix by the + /// specified `gap`, rather than aligning outline-wide. + #[func(contextual)] + pub fn indented( + &self, + engine: &mut Engine, + context: Tracked, + span: Span, + /// The `prefix` is aligned with the `inner` content of entries that + /// have level one less. + /// + /// In the default show rule, this is just `it.prefix()`, but it can be + /// freely customized. + prefix: Option, + /// The formatted inner content of the entry. + /// + /// In the default show rule, this is just `it.inner()`, but it can be + /// freely customized. + inner: Content, + /// The gap between the prefix and the inner content. + #[named] + #[default(Em::new(0.5).into())] + gap: Length, + ) -> SourceResult { + let styles = context.styles().at(span)?; + let outline = Self::parent_in(styles) + .ok_or("must be called within the context of an outline") + .at(span)?; + let outline_loc = outline.location().unwrap(); + + let prefix_width = prefix + .as_ref() + .map(|prefix| measure_prefix(engine, prefix, outline_loc, styles)) + .transpose()?; + let prefix_inset = prefix_width.map(|w| w + gap.resolve(styles)); + + let indent = outline.indent(styles); + let (base_indent, hanging_indent) = match &indent { + Smart::Auto => compute_auto_indents( + engine.introspector, + outline_loc, + styles, + self.level, + prefix_inset, + ), + Smart::Custom(amount) => { + let base = amount.resolve(engine, context, self.level, span)?; + (base, prefix_inset) } }; + let body = if let ( + Some(prefix), + Some(prefix_width), + Some(prefix_inset), + Some(hanging_indent), + ) = (prefix, prefix_width, prefix_inset, hanging_indent) + { + // Save information about our prefix that other outline entries + // can query for (within `compute_auto_indent`) to align + // themselves). + let mut seq = Vec::with_capacity(5); + if indent.is_auto() { + seq.push(PrefixInfo::new(outline_loc, self.level, prefix_inset).pack()); + } + + // Dedent the prefix by the amount of hanging indent and then skip + // ahead so that the inner contents are aligned. + seq.extend([ + HElem::new((-hanging_indent).into()).pack(), + prefix, + HElem::new((hanging_indent - prefix_width).into()).pack(), + inner, + ]); + Content::sequence(seq) + } else { + inner + }; + + let inset = Sides::default().with( + TextElem::dir_in(styles).start(), + Some(base_indent + Rel::from(hanging_indent.unwrap_or_default())), + ); + + Ok(BlockElem::new() + .with_inset(inset) + .with_body(Some(BlockBody::Content(body))) + .pack() + .spanned(span)) + } + + /// Formats the element's numbering (if any). + /// + /// This also appends the element's supplement in case of figures or + /// equations. For instance, it would output `1.1` for a heading, but + /// `Figure 1` for a figure, as is usual for outlines. + #[func(contextual)] + pub fn prefix( + &self, + engine: &mut Engine, + context: Tracked, + span: Span, + ) -> SourceResult> { + let outlinable = self.outlinable().at(span)?; + let Some(numbering) = outlinable.numbering() else { return Ok(None) }; + let loc = self.element_location().at(span)?; + let styles = context.styles().at(span)?; + let numbers = + outlinable.counter().display_at_loc(engine, loc, styles, numbering)?; + Ok(Some(outlinable.prefix(numbers))) + } + + /// Creates the default inner content of the entry. + /// + /// This includes the body, the fill, and page number. + #[func(contextual)] + pub fn inner( + &self, + engine: &mut Engine, + context: Tracked, + span: Span, + ) -> SourceResult { + let styles = context.styles().at(span)?; + + let mut seq = vec![]; + // Isolate the entry body in RTL because the page number is typically // LTR. I'm not sure whether LTR should conceptually also be isolated, // but in any case we don't do it for now because the text shaping @@ -512,32 +591,174 @@ impl Show for Packed { seq.push(TextElem::packed("\u{202B}")); } - seq.push(self.body().clone().linked(Destination::Location(location))); + seq.push(self.body().at(span)?); if rtl { // "Pop Directional Formatting" seq.push(TextElem::packed("\u{202C}")); } - // Add filler symbols between the section name and page number. - if let Some(filler) = self.fill() { + // Add the filler between the section name and page number. + if let Some(filler) = self.fill(styles) { seq.push(SpaceElem::shared().clone()); seq.push( BoxElem::new() .with_body(Some(filler.clone())) .with_width(Fr::one().into()) .pack() - .spanned(self.span()), + .spanned(span), ); seq.push(SpaceElem::shared().clone()); } else { - seq.push(HElem::new(Fr::one().into()).pack().spanned(self.span())); + seq.push(HElem::new(Fr::one().into()).pack().spanned(span)); } - // Add the page number. - let page = self.page().clone().linked(Destination::Location(location)); - seq.push(page); + // Add the page number. The word joiner in front ensures that the page + // number doesn't stand alone in its line. + seq.push(TextElem::packed("\u{2060}")); + seq.push(self.page(engine, context, span)?); Ok(Content::sequence(seq)) } + + /// The content which is displayed in place of the referred element at its + /// entry in the outline. For a heading, this is its + /// [`body`]($heading.body), for a figure a caption, and for equations it is + /// empty. + #[func] + pub fn body(&self) -> StrResult { + Ok(self.outlinable()?.body()) + } + + /// The page number of this entry's element, formatted with the numbering + /// set for the referenced page. + #[func(contextual)] + pub fn page( + &self, + engine: &mut Engine, + context: Tracked, + span: Span, + ) -> SourceResult { + let loc = self.element_location().at(span)?; + let styles = context.styles().at(span)?; + let numbering = engine + .introspector + .page_numbering(loc) + .cloned() + .unwrap_or_else(|| NumberingPattern::from_str("1").unwrap().into()); + Counter::new(CounterKey::Page).display_at_loc(engine, loc, styles, &numbering) + } +} + +impl OutlineEntry { + fn outlinable(&self) -> StrResult<&dyn Outlinable> { + self.element + .with::() + .ok_or_else(|| error!("cannot outline {}", self.element.func().name())) + } + + fn element_location(&self) -> HintedStrResult { + let elem = &self.element; + elem.location().ok_or_else(|| { + if elem.can::() && elem.can::() { + error!( + "{} must have a location", elem.func().name(); + hint: "try using a show rule to customize the outline.entry instead", + ) + } else { + error!("cannot outline {}", elem.func().name()) + } + }) + } +} + +cast! { + OutlineEntry, + v: Content => v.unpack::().map_err(|_| "expected outline entry")? +} + +/// Measures the width of a prefix. +fn measure_prefix( + engine: &mut Engine, + prefix: &Content, + loc: Location, + styles: StyleChain, +) -> SourceResult { + let pod = Region::new(Axes::splat(Abs::inf()), Axes::splat(false)); + let link = LocatorLink::measure(loc); + Ok((engine.routines.layout_frame)(engine, prefix, Locator::link(&link), styles, pod)? + .width()) +} + +/// Compute the base indent and hanging indent for an auto-indented outline +/// entry of the given level, with the given prefix inset. +fn compute_auto_indents( + introspector: Tracked, + outline_loc: Location, + styles: StyleChain, + level: NonZeroUsize, + prefix_inset: Option, +) -> (Rel, Option) { + let indents = query_prefix_widths(introspector, outline_loc); + + let fallback = Em::new(1.2).resolve(styles); + let get = |i: usize| indents.get(i).copied().flatten().unwrap_or(fallback); + + let last = level.get() - 1; + let base: Abs = (0..last).map(get).sum(); + let hang = prefix_inset.map(|p| p.max(get(last))); + + (base.into(), hang) +} + +/// Determines the maximum prefix inset (prefix width + gap) at each outline +/// level, for the outline with the given `loc`. Levels for which there is no +/// information available yield `None`. +#[comemo::memoize] +fn query_prefix_widths( + introspector: Tracked, + outline_loc: Location, +) -> SmallVec<[Option; 4]> { + let mut widths = SmallVec::<[Option; 4]>::new(); + let elems = introspector.query(&select_where!(PrefixInfo, Key => outline_loc)); + for elem in &elems { + let info = elem.to_packed::().unwrap(); + let level = info.level.get(); + if widths.len() < level { + widths.resize(level, None); + } + widths[level - 1].get_or_insert(info.inset).set_max(info.inset); + } + widths +} + +/// Helper type for introspection-based prefix alignment. +#[elem(Construct, Locatable, Show)] +struct PrefixInfo { + /// The location of the outline this prefix is part of. This is used to + /// scope prefix computations to a specific outline. + #[required] + key: Location, + + /// The level of this prefix's entry. + #[required] + #[internal] + level: NonZeroUsize, + + /// The width of the prefix, including the gap. + #[required] + #[internal] + inset: Abs, +} + +impl Construct for PrefixInfo { + fn construct(_: &mut Engine, args: &mut Args) -> SourceResult { + 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/par.rs b/crates/typst-library/src/model/par.rs index 8b82abdf7..cf31b5195 100644 --- a/crates/typst-library/src/model/par.rs +++ b/crates/typst-library/src/model/par.rs @@ -1,22 +1,78 @@ -use std::fmt::{self, Debug, Formatter}; - use typst_utils::singleton; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - elem, scope, Args, Cast, Construct, Content, NativeElement, Packed, Set, Smart, - StyleVec, Unlabellable, + cast, dict, elem, scope, Args, Cast, Construct, Content, Dict, NativeElement, Packed, + Smart, Unlabellable, Value, }; use crate::introspection::{Count, CounterUpdate, Locatable}; use crate::layout::{Em, HAlignment, Length, OuterHAlignment}; use crate::model::Numbering; -/// Arranges text, spacing and inline-level elements into a paragraph. +/// A logical subdivison of textual content. /// -/// Although this function is primarily used in set rules to affect paragraph -/// properties, it can also be used to explicitly render its argument onto a -/// paragraph of its own. +/// Typst automatically collects _inline-level_ elements into paragraphs. +/// Inline-level elements include [text], [horizontal spacing]($h), +/// [boxes]($box), and [inline equations]($math.equation). +/// +/// To separate paragraphs, use a blank line (or an explicit [`parbreak`]). +/// Paragraphs are also automatically interrupted by any block-level element +/// (like [`block`], [`place`], or anything that shows itself as one of these). +/// +/// The `par` element is primarily used in set rules to affect paragraph +/// properties, but it can also be used to explicitly display its argument as a +/// paragraph of its own. Then, the paragraph's body may not contain any +/// block-level content. +/// +/// # Boxes and blocks +/// As explained above, usually paragraphs only contain inline-level content. +/// However, you can integrate any kind of block-level content into a paragraph +/// by wrapping it in a [`box`]. +/// +/// Conversely, you can separate inline-level content from a paragraph by +/// wrapping it in a [`block`]. In this case, it will not become part of any +/// paragraph at all. Read the following section for an explanation of why that +/// matters and how it differs from just adding paragraph breaks around the +/// content. +/// +/// # What becomes a paragraph? +/// When you add inline-level content to your document, Typst will automatically +/// wrap it in paragraphs. However, a typical document also contains some text +/// that is not semantically part of a paragraph, for example in a heading or +/// caption. +/// +/// The rules for when Typst wraps inline-level content in a paragraph are as +/// follows: +/// +/// - All text at the root of a document is wrapped in paragraphs. +/// +/// - Text in a container (like a `block`) is only wrapped in a paragraph if the +/// container holds any block-level content. If all of the contents are +/// inline-level, no paragraph is created. +/// +/// In the laid-out document, it's not immediately visible whether text became +/// part of a paragraph. However, it is still important for various reasons: +/// +/// - Certain paragraph styling like `first-line-indent` will only apply to +/// proper paragraphs, not any text. Similarly, `par` show rules of course +/// only trigger on paragraphs. +/// +/// - A proper distinction between paragraphs and other text helps people who +/// rely on assistive technologies (such as screen readers) navigate and +/// understand the document properly. Currently, this only applies to HTML +/// export since Typst does not yet output accessible PDFs, but support for +/// this is planned for the near future. +/// +/// - HTML export will generate a `

` tag only for paragraphs. +/// +/// When creating custom reusable components, you can and should take charge +/// over whether Typst creates paragraphs. By wrapping text in a [`block`] +/// instead of just adding paragraph breaks around it, you can force the absence +/// of a paragraph. Conversely, by adding a [`parbreak`] after some content in a +/// container, you can force it to become a paragraph even if it's just one +/// word. This is, for example, what [non-`tight`]($list.tight) lists do to +/// force their items to become paragraphs. /// /// # Example /// ```example @@ -37,7 +93,7 @@ use crate::model::Numbering; /// let $a$ be the smallest of the /// three integers. Then, we ... /// ``` -#[elem(scope, title = "Paragraph", Debug, Construct)] +#[elem(scope, title = "Paragraph")] pub struct ParElem { /// The spacing between lines. /// @@ -53,7 +109,6 @@ pub struct ParElem { /// distribution of the top- and bottom-edge values affects the bounds of /// the first and last line. #[resolve] - #[ghost] #[default(Em::new(0.65).into())] pub leading: Length, @@ -68,7 +123,6 @@ pub struct ParElem { /// takes precedence over the paragraph spacing. Headings, for instance, /// reduce the spacing below them by default for a better look. #[resolve] - #[ghost] #[default(Em::new(1.2).into())] pub spacing: Length, @@ -81,7 +135,6 @@ pub struct ParElem { /// Note that the current [alignment]($align.alignment) still has an effect /// on the placement of the last line except if it ends with a /// [justified line break]($linebreak.justify). - #[ghost] #[default(false)] pub justify: bool, @@ -106,35 +159,66 @@ pub struct ParElem { /// challenging to break in a visually /// pleasing way. /// ``` - #[ghost] pub linebreaks: Smart, /// The indent the first line of a paragraph should have. /// - /// Only the first line of a consecutive paragraph will be indented (not - /// the first one in a block or on the page). + /// By default, only the first line of a consecutive paragraph will be + /// indented (not the first one in the document or container, and not + /// paragraphs immediately following other block-level elements). + /// + /// If you want to indent all paragraphs instead, you can pass a dictionary + /// containing the `amount` of indent as a length and the pair + /// `{all: true}`. When `all` is omitted from the dictionary, it defaults to + /// `{false}`. /// /// By typographic convention, paragraph breaks are indicated either by some - /// space between paragraphs or by indented first lines. Consider reducing - /// the [paragraph spacing]($block.spacing) to the [`leading`]($par.leading) - /// when using this property (e.g. using `[#set par(spacing: 0.65em)]`). - #[ghost] - pub first_line_indent: Length, + /// space between paragraphs or by indented first lines. Consider + /// - reducing the [paragraph `spacing`]($par.spacing) to the + /// [`leading`]($par.leading) using `{set par(spacing: 0.65em)}` + /// - increasing the [block `spacing`]($block.spacing) (which inherits the + /// paragraph spacing by default) to the original paragraph spacing using + /// `{set block(spacing: 1.2em)}` + /// + /// ```example + /// #set block(spacing: 1.2em) + /// #set par( + /// first-line-indent: 1.5em, + /// spacing: 0.65em, + /// ) + /// + /// The first paragraph is not affected + /// by the indent. + /// + /// But the second paragraph is. + /// + /// #line(length: 100%) + /// + /// #set par(first-line-indent: ( + /// amount: 1.5em, + /// all: true, + /// )) + /// + /// Now all paragraphs are affected + /// by the first line indent. + /// + /// Even the first one. + /// ``` + pub first_line_indent: FirstLineIndent, - /// The indent all but the first line of a paragraph should have. - #[ghost] + /// The indent that all but the first line of a paragraph should have. + /// + /// ```example + /// #set par(hanging-indent: 1em) + /// + /// #lorem(15) + /// ``` #[resolve] pub hanging_indent: Length, /// The contents of the paragraph. - #[external] #[required] pub body: Content, - - /// The paragraph's children. - #[internal] - #[variadic] - pub children: StyleVec, } #[scope] @@ -143,28 +227,6 @@ impl ParElem { type ParLine; } -impl Construct for ParElem { - fn construct(engine: &mut Engine, args: &mut Args) -> SourceResult { - // The paragraph constructor is special: It doesn't create a paragraph - // element. Instead, it just ensures that the passed content lives in a - // separate paragraph and styles it. - let styles = Self::set(engine, args)?; - let body = args.expect::("body")?; - Ok(Content::sequence([ - ParbreakElem::shared().clone(), - body.styled_with_map(styles), - ParbreakElem::shared().clone(), - ])) - } -} - -impl Debug for ParElem { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "Par ")?; - self.children.fmt(f) - } -} - /// How to determine line breaks in a paragraph. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] pub enum Linebreaks { @@ -177,6 +239,36 @@ pub enum Linebreaks { Optimized, } +/// Configuration for first line indent. +#[derive(Debug, Default, Copy, Clone, PartialEq, Hash)] +pub struct FirstLineIndent { + /// The amount of indent. + pub amount: Length, + /// Whether to indent all paragraphs, not just consecutive ones. + pub all: bool, +} + +cast! { + FirstLineIndent, + self => Value::Dict(self.into()), + amount: Length => Self { amount, all: false }, + mut dict: Dict => { + let amount = dict.take("amount")?.cast()?; + let all = dict.take("all").ok().map(|v| v.cast()).transpose()?.unwrap_or(false); + dict.finish(&["amount", "all"])?; + Self { amount, all } + }, +} + +impl From for Dict { + fn from(indent: FirstLineIndent) -> Self { + dict! { + "amount" => indent.amount, + "all" => indent.all, + } + } +} + /// A paragraph break. /// /// This starts a new paragraph. Especially useful when used within code like diff --git a/crates/typst-library/src/model/quote.rs b/crates/typst-library/src/model/quote.rs index 110825f13..919ab12c7 100644 --- a/crates/typst-library/src/model/quote.rs +++ b/crates/typst-library/src/model/quote.rs @@ -2,13 +2,14 @@ use crate::diag::SourceResult; use crate::engine::Engine; use crate::foundations::{ cast, elem, Content, Depth, Label, NativeElement, Packed, Show, ShowSet, Smart, - StyleChain, Styles, + StyleChain, Styles, TargetElem, }; +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}; +use crate::model::{CitationForm, CiteElem, Destination, LinkElem, LinkTarget}; use crate::text::{SmartQuoteElem, SmartQuotes, SpaceElem, TextElem}; /// Displays a quote alongside an optional attribution. @@ -156,8 +157,9 @@ cast! { 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 mut realized = self.body.clone(); let block = self.block(styles); + let html = TargetElem::target_in(styles).is_html(); if self.quotes(styles) == Smart::Custom(true) || !block { let quotes = SmartQuotes::get( @@ -171,50 +173,69 @@ impl Show for Packed { let Depth(depth) = QuoteElem::depth_in(styles); let double = depth % 2 == 0; - // Add zero-width weak spacing to make the quotes "sticky". - let hole = HElem::hole().pack(); + 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)), - hole.clone(), realized, - hole, TextElem::packed(quotes.close(double)), ]) .styled(QuoteElem::set_depth(Depth(1))); } + let attribution = self.attribution(styles); + if block { - realized = BlockElem::new() - .with_body(Some(BlockBody::Content(realized))) - .pack() - .spanned(self.span()); - - if let Some(attribution) = self.attribution(styles).as_ref() { - let mut seq = vec![TextElem::packed('—'), SpaceElem::shared().clone()]; - - match attribution { - Attribution::Content(content) => { - seq.push(content.clone()); - } - Attribution::Label(label) => { - seq.push( - CiteElem::new(*label) - .with_form(Some(CitationForm::Prose)) - .pack() - .spanned(self.span()), - ); + 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()); - // Use v(0.9em, weak: true) bring the attribution closer to the - // quote. - let gap = Spacing::Rel(Em::new(0.9).into()); - let v = VElem::new(gap).with_weak(true).pack(); - realized += v + Content::sequence(seq).aligned(Alignment::END); + if let Some(attribution) = attribution.as_ref() { + 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); + } } - realized = PadElem::new(realized).pack(); - } else if let Some(Attribution::Label(label)) = self.attribution(styles) { + 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()); } diff --git a/crates/typst-library/src/model/reference.rs b/crates/typst-library/src/model/reference.rs index 96aa2117d..316617688 100644 --- a/crates/typst-library/src/model/reference.rs +++ b/crates/typst-library/src/model/reference.rs @@ -182,9 +182,8 @@ impl Synthesize for Packed { elem.push_citation(Some(citation)); elem.push_element(None); - let target = *elem.target(); - if !BibliographyElem::has(engine, target) { - if let Ok(found) = engine.introspector.query_label(target).cloned() { + if !BibliographyElem::has(engine, elem.target) { + if let Ok(found) = engine.introspector.query_label(elem.target).cloned() { elem.push_element(Some(found)); return Ok(()); } @@ -197,8 +196,7 @@ 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 { - let target = *self.target(); - let elem = engine.introspector.query_label(target); + let elem = engine.introspector.query_label(self.target); let span = self.span(); let form = self.form(styles); @@ -229,7 +227,7 @@ impl Show for Packed { } // RefForm::Normal - if BibliographyElem::has(engine, target) { + if BibliographyElem::has(engine, self.target) { if elem.is_ok() { bail!(span, "label occurs in the document and its bibliography"); } @@ -240,7 +238,7 @@ impl Show for Packed { let elem = elem.at(span)?; if let Some(footnote) = elem.to_packed::() { - return Ok(footnote.into_ref(target).pack().spanned(span)); + return Ok(footnote.into_ref(self.target).pack().spanned(span)); } let elem = elem.clone(); @@ -319,7 +317,7 @@ fn to_citation( engine: &mut Engine, styles: StyleChain, ) -> SourceResult> { - let mut elem = Packed::new(CiteElem::new(*reference.target()).with_supplement( + let mut elem = Packed::new(CiteElem::new(reference.target).with_supplement( match reference.supplement(styles).clone() { Smart::Custom(Some(Supplement::Content(content))) => Some(content), _ => None, diff --git a/crates/typst-library/src/model/table.rs b/crates/typst-library/src/model/table.rs index 7dfaf45d7..82c1cc08b 100644 --- a/crates/typst-library/src/model/table.rs +++ b/crates/typst-library/src/model/table.rs @@ -7,7 +7,11 @@ 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::Locator; +use crate::layout::grid::resolve::{table_to_cellgrid, Cell, CellGrid, Entry}; use crate::layout::{ show_grid_cell, Abs, Alignment, BlockElem, Celled, GridCell, GridFooter, GridHLine, GridHeader, GridVLine, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, @@ -258,11 +262,65 @@ 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(styles)) { + attrs.push(attr::colspan, colspan); + } + if let Some(rowspan) = span(cell.rowspan(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.cols.len()).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)) + }; + + let footer = grid.footer.map(|ft| { + let rows = rows.drain(ft.unwrap().start..); + elem(tag::tfoot, Content::sequence(rows.map(|row| tr(tag::td, row)))) + }); + let header = grid.header.map(|hd| { + let rows = rows.drain(..hd.unwrap().end); + elem(tag::thead, Content::sequence(rows.map(|row| tr(tag::th, row)))) + }); + + let mut body = Content::sequence(rows.into_iter().map(|row| 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, _: StyleChain) -> SourceResult { - Ok(BlockElem::multi_layouter(self.clone(), engine.routines.layout_table) - .pack() - .spanned(self.span())) + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { + Ok(if TargetElem::target_in(styles).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())) } } @@ -706,7 +764,7 @@ cast! { impl Show for Packed { fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult { - show_grid_cell(self.body().clone(), self.inset(styles), self.align(styles)) + show_grid_cell(self.body.clone(), self.inset(styles), self.align(styles)) } } diff --git a/crates/typst-library/src/model/terms.rs b/crates/typst-library/src/model/terms.rs index 13aa8c6d5..e197ff318 100644 --- a/crates/typst-library/src/model/terms.rs +++ b/crates/typst-library/src/model/terms.rs @@ -1,4 +1,4 @@ -use typst_utils::Numeric; +use typst_utils::{Get, Numeric}; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; @@ -7,8 +7,8 @@ use crate::foundations::{ Styles, TargetElem, }; use crate::html::{tag, HtmlElem}; -use crate::layout::{Dir, Em, HElem, Length, Sides, StackChild, StackElem, VElem}; -use crate::model::{ListItemLike, ListLike, ParElem}; +use crate::layout::{Em, HElem, Length, Sides, StackChild, StackElem, VElem}; +use crate::model::{ListItemLike, ListLike, ParElem, ParbreakElem}; use crate::text::TextElem; /// A list of terms and their descriptions. @@ -105,6 +105,11 @@ pub struct TermsElem { /// ``` #[variadic] pub children: Vec>, + + /// Whether we are currently within a term list. + #[internal] + #[ghost] + pub within: bool, } #[scope] @@ -116,17 +121,25 @@ impl TermsElem { impl Show for Packed { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { let span = self.span(); + let tight = self.tight(styles); + if TargetElem::target_in(styles).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(item.description.clone())) + .with_body(Some(description)) .pack() .spanned(item.description.span()), ] @@ -139,7 +152,7 @@ impl Show for Packed { let indent = self.indent(styles); let hanging_indent = self.hanging_indent(styles); let gutter = self.spacing(styles).unwrap_or_else(|| { - if self.tight(styles) { + if tight { ParElem::leading_in(styles).into() } else { ParElem::spacing_in(styles).into() @@ -151,29 +164,31 @@ impl Show for Packed { .then(|| HElem::new((-hanging_indent).into()).pack().spanned(span)); let mut children = vec![]; - for child in self.children().iter() { + for child in self.children.iter() { let mut seq = vec![]; seq.extend(unpad.clone()); - seq.push(child.term().clone().strong()); + seq.push(child.term.clone().strong()); seq.push((*separator).clone()); - seq.push(child.description().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 mut padding = Sides::default(); - if TextElem::dir_in(styles) == Dir::LTR { - padding.left = pad.into(); - } else { - padding.right = pad.into(); - } + let padding = Sides::default().with(TextElem::dir_in(styles).start(), pad.into()); let mut realized = StackElem::new(children) .with_spacing(Some(gutter.into())) .pack() .spanned(span) - .padded(padding); + .padded(padding) + .styled(TermsElem::set_within(true)); - if self.tight(styles) { + if tight { let leading = ParElem::leading_in(styles); let spacing = VElem::new(leading.into()) .with_weak(true) diff --git a/crates/typst-library/src/pdf/embed.rs b/crates/typst-library/src/pdf/embed.rs new file mode 100644 index 000000000..f9ca3ca09 --- /dev/null +++ b/crates/typst-library/src/pdf/embed.rs @@ -0,0 +1,99 @@ +use ecow::EcoString; +use typst_syntax::Spanned; + +use crate::diag::{At, SourceResult}; +use crate::engine::Engine; +use crate::foundations::{elem, Bytes, Cast, Content, Derived, Packed, Show, StyleChain}; +use crate::introspection::Locatable; +use crate::World; + +/// A file that will be embedded into the output PDF. +/// +/// This can be used to distribute additional files that are related to the PDF +/// within it. PDF readers will display the files in a file listing. +/// +/// Some international standards use this mechanism to embed machine-readable +/// data (e.g., ZUGFeRD/Factur-X for invoices) that mirrors the visual content +/// of the PDF. +/// +/// # Example +/// ```typ +/// #pdf.embed( +/// "experiment.csv", +/// relationship: "supplement", +/// mime-type: "text/csv", +/// description: "Raw Oxygen readings from the Arctic experiment", +/// ) +/// ``` +/// +/// # Notes +/// - 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)] +pub struct EmbedElem { + /// Path of the file to be embedded. + /// + /// Must always be specified, but is only read from if no data is provided + /// in the following argument. + /// + /// For more details about paths, see the [Paths section]($syntax/#paths). + #[required] + #[parse( + let Spanned { v: path, span } = + args.expect::>("path")?; + let id = span.resolve_path(&path).at(span)?; + // The derived part is the project-relative resolved path. + let resolved = id.vpath().as_rootless_path().to_string_lossy().replace("\\", "/").into(); + Derived::new(path.clone(), resolved) + )] + #[borrowed] + pub path: Derived, + + /// Raw file data, optionally. + /// + /// If omitted, the data is read from the specified path. + #[positional] + // Not actually required as an argument, but always present as a field. + // We can't distinguish between the two at the moment. + #[required] + #[parse( + match args.find::()? { + Some(data) => data, + None => engine.world.file(id).at(span)?, + } + )] + pub data: Bytes, + + /// The relationship of the embedded file to the document. + /// + /// Ignored if export doesn't target PDF/A-3. + pub relationship: Option, + + /// The MIME type of the embedded file. + #[borrowed] + pub mime_type: Option, + + /// A description for the embedded file. + #[borrowed] + pub description: Option, +} + +impl Show for Packed { + fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { + Ok(Content::empty()) + } +} + +/// The relationship of an embedded file with the document. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum EmbeddedFileRelationship { + /// The PDF document was created from the source file. + Source, + /// The file was used to derive a visual presentation in the PDF. + Data, + /// An alternative representation of the document. + Alternative, + /// Additional resources for the document. + Supplement, +} diff --git a/crates/typst-library/src/pdf/mod.rs b/crates/typst-library/src/pdf/mod.rs new file mode 100644 index 000000000..669835d4c --- /dev/null +++ b/crates/typst-library/src/pdf/mod.rs @@ -0,0 +1,24 @@ +//! PDF-specific functionality. + +mod embed; + +pub use self::embed::*; + +use crate::foundations::{category, Category, Module, Scope}; + +/// PDF-specific functionality. +#[category] +pub static PDF: Category; + +/// Hook up the `pdf` module. +pub(super) fn define(global: &mut Scope) { + global.category(PDF); + global.define_module(module()); +} + +/// Hook up all `pdf` definitions. +pub fn module() -> Module { + let mut scope = Scope::deduplicating(); + scope.define_elem::(); + Module::new("pdf", scope) +} diff --git a/crates/typst-library/src/routines.rs b/crates/typst-library/src/routines.rs index a11268604..b283052a4 100644 --- a/crates/typst-library/src/routines.rs +++ b/crates/typst-library/src/routines.rs @@ -10,8 +10,7 @@ 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, StyleVec, - Styles, Value, + Args, Cast, Closure, Content, Context, Func, Packed, Scope, StyleChain, Styles, Value, }; use crate::introspection::{Introspector, Locator, SplitLocator}; use crate::layout::{ @@ -104,26 +103,6 @@ routines! { region: Region, ) -> SourceResult - /// Lays out inline content. - fn layout_inline( - engine: &mut Engine, - children: &StyleVec, - locator: Locator, - styles: StyleChain, - consecutive: bool, - region: Size, - expand: bool, - ) -> SourceResult - - /// Lays out a [`BoxElem`]. - fn layout_box( - elem: &Packed, - engine: &mut Engine, - locator: Locator, - styles: StyleChain, - region: Size, - ) -> SourceResult - /// Lays out a [`ListElem`]. fn layout_list( elem: &Packed, @@ -348,17 +327,62 @@ 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), - /// A nested realization in a container (e.g. a `block`). - LayoutFragment, + /// 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), + /// 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), - /// A nested realization in a container (e.g. a `block`). - HtmlFragment, + /// 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), /// A realization within math. Math, } +impl RealizationKind<'_> { + /// It this a realization for HTML export? + pub fn is_html(&self) -> bool { + 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(_)) + } + + /// 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), + _ => None, + } + } + + /// 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), + _ => None, + } + } +} + +/// The kind of fragment output that realization produced. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum FragmentKind { + /// The fragment's contents were fully inline, and as a result, the output + /// elements are too. + Inline, + /// The fragment contained non-inline content, so inline content was forced + /// into paragraphs, and as a result, the output elements are not inline. + Block, +} + /// Temporary storage arenas for lifetime extension during realization. /// /// Must be kept live while the content returned from realization is processed. diff --git a/crates/typst-library/src/text/deco.rs b/crates/typst-library/src/text/deco.rs index 5da7ecec4..485d0edcf 100644 --- a/crates/typst-library/src/text/deco.rs +++ b/crates/typst-library/src/text/deco.rs @@ -81,7 +81,7 @@ pub struct UnderlineElem { impl Show for Packed { #[typst_macros::time(name = "underline", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(TextElem::set_deco(smallvec![Decoration { + Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { line: DecoLine::Underline { stroke: self.stroke(styles).unwrap_or_default(), offset: self.offset(styles), @@ -173,7 +173,7 @@ pub struct OverlineElem { impl Show for Packed { #[typst_macros::time(name = "overline", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(TextElem::set_deco(smallvec![Decoration { + Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { line: DecoLine::Overline { stroke: self.stroke(styles).unwrap_or_default(), offset: self.offset(styles), @@ -250,7 +250,7 @@ pub struct StrikeElem { impl Show for Packed { #[typst_macros::time(name = "strike", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(TextElem::set_deco(smallvec![Decoration { + Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { // Note that we do not support evade option for strikethrough. line: DecoLine::Strikethrough { stroke: self.stroke(styles).unwrap_or_default(), @@ -345,7 +345,7 @@ pub struct HighlightElem { impl Show for Packed { #[typst_macros::time(name = "highlight", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(TextElem::set_deco(smallvec![Decoration { + Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { line: DecoLine::Highlight { fill: self.fill(styles), stroke: self diff --git a/crates/typst-library/src/text/font/color.rs b/crates/typst-library/src/text/font/color.rs index 25ba79d25..e2487e316 100644 --- a/crates/typst-library/src/text/font/color.rs +++ b/crates/typst-library/src/text/font/color.rs @@ -106,7 +106,7 @@ fn draw_raster_glyph( raster_image: ttf_parser::RasterGlyphImage, ) -> Option<()> { let image = Image::new( - Bytes::from(raster_image.data).into(), + Bytes::new(raster_image.data).to_vec(), RasterFormat::Png.into(), &Default::default(), ) @@ -184,7 +184,7 @@ fn draw_colr_glyph( let data = svg.end_document().into_bytes(); let image = Image::new( - Bytes::from(data).into(), + Bytes::new(data).into(), VectorFormat::Svg.into(), &Default::default(), ) diff --git a/crates/typst-library/src/text/font/exceptions.rs b/crates/typst-library/src/text/font/exceptions.rs index 465ec510c..00038c50c 100644 --- a/crates/typst-library/src/text/font/exceptions.rs +++ b/crates/typst-library/src/text/font/exceptions.rs @@ -228,6 +228,8 @@ static EXCEPTION_MAP: phf::Map<&'static str, Exception> = phf::phf_map! { .style(FontStyle::Oblique), "NewCMSans10-Regular" => Exception::new() .family("New Computer Modern Sans"), + "NewCMSansMath-Regular" => Exception::new() + .family("New Computer Modern Sans Math"), "NewCMUncial08-Bold" => Exception::new() .family("New Computer Modern Uncial 08"), "NewCMUncial08-Book" => Exception::new() diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index 25ed009e9..edbd24139 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -555,6 +555,7 @@ pub struct TextElem { /// #lorem(10) /// ``` #[fold] + #[ghost] pub costs: Costs, /// Whether to apply kerning. @@ -754,11 +755,10 @@ pub struct TextElem { #[ghost] pub case: Option, - /// Whether small capital glyphs should be used. ("smcp") + /// Whether small capital glyphs should be used. ("smcp", "c2sc") #[internal] - #[default(false)] #[ghost] - pub smallcaps: bool, + pub smallcaps: Option, } impl TextElem { @@ -793,7 +793,7 @@ impl Construct for TextElem { impl PlainText for Packed { fn plain_text(&self, text: &mut EcoString) { - text.push_str(self.text()); + text.push_str(&self.text); } } @@ -1248,8 +1248,11 @@ pub fn features(styles: StyleChain) -> Vec { } // Features that are off by default in Harfbuzz are only added if enabled. - if TextElem::smallcaps_in(styles) { + if let Some(sc) = TextElem::smallcaps_in(styles) { feat(b"smcp", 1); + if sc == Smallcaps::All { + feat(b"c2sc", 1); + } } if TextElem::alternates_in(styles) { @@ -1431,3 +1434,13 @@ fn check_font_list(engine: &mut Engine, list: &Spanned) { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_text_elem_size() { + assert_eq!(std::mem::size_of::(), std::mem::size_of::()); + } +} diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs index 10a7cfee1..01d6d8f01 100644 --- a/crates/typst-library/src/text/raw.rs +++ b/crates/typst-library/src/text/raw.rs @@ -1,23 +1,25 @@ use std::cell::LazyCell; -use std::hash::Hash; use std::ops::Range; use std::sync::{Arc, LazyLock}; +use comemo::Tracked; use ecow::{eco_format, EcoString, EcoVec}; -use syntect::highlighting::{self as synt, Theme}; +use syntect::highlighting as synt; use syntect::parsing::{SyntaxDefinition, SyntaxSet, SyntaxSetBuilder}; use typst_syntax::{split_newlines, LinkedNode, Span, Spanned}; +use typst_utils::ManuallyHash; use unicode_segmentation::UnicodeSegmentation; use super::Lang; -use crate::diag::{At, FileError, HintedStrResult, SourceResult, StrResult}; +use crate::diag::{At, FileError, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, Args, Array, Bytes, Content, Fold, NativeElement, Packed, - PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, TargetElem, Value, + cast, elem, scope, Bytes, Content, Derived, NativeElement, OneOrMultiple, Packed, + PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, TargetElem, }; use crate::html::{tag, HtmlElem}; use crate::layout::{BlockBody, BlockElem, Em, HAlignment}; +use crate::loading::{DataSource, Load}; use crate::model::{Figurable, ParElem}; use crate::text::{ FontFamily, FontList, Hyphenate, LinebreakElem, LocalName, TextElem, TextSize, @@ -25,12 +27,6 @@ use crate::text::{ use crate::visualize::Color; use crate::World; -// Shorthand for highlighter closures. -type StyleFn<'a> = - &'a mut dyn FnMut(usize, &LinkedNode, Range, synt::Style) -> Content; -type LineFn<'a> = &'a mut dyn FnMut(usize, Range, &mut Vec); -type ThemeArgType = Smart>; - /// Raw text with optional syntax highlighting. /// /// Displays the text verbatim and in a monospace font. This is typically used @@ -186,9 +182,15 @@ pub struct RawElem { #[default(HAlignment::Start)] pub align: HAlignment, - /// One or multiple additional syntax definitions to load. The syntax - /// definitions should be in the - /// [`sublime-syntax` file format](https://www.sublimetext.com/docs/syntax.html). + /// Additional syntax definitions to load. The syntax definitions should be + /// in the [`sublime-syntax` file format](https://www.sublimetext.com/docs/syntax.html). + /// + /// You can pass any of the following values: + /// + /// - A path string to load a syntax file from the given path. For more + /// details about paths, see the [Paths section]($syntax/#paths). + /// - Raw bytes from which the syntax should be decoded. + /// - An array where each item is one the above. /// /// ````example /// #set raw(syntaxes: "SExpressions.sublime-syntax") @@ -201,22 +203,24 @@ pub struct RawElem { /// (* x (factorial (- x 1))))) /// ``` /// ```` - #[parse( - let (syntaxes, syntaxes_data) = parse_syntaxes(engine, args)?; - syntaxes - )] + #[parse(match args.named("syntaxes")? { + Some(sources) => Some(RawSyntax::load(engine.world, sources)?), + None => None, + })] #[fold] - pub syntaxes: SyntaxPaths, + pub syntaxes: Derived, Vec>, - /// The raw file buffers of syntax definition files. - #[internal] - #[parse(syntaxes_data)] - #[fold] - pub syntaxes_data: Vec, - - /// The theme to use for syntax highlighting. Theme files should be in the + /// The theme to use for syntax highlighting. Themes should be in the /// [`tmTheme` file format](https://www.sublimetext.com/docs/color_schemes_tmtheme.html). /// + /// You can pass any of the following values: + /// + /// - `{none}`: Disables syntax highlighting. + /// - `{auto}`: Highlights with Typst's default theme. + /// - A path string to load a theme file from the given path. For more + /// details about paths, see the [Paths section]($syntax/#paths). + /// - Raw bytes from which the theme should be decoded. + /// /// Applying a theme only affects the color of specifically highlighted /// text. It does not consider the theme's foreground and background /// properties, so that you retain control over the color of raw text. You @@ -224,8 +228,6 @@ pub struct RawElem { /// the background with a [filled block]($block.fill). You could also use /// the [`xml`] function to extract these properties from the theme. /// - /// Additionally, you can set the theme to `{none}` to disable highlighting. - /// /// ````example /// #set raw(theme: "halcyon.tmTheme") /// #show raw: it => block( @@ -240,18 +242,16 @@ pub struct RawElem { /// #let hi = "Hello World" /// ``` /// ```` - #[parse( - let (theme_path, theme_data) = parse_theme(engine, args)?; - theme_path - )] + #[parse(match args.named::>>>("theme")? { + Some(Spanned { v: Smart::Custom(Some(source)), span }) => Some(Smart::Custom( + Some(RawTheme::load(engine.world, Spanned::new(source, span))?) + )), + Some(Spanned { v: Smart::Custom(None), .. }) => Some(Smart::Custom(None)), + Some(Spanned { v: Smart::Auto, .. }) => Some(Smart::Auto), + None => None, + })] #[borrowed] - pub theme: ThemeArgType, - - /// The raw file buffer of syntax theme file. - #[internal] - #[parse(theme_data.map(Some))] - #[borrowed] - pub theme_data: Option, + pub theme: Smart>>, /// The size for a tab stop in spaces. A tab is replaced with enough spaces to /// align with the next multiple of the size. @@ -315,7 +315,7 @@ impl Packed { #[comemo::memoize] fn highlight(&self, styles: StyleChain) -> Vec> { let elem = self.as_ref(); - let lines = preprocess(elem.text(), styles, self.span()); + let lines = preprocess(&elem.text, styles, self.span()); let count = lines.len() as i64; let lang = elem @@ -325,9 +325,6 @@ impl Packed { .map(|s| s.to_lowercase()) .or(Some("txt".into())); - let extra_syntaxes = LazyCell::new(|| { - load_syntaxes(&elem.syntaxes(styles), &elem.syntaxes_data(styles)).unwrap() - }); let non_highlighted_result = |lines: EcoVec<(EcoString, Span)>| { lines.into_iter().enumerate().map(|(i, (line, line_span))| { Packed::new(RawLine::new( @@ -340,17 +337,13 @@ impl Packed { }) }; - let theme = elem.theme(styles).as_ref().as_ref().map(|theme_path| { - theme_path.as_ref().map(|path| { - load_theme(path, elem.theme_data(styles).as_ref().as_ref().unwrap()) - .unwrap() - }) - }); - let theme: &Theme = match theme { + let syntaxes = LazyCell::new(|| elem.syntaxes(styles)); + let theme: &synt::Theme = match elem.theme(styles) { Smart::Auto => &RAW_THEME, - Smart::Custom(Some(ref theme)) => theme, + Smart::Custom(Some(theme)) => theme.derived.get(), Smart::Custom(None) => return non_highlighted_result(lines).collect(), }; + let foreground = theme.settings.foreground.unwrap_or(synt::Color::BLACK); let mut seq = vec![]; @@ -391,13 +384,14 @@ impl Packed { ) .highlight(); } else if let Some((syntax_set, syntax)) = lang.and_then(|token| { - RAW_SYNTAXES - .find_syntax_by_token(&token) - .map(|syntax| (&*RAW_SYNTAXES, syntax)) - .or_else(|| { - extra_syntaxes - .find_syntax_by_token(&token) - .map(|syntax| (&**extra_syntaxes, syntax)) + // Prefer user-provided syntaxes over built-in ones. + syntaxes + .derived + .iter() + .map(|syntax| syntax.get()) + .chain(std::iter::once(&*RAW_SYNTAXES)) + .find_map(|set| { + set.find_syntax_by_token(&token).map(|syntax| (set, syntax)) }) }) { let mut highlighter = syntect::easy::HighlightLines::new(syntax, theme); @@ -496,7 +490,7 @@ impl Figurable for Packed {} impl PlainText for Packed { fn plain_text(&self, text: &mut EcoString) { - text.push_str(&self.text().get()); + text.push_str(&self.text.get()); } } @@ -532,6 +526,89 @@ cast! { v: EcoString => Self::Text(v), } +/// A loaded syntax. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct RawSyntax(Arc>); + +impl RawSyntax { + /// Load syntaxes from sources. + fn load( + world: Tracked, + sources: Spanned>, + ) -> SourceResult, Vec>> { + let data = sources.load(world)?; + let list = sources + .v + .0 + .iter() + .zip(&data) + .map(|(source, data)| Self::decode(source, data)) + .collect::>() + .at(sources.span)?; + Ok(Derived::new(sources.v, list)) + } + + /// Decode a syntax from a loaded source. + #[comemo::memoize] + #[typst_macros::time(name = "load syntaxes")] + fn decode(source: &DataSource, data: &Bytes) -> StrResult { + let src = data.as_str().map_err(FileError::from)?; + let syntax = SyntaxDefinition::load_from_str(src, false, None).map_err( + |err| match source { + DataSource::Path(path) => { + eco_format!("failed to parse syntax file `{path}` ({err})") + } + DataSource::Bytes(_) => { + eco_format!("failed to parse syntax ({err})") + } + }, + )?; + + let mut builder = SyntaxSetBuilder::new(); + builder.add(syntax); + + Ok(RawSyntax(Arc::new(ManuallyHash::new( + builder.build(), + typst_utils::hash128(data), + )))) + } + + /// Return the underlying syntax set. + fn get(&self) -> &SyntaxSet { + self.0.as_ref() + } +} + +/// A loaded syntect theme. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct RawTheme(Arc>); + +impl RawTheme { + /// Load a theme from a data source. + fn load( + world: Tracked, + source: Spanned, + ) -> SourceResult> { + let data = source.load(world)?; + let theme = Self::decode(&data).at(source.span)?; + Ok(Derived::new(source.v, theme)) + } + + /// Decode a theme from bytes. + #[comemo::memoize] + fn decode(data: &Bytes) -> StrResult { + let mut cursor = std::io::Cursor::new(data.as_slice()); + let theme = synt::ThemeSet::load_from_reader(&mut cursor) + .map_err(|err| eco_format!("failed to parse theme ({err})"))?; + Ok(RawTheme(Arc::new(ManuallyHash::new(theme, typst_utils::hash128(data))))) + } + + /// Get the underlying syntect theme. + pub fn get(&self) -> &synt::Theme { + self.0.as_ref() + } +} + /// A highlighted line of raw text. /// /// This is a helper element that is synthesized by [`raw`] elements. @@ -561,13 +638,13 @@ pub struct RawLine { 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()) + Ok(self.body.clone()) } } impl PlainText for Packed { fn plain_text(&self, text: &mut EcoString) { - text.push_str(self.text()); + text.push_str(&self.text); } } @@ -593,6 +670,11 @@ struct ThemedHighlighter<'a> { line_fn: LineFn<'a>, } +// Shorthands for highlighter closures. +type StyleFn<'a> = + &'a mut dyn FnMut(usize, &LinkedNode, Range, synt::Style) -> Content; +type LineFn<'a> = &'a mut dyn FnMut(usize, Range, &mut Vec); + impl<'a> ThemedHighlighter<'a> { pub fn new( code: &'a str, @@ -738,108 +820,50 @@ fn to_syn(color: Color) -> synt::Color { synt::Color { r, g, b, a } } -/// A list of raw syntax file paths. -#[derive(Debug, Default, Clone, PartialEq, Hash)] -pub struct SyntaxPaths(Vec); - -cast! { - SyntaxPaths, - self => self.0.into_value(), - v: EcoString => Self(vec![v]), - v: Array => Self(v.into_iter().map(Value::cast).collect::>()?), -} - -impl Fold for SyntaxPaths { - fn fold(self, outer: Self) -> Self { - Self(self.0.fold(outer.0)) +/// Create a syntect theme item. +fn item( + scope: &str, + color: Option<&str>, + font_style: Option, +) -> synt::ThemeItem { + synt::ThemeItem { + scope: scope.parse().unwrap(), + style: synt::StyleModifier { + foreground: color.map(|s| to_syn(s.parse::().unwrap())), + background: None, + font_style, + }, } } -/// Load a syntax set from a list of syntax file paths. -#[comemo::memoize] -#[typst_macros::time(name = "load syntaxes")] -fn load_syntaxes(paths: &SyntaxPaths, bytes: &[Bytes]) -> StrResult> { - let mut out = SyntaxSetBuilder::new(); +/// Replace tabs with spaces to align with multiples of `tab_size`. +fn align_tabs(text: &str, tab_size: usize) -> EcoString { + let replacement = " ".repeat(tab_size); + let divisor = tab_size.max(1); + let amount = text.chars().filter(|&c| c == '\t').count(); - // We might have multiple sublime-syntax/yaml files - for (path, bytes) in paths.0.iter().zip(bytes.iter()) { - let src = std::str::from_utf8(bytes).map_err(FileError::from)?; - out.add(SyntaxDefinition::load_from_str(src, false, None).map_err(|err| { - eco_format!("failed to parse syntax file `{path}` ({err})") - })?); + let mut res = EcoString::with_capacity(text.len() - amount + amount * tab_size); + let mut column = 0; + + for grapheme in text.graphemes(true) { + match grapheme { + "\t" => { + let required = tab_size - column % divisor; + res.push_str(&replacement[..required]); + column += required; + } + "\n" => { + res.push_str(grapheme); + column = 0; + } + _ => { + res.push_str(grapheme); + column += 1; + } + } } - Ok(Arc::new(out.build())) -} - -/// Function to parse the syntaxes argument. -/// Much nicer than having it be part of the `element` macro. -fn parse_syntaxes( - engine: &mut Engine, - args: &mut Args, -) -> SourceResult<(Option, Option>)> { - let Some(Spanned { v: paths, span }) = - args.named::>("syntaxes")? - else { - return Ok((None, None)); - }; - - // Load syntax files. - let data = paths - .0 - .iter() - .map(|path| { - let id = span.resolve_path(path).at(span)?; - engine.world.file(id).at(span) - }) - .collect::>>()?; - - // Check that parsing works. - let _ = load_syntaxes(&paths, &data).at(span)?; - - Ok((Some(paths), Some(data))) -} - -#[comemo::memoize] -#[typst_macros::time(name = "load theme")] -fn load_theme(path: &str, bytes: &Bytes) -> StrResult> { - let mut cursor = std::io::Cursor::new(bytes.as_slice()); - - synt::ThemeSet::load_from_reader(&mut cursor) - .map(Arc::new) - .map_err(|err| eco_format!("failed to parse theme file `{path}` ({err})")) -} - -/// Function to parse the theme argument. -/// Much nicer than having it be part of the `element` macro. -fn parse_theme( - engine: &mut Engine, - args: &mut Args, -) -> SourceResult<(Option, Option)> { - let Some(Spanned { v: path, span }) = args.named::>("theme")? - else { - // Argument `theme` not found. - return Ok((None, None)); - }; - - let Smart::Custom(path) = path else { - // Argument `theme` is `auto`. - return Ok((Some(Smart::Auto), None)); - }; - - let Some(path) = path else { - // Argument `theme` is `none`. - return Ok((Some(Smart::Custom(None)), None)); - }; - - // Load theme file. - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - - // Check that parsing works. - let _ = load_theme(&path, &data).at(span)?; - - Ok((Some(Smart::Custom(Some(path))), Some(data))) + res } /// The syntect syntax definitions. @@ -886,49 +910,3 @@ pub static RAW_THEME: LazyLock = LazyLock::new(|| synt::Theme { item("markup.deleted, meta.diff.header.from-file", Some("#d73a49"), None), ], }); - -/// Create a syntect theme item. -fn item( - scope: &str, - color: Option<&str>, - font_style: Option, -) -> synt::ThemeItem { - synt::ThemeItem { - scope: scope.parse().unwrap(), - style: synt::StyleModifier { - foreground: color.map(|s| to_syn(s.parse::().unwrap())), - background: None, - font_style, - }, - } -} - -/// Replace tabs with spaces to align with multiples of `tab_size`. -fn align_tabs(text: &str, tab_size: usize) -> EcoString { - let replacement = " ".repeat(tab_size); - let divisor = tab_size.max(1); - let amount = text.chars().filter(|&c| c == '\t').count(); - - let mut res = EcoString::with_capacity(text.len() - amount + amount * tab_size); - let mut column = 0; - - for grapheme in text.graphemes(true) { - match grapheme { - "\t" => { - let required = tab_size - column % divisor; - res.push_str(&replacement[..required]); - column += required; - } - "\n" => { - res.push_str(grapheme); - column = 0; - } - _ => { - res.push_str(grapheme); - column += 1; - } - } - } - - res -} diff --git a/crates/typst-library/src/text/shift.rs b/crates/typst-library/src/text/shift.rs index 9723bbf0c..3eec0758b 100644 --- a/crates/typst-library/src/text/shift.rs +++ b/crates/typst-library/src/text/shift.rs @@ -50,7 +50,7 @@ pub struct SubElem { impl Show for Packed { #[typst_macros::time(name = "sub", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let body = self.body().clone(); + let body = self.body.clone(); if self.typographic(styles) { if let Some(text) = convert_script(&body, true) { @@ -109,7 +109,7 @@ pub struct SuperElem { impl Show for Packed { #[typst_macros::time(name = "super", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let body = self.body().clone(); + let body = self.body.clone(); if self.typographic(styles) { if let Some(text) = convert_script(&body, false) { @@ -132,9 +132,9 @@ fn convert_script(content: &Content, sub: bool) -> Option { Some(' '.into()) } else if let Some(elem) = content.to_packed::() { if sub { - elem.text().chars().map(to_subscript_codepoint).collect() + elem.text.chars().map(to_subscript_codepoint).collect() } else { - elem.text().chars().map(to_superscript_codepoint).collect() + elem.text.chars().map(to_superscript_codepoint).collect() } } else if let Some(sequence) = content.to_packed::() { sequence diff --git a/crates/typst-library/src/text/smallcaps.rs b/crates/typst-library/src/text/smallcaps.rs index bf003bd1c..924a45e8c 100644 --- a/crates/typst-library/src/text/smallcaps.rs +++ b/crates/typst-library/src/text/smallcaps.rs @@ -12,11 +12,11 @@ use crate::text::TextElem; /// ``` /// /// # Smallcaps fonts -/// By default, this enables the OpenType `smcp` feature for the font. Not all -/// fonts support this feature. Sometimes smallcaps are part of a dedicated -/// font. This is, for example, the case for the _Latin Modern_ family of fonts. -/// In those cases, you can use a show-set rule to customize the appearance of -/// the text in smallcaps: +/// By default, this uses the `smcp` and `c2sc` OpenType features on the font. +/// Not all fonts support these features. Sometimes, smallcaps are part of a +/// dedicated font. This is, for example, the case for the _Latin Modern_ family +/// of fonts. In those cases, you can use a show-set rule to customize the +/// appearance of the text in smallcaps: /// /// ```typ /// #show smallcaps: set text(font: "Latin Modern Roman Caps") @@ -45,6 +45,17 @@ use crate::text::TextElem; /// ``` #[elem(title = "Small Capitals", Show)] pub struct SmallcapsElem { + /// Whether to turn uppercase letters into small capitals as well. + /// + /// Unless overridden by a show rule, this enables the `c2sc` OpenType + /// feature. + /// + /// ```example + /// #smallcaps(all: true)[UNICEF] is an + /// agency of #smallcaps(all: true)[UN]. + /// ``` + #[default(false)] + pub all: bool, /// The content to display in small capitals. #[required] pub body: Content, @@ -52,7 +63,17 @@ pub struct SmallcapsElem { impl Show for Packed { #[typst_macros::time(name = "smallcaps", span = self.span())] - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(TextElem::set_smallcaps(true))) + fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { + let sc = if self.all(styles) { Smallcaps::All } else { Smallcaps::Minuscules }; + Ok(self.body.clone().styled(TextElem::set_smallcaps(Some(sc)))) } } + +/// What becomes small capitals. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum Smallcaps { + /// Minuscules become small capitals. + Minuscules, + /// All letters become small capitals. + All, +} diff --git a/crates/typst-library/src/visualize/color.rs b/crates/typst-library/src/visualize/color.rs index 8ff8dbdbc..b14312513 100644 --- a/crates/typst-library/src/visualize/color.rs +++ b/crates/typst-library/src/visualize/color.rs @@ -248,8 +248,6 @@ impl Color { /// ``` #[func] pub fn luma( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The lightness component. #[external] @@ -300,8 +298,6 @@ impl Color { /// ``` #[func] pub fn oklab( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The lightness component. #[external] @@ -358,8 +354,6 @@ impl Color { /// ``` #[func] pub fn oklch( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The lightness component. #[external] @@ -420,8 +414,6 @@ impl Color { /// ``` #[func(title = "Linear RGB")] pub fn linear_rgb( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The red component. #[external] @@ -477,8 +469,6 @@ impl Color { /// ``` #[func(title = "RGB")] pub fn rgb( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The red component. #[external] @@ -555,8 +545,6 @@ impl Color { /// ``` #[func(title = "CMYK")] pub fn cmyk( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The cyan component. #[external] @@ -614,8 +602,6 @@ impl Color { /// ``` #[func(title = "HSL")] pub fn hsl( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The hue angle. #[external] @@ -673,8 +659,6 @@ impl Color { /// ``` #[func(title = "HSV")] pub fn hsv( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The hue angle. #[external] @@ -898,7 +882,6 @@ impl Color { #[func] pub fn saturate( self, - /// The call span span: Span, /// The factor to saturate the color by. factor: Ratio, @@ -924,7 +907,6 @@ impl Color { #[func] pub fn desaturate( self, - /// The call span span: Span, /// The factor to desaturate the color by. factor: Ratio, @@ -1001,7 +983,6 @@ impl Color { #[func] pub fn rotate( self, - /// The call span span: Span, /// The angle to rotate the hue by. angle: Angle, diff --git a/crates/typst-library/src/visualize/gradient.rs b/crates/typst-library/src/visualize/gradient.rs index e16e5d88a..431f07dd4 100644 --- a/crates/typst-library/src/visualize/gradient.rs +++ b/crates/typst-library/src/visualize/gradient.rs @@ -200,9 +200,7 @@ impl Gradient { /// ``` #[func(title = "Linear Gradient")] pub fn linear( - /// The args of this function. args: &mut Args, - /// The call site of this function. span: Span, /// The color [stops](#stops) of the gradient. #[variadic] @@ -292,7 +290,6 @@ impl Gradient { /// ``` #[func] fn radial( - /// The call site of this function. span: Span, /// The color [stops](#stops) of the gradient. #[variadic] @@ -407,7 +404,6 @@ impl Gradient { /// ``` #[func] pub fn conic( - /// The call site of this function. span: Span, /// The color [stops](#stops) of the gradient. #[variadic] diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 37f42155f..d268d9e0c 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -20,11 +20,11 @@ use typst_utils::LazyHash; use crate::diag::{bail, At, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, func, scope, AutoValue, Bytes, Cast, Content, Dict, NativeElement, + cast, elem, func, scope, AutoValue, Bytes, Cast, Content, Derived, Dict, NativeElement, Packed, Show, Smart, StyleChain, Value, }; use crate::layout::{BlockElem, Length, Rel, Sizing}; -use crate::loading::Readable; +use crate::loading::{DataSource, Load, Readable}; use crate::model::Figurable; use crate::text::LocalName; use crate::World; @@ -49,25 +49,16 @@ use crate::World; /// ``` #[elem(scope, Show, LocalName, Figurable)] pub struct ImageElem { - /// Path to an image file. + /// A path to an image file or raw bytes making up an encoded image. /// - /// For more details, see the [Paths section]($syntax/#paths). + /// For more details about paths, see the [Paths section]($syntax/#paths). #[required] #[parse( - let Spanned { v: path, span } = - args.expect::>("path to image file")?; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - path + let source = args.expect::>("source")?; + let data = source.load(engine.world)?; + Derived::new(source.v, data) )] - #[borrowed] - pub path: EcoString, - - /// The data required to decode the image. - #[internal] - #[required] - #[parse(data.into())] - pub source: ImageSource, + pub source: Derived, /// The image's format. Detected automatically by default. /// @@ -116,6 +107,9 @@ pub struct ImageElem { impl ImageElem { /// Decode a raster or vector graphic from bytes or a string. /// + /// This function is deprecated. The [`image`] function now accepts bytes + /// directly. + /// /// ```example /// #let original = read("diagram.svg") /// #let changed = original.replace( @@ -128,7 +122,6 @@ impl ImageElem { /// ``` #[func(title = "Decode Image")] pub fn decode( - /// The call span of this function. span: Span, /// The data to decode as an image. Can be a string for SVGs. source: ImageSource, @@ -154,7 +147,9 @@ impl ImageElem { #[named] scaling: Option, ) -> StrResult { - let mut elem = ImageElem::new(EcoString::new(), source); + let bytes = data.into_bytes(); + let source = Derived::new(DataSource::Bytes(bytes.clone()), bytes); + let mut elem = ImageElem::new(source); if let Some(format) = format { elem.push_format(format); } @@ -380,6 +375,22 @@ pub enum ImageFormat { Pixmap(PixmapFormat), } +impl ImageFormat { + /// Try to detect the format of an image from data. + pub fn detect(data: &[u8]) -> Option { + if let Some(format) = RasterFormat::detect(data) { + return Some(Self::Raster(format)); + } + + // SVG or compressed SVG. + if data.starts_with(b" impl Iterator + Clone { + self.struct_fields().filter(|field| !field.required) + } + /// Fields that are relevant for equality. /// /// Synthesized fields are excluded to ensure equality before and after @@ -442,9 +447,9 @@ fn create_inherent_impl(element: &Elem) -> TokenStream { let Elem { ident, .. } = element; let new_func = create_new_func(element); - let with_field_methods = element.struct_fields().map(create_with_field_method); - let push_field_methods = element.struct_fields().map(create_push_field_method); - let field_methods = element.struct_fields().map(create_field_method); + let with_field_methods = element.accessor_fields().map(create_with_field_method); + let push_field_methods = element.accessor_fields().map(create_push_field_method); + let field_methods = element.accessor_fields().map(create_field_method); let field_in_methods = element.style_fields().map(create_field_in_method); let set_field_methods = element.style_fields().map(create_set_field_method); diff --git a/crates/typst-pdf/src/catalog.rs b/crates/typst-pdf/src/catalog.rs index c4b0e2e83..709b01553 100644 --- a/crates/typst-pdf/src/catalog.rs +++ b/crates/typst-pdf/src/catalog.rs @@ -12,7 +12,7 @@ use typst_syntax::Span; use xmp_writer::{DateTime, LangId, RenditionClass, XmpWriter}; use crate::page::PdfPageLabel; -use crate::{hash_base64, outline, TextStrExt, Timezone, WithEverything}; +use crate::{hash_base64, outline, TextStrExt, Timestamp, Timezone, WithEverything}; /// Write the document catalog. pub fn write_catalog( @@ -86,23 +86,10 @@ pub fn write_catalog( info.keywords(TextStr::trimmed(&joined)); xmp.pdf_keywords(&joined); } - - // (1) If the `document.date` is set to specific `datetime` or `none`, use it. - // (2) If the `document.date` is set to `auto` or not set, try to use the - // date from the options. - // (3) Otherwise, we don't write date metadata. - let (date, tz) = match (ctx.document.info.date, ctx.options.timestamp) { - (Smart::Custom(date), _) => (date, None), - (Smart::Auto, Some(timestamp)) => { - (Some(timestamp.datetime), Some(timestamp.timezone)) - } - _ => (None, None), - }; - if let Some(date) = date { - if let Some(pdf_date) = pdf_date(date, tz) { - info.creation_date(pdf_date); - info.modified_date(pdf_date); - } + let (date, tz) = document_date(ctx.document.info.date, ctx.options.timestamp); + if let Some(pdf_date) = date.and_then(|date| pdf_date(date, tz)) { + info.creation_date(pdf_date); + info.modified_date(pdf_date); } info.finish(); @@ -154,7 +141,7 @@ pub fn write_catalog( } // Assert dominance. - if ctx.options.standards.pdfa { + if let Some((part, conformance)) = ctx.options.standards.pdfa_part { let mut extension_schemas = xmp.extension_schemas(); extension_schemas .xmp_media_management() @@ -162,8 +149,8 @@ pub fn write_catalog( .describe_instance_id(); extension_schemas.pdf().properties().describe_all(); extension_schemas.finish(); - xmp.pdfa_part(2); - xmp.pdfa_conformance("B"); + xmp.pdfa_part(part); + xmp.pdfa_conformance(conformance); } let xmp_buf = xmp.finish(None); @@ -182,13 +169,35 @@ pub fn write_catalog( catalog.viewer_preferences().direction(dir); catalog.metadata(meta_ref); - // Write the named destination tree if there are any entries. - if !ctx.references.named_destinations.dests.is_empty() { + let has_dests = !ctx.references.named_destinations.dests.is_empty(); + let has_embeddings = !ctx.references.embedded_files.is_empty(); + + // Write the `/Names` dictionary. + if has_dests || has_embeddings { + // Write the named destination tree if there are any entries. let mut name_dict = catalog.names(); - let mut dests_name_tree = name_dict.destinations(); - let mut names = dests_name_tree.names(); - for &(name, dest_ref, ..) in &ctx.references.named_destinations.dests { - names.insert(Str(name.resolve().as_bytes()), dest_ref); + if has_dests { + let mut dests_name_tree = name_dict.destinations(); + let mut names = dests_name_tree.names(); + for &(name, dest_ref, ..) in &ctx.references.named_destinations.dests { + names.insert(Str(name.resolve().as_bytes()), dest_ref); + } + } + + if has_embeddings { + let mut embedded_files = name_dict.embedded_files(); + let mut names = embedded_files.names(); + for (name, file_ref) in &ctx.references.embedded_files { + names.insert(Str(name.as_bytes()), *file_ref); + } + } + } + + if has_embeddings && ctx.options.standards.pdfa { + // PDF 2.0, but ISO 19005-3 (PDF/A-3) Annex E allows it for PDF/A-3. + let mut associated_files = catalog.insert(Name(b"AF")).array().typed(); + for (_, file_ref) in ctx.references.embedded_files { + associated_files.item(file_ref).finish(); } } @@ -289,8 +298,27 @@ pub(crate) fn write_page_labels( result } +/// Resolve the document date. +/// +/// (1) If the `document.date` is set to specific `datetime` or `none`, use it. +/// (2) If the `document.date` is set to `auto` or not set, try to use the +/// date from the options. +/// (3) Otherwise, we don't write date metadata. +pub fn document_date( + document_date: Smart>, + timestamp: Option, +) -> (Option, Option) { + match (document_date, timestamp) { + (Smart::Custom(date), _) => (date, None), + (Smart::Auto, Some(timestamp)) => { + (Some(timestamp.datetime), Some(timestamp.timezone)) + } + _ => (None, None), + } +} + /// Converts a datetime to a pdf-writer date. -fn pdf_date(datetime: Datetime, tz: Option) -> Option { +pub fn pdf_date(datetime: Datetime, tz: Option) -> Option { let year = datetime.year().filter(|&y| y >= 0)? as u16; let mut pdf_date = pdf_writer::Date::new(year); diff --git a/crates/typst-pdf/src/embed.rs b/crates/typst-pdf/src/embed.rs new file mode 100644 index 000000000..597638f4b --- /dev/null +++ b/crates/typst-pdf/src/embed.rs @@ -0,0 +1,122 @@ +use std::collections::BTreeMap; + +use ecow::EcoString; +use pdf_writer::types::AssociationKind; +use pdf_writer::{Filter, Finish, Name, Ref, Str, TextStr}; +use typst_library::diag::{bail, SourceResult}; +use typst_library::foundations::{NativeElement, Packed, StyleChain}; +use typst_library::pdf::{EmbedElem, EmbeddedFileRelationship}; + +use crate::catalog::{document_date, pdf_date}; +use crate::{deflate, NameExt, PdfChunk, StrExt, WithGlobalRefs}; + +/// Query for all [`EmbedElem`] and write them and their file specifications. +/// +/// This returns a map of embedding names and references so that we can later +/// add them to the catalog's `/Names` dictionary. +pub fn write_embedded_files( + ctx: &WithGlobalRefs, +) -> SourceResult<(PdfChunk, BTreeMap)> { + let mut chunk = PdfChunk::new(); + let mut embedded_files = BTreeMap::default(); + + let elements = ctx.document.introspector.query(&EmbedElem::elem().select()); + for elem in &elements { + if !ctx.options.standards.embedded_files { + // PDF/A-2 requires embedded files to be PDF/A-1 or PDF/A-2, + // which we don't currently check. + bail!( + elem.span(), + "file embeddings are not currently supported for PDF/A-2"; + hint: "PDF/A-3 supports arbitrary embedded files" + ); + } + + let embed = elem.to_packed::().unwrap(); + if embed.path.derived.len() > Str::PDFA_LIMIT { + bail!(embed.span(), "embedded file path is too long"); + } + + let id = embed_file(ctx, &mut chunk, embed)?; + if embedded_files.insert(embed.path.derived.clone(), id).is_some() { + bail!( + elem.span(), + "duplicate embedded file for path `{}`", embed.path.derived; + hint: "embedded file paths must be unique", + ); + } + } + + Ok((chunk, embedded_files)) +} + +/// Write the embedded file stream and its file specification. +fn embed_file( + ctx: &WithGlobalRefs, + chunk: &mut PdfChunk, + embed: &Packed, +) -> SourceResult { + let embedded_file_stream_ref = chunk.alloc.bump(); + let file_spec_dict_ref = chunk.alloc.bump(); + + let data = embed.data.as_slice(); + let compressed = deflate(data); + + let mut embedded_file = chunk.embedded_file(embedded_file_stream_ref, &compressed); + embedded_file.filter(Filter::FlateDecode); + + if let Some(mime_type) = embed.mime_type(StyleChain::default()) { + if mime_type.len() > Name::PDFA_LIMIT { + bail!(embed.span(), "embedded file MIME type is too long"); + } + embedded_file.subtype(Name(mime_type.as_bytes())); + } else if ctx.options.standards.pdfa { + bail!(embed.span(), "embedded files must have a MIME type in PDF/A-3"); + } + + let mut params = embedded_file.params(); + params.size(data.len() as i32); + + let (date, tz) = document_date(ctx.document.info.date, ctx.options.timestamp); + if let Some(pdf_date) = date.and_then(|date| pdf_date(date, tz)) { + params.modification_date(pdf_date); + } else if ctx.options.standards.pdfa { + bail!( + embed.span(), + "the document must have a date when embedding files in PDF/A-3"; + hint: "`set document(date: none)` must not be used in this case" + ); + } + + params.finish(); + embedded_file.finish(); + + let mut file_spec = chunk.file_spec(file_spec_dict_ref); + file_spec.path(Str(embed.path.derived.as_bytes())); + file_spec.unic_file(TextStr(&embed.path.derived)); + file_spec + .insert(Name(b"EF")) + .dict() + .pair(Name(b"F"), embedded_file_stream_ref) + .pair(Name(b"UF"), embedded_file_stream_ref); + + if ctx.options.standards.pdfa { + // PDF 2.0, but ISO 19005-3 (PDF/A-3) Annex E allows it for PDF/A-3. + file_spec.association_kind(match embed.relationship(StyleChain::default()) { + Some(EmbeddedFileRelationship::Source) => AssociationKind::Source, + Some(EmbeddedFileRelationship::Data) => AssociationKind::Data, + Some(EmbeddedFileRelationship::Alternative) => AssociationKind::Alternative, + Some(EmbeddedFileRelationship::Supplement) => AssociationKind::Supplement, + None => AssociationKind::Unspecified, + }); + } + + if let Some(description) = embed.description(StyleChain::default()) { + if description.len() > Str::PDFA_LIMIT { + bail!(embed.span(), "embedded file description is too long"); + } + file_spec.description(TextStr(description)); + } + + Ok(file_spec_dict_ref) +} diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs index f45c62bb5..88e62389c 100644 --- a/crates/typst-pdf/src/lib.rs +++ b/crates/typst-pdf/src/lib.rs @@ -4,6 +4,7 @@ mod catalog; mod color; mod color_font; mod content; +mod embed; mod extg; mod font; mod gradient; @@ -14,12 +15,13 @@ mod page; mod resources; mod tiling; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::fmt::{self, Debug, Formatter}; use std::hash::Hash; use std::ops::{Deref, DerefMut}; use base64::Engine; +use ecow::EcoString; use pdf_writer::{Chunk, Name, Pdf, Ref, Str, TextStr}; use serde::{Deserialize, Serialize}; use typst_library::diag::{bail, SourceResult, StrResult}; @@ -33,6 +35,7 @@ use typst_utils::Deferred; use crate::catalog::write_catalog; use crate::color::{alloc_color_functions_refs, ColorFunctionRefs}; use crate::color_font::{write_color_fonts, ColorFontSlice}; +use crate::embed::write_embedded_files; use crate::extg::{write_graphic_states, ExtGState}; use crate::font::write_fonts; use crate::gradient::{write_gradients, PdfGradient}; @@ -67,6 +70,7 @@ pub fn pdf(document: &PagedDocument, options: &PdfOptions) -> SourceResult, } impl PdfStandards { /// Validates a list of PDF standards for compatibility and returns their /// encapsulated representation. pub fn new(list: &[PdfStandard]) -> StrResult { - Ok(Self { pdfa: list.contains(&PdfStandard::A_2b) }) + let a2b = list.contains(&PdfStandard::A_2b); + let a3b = list.contains(&PdfStandard::A_3b); + + if a2b && a3b { + bail!("PDF cannot conform to A-2B and A-3B at the same time") + } + + let pdfa = a2b || a3b; + Ok(Self { + pdfa, + embedded_files: !a2b, + pdfa_part: pdfa.then_some((if a2b { 2 } else { 3 }, "B")), + }) } } @@ -166,10 +188,9 @@ impl Debug for PdfStandards { } } -#[allow(clippy::derivable_impls)] impl Default for PdfStandards { fn default() -> Self { - Self { pdfa: false } + Self { pdfa: false, embedded_files: true, pdfa_part: None } } } @@ -186,6 +207,9 @@ pub enum PdfStandard { /// PDF/A-2b. #[serde(rename = "a-2b")] A_2b, + /// PDF/A-3b. + #[serde(rename = "a-3b")] + A_3b, } /// A struct to build a PDF following a fixed succession of phases. @@ -316,6 +340,8 @@ struct References { tilings: HashMap, /// The IDs of written external graphics states. ext_gs: HashMap, + /// The names and references for embedded files. + embedded_files: BTreeMap, } /// At this point, the references have been assigned to all resources. The page @@ -481,6 +507,14 @@ impl Renumber for HashMap { } } +impl Renumber for BTreeMap { + fn renumber(&mut self, offset: i32) { + for v in self.values_mut() { + v.renumber(offset); + } + } +} + impl Renumber for Option { fn renumber(&mut self, offset: i32) { if let Some(r) = self { diff --git a/crates/typst-pdf/src/outline.rs b/crates/typst-pdf/src/outline.rs index b9e71319f..ff72eb86a 100644 --- a/crates/typst-pdf/src/outline.rs +++ b/crates/typst-pdf/src/outline.rs @@ -184,8 +184,7 @@ fn write_outline_item( outline.count(-(node.children.len() as i32)); } - let body = node.element.body(); - outline.title(TextStr::trimmed(body.plain_text().trim())); + outline.title(TextStr::trimmed(node.element.body.plain_text().trim())); let loc = node.element.location().unwrap(); let pos = ctx.document.introspector.position(loc); diff --git a/crates/typst-realize/src/lib.rs b/crates/typst-realize/src/lib.rs index 6ab6d81c5..50685a962 100644 --- a/crates/typst-realize/src/lib.rs +++ b/crates/typst-realize/src/lib.rs @@ -15,7 +15,7 @@ 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, StyleVec, StyledElem, Styles, + SequenceElem, Show, ShowSet, Style, StyleChain, StyledElem, Styles, SymbolElem, Synthesize, Transformation, }; use typst_library::html::{tag, HtmlElem}; @@ -28,7 +28,7 @@ use typst_library::model::{ CiteElem, CiteGroup, DocumentElem, EnumElem, ListElem, ListItemLike, ListLike, ParElem, ParbreakElem, TermsElem, }; -use typst_library::routines::{Arenas, Pair, RealizationKind}; +use typst_library::routines::{Arenas, FragmentKind, Pair, RealizationKind}; use typst_library::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; use typst_syntax::Span; use typst_utils::{SliceExt, SmallBitSet}; @@ -48,17 +48,18 @@ pub fn realize<'a>( locator, arenas, rules: match kind { - RealizationKind::LayoutDocument(_) | 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::HtmlFragment(_) => HTML_FRAGMENT_RULES, RealizationKind::Math => MATH_RULES, }, sink: vec![], groupings: ArrayVec::new(), outside: matches!(kind, RealizationKind::LayoutDocument(_)), may_attach: false, + saw_parbreak: false, kind, }; @@ -98,6 +99,8 @@ struct State<'a, 'x, 'y, 'z> { outside: bool, /// Whether now following attach spacing can survive. may_attach: bool, + /// Whether we visited any paragraph breaks. + saw_parbreak: bool, } /// Defines a rule for how certain elements shall be grouped during realization. @@ -125,6 +128,10 @@ struct GroupingRule { struct Grouping<'a> { /// The position in `s.sink` where the group starts. start: usize, + /// Only applies to `PAR` grouping: Whether this paragraph group is + /// interrupted, but not yet finished because it may be ignored due to being + /// fully inline. + interrupted: bool, /// The rule used for this grouping. rule: &'a GroupingRule, } @@ -241,7 +248,7 @@ fn visit<'a>( return Ok(()); } - // Recurse into sequences. Styled elements and sequences can currently also + // Recurse into sequences. Styled elements and sequences can currently also // have labels, so this needs to happen before they are handled. if let Some(sequence) = content.to_packed::() { for elem in &sequence.children { @@ -295,7 +302,14 @@ fn visit_math_rules<'a>( // In normal realization, we apply regex show rules to consecutive // textual elements via `TEXTUAL` grouping. However, in math, this is // not desirable, so we just do it on a per-element basis. - if let Some(elem) = content.to_packed::() { + if let Some(elem) = content.to_packed::() { + if let Some(m) = + find_regex_match_in_str(elem.text.encode_utf8(&mut [0; 4]), styles) + { + visit_regex_match(s, &[(content, styles)], m)?; + return Ok(true); + } + } else if let Some(elem) = content.to_packed::() { if let Some(m) = find_regex_match_in_str(&elem.text, styles) { visit_regex_match(s, &[(content, styles)], m)?; return Ok(true); @@ -308,6 +322,14 @@ fn visit_math_rules<'a>( visit(s, s.store(eq), styles)?; return Ok(true); } + + // Symbols in non-math content transparently convert to `TextElem` so we + // don't have to handle them in non-math layout. + if let Some(elem) = content.to_packed::() { + let text = TextElem::packed(elem.text).spanned(elem.span()); + visit(s, s.store(text), styles)?; + return Ok(true); + } } Ok(false) @@ -560,19 +582,21 @@ fn visit_styled<'a>( for style in local.iter() { let Some(elem) = style.element() else { continue }; if elem == DocumentElem::elem() { - match &mut s.kind { - RealizationKind::LayoutDocument(info) - | RealizationKind::HtmlDocument(info) => info.populate(&local), - _ => bail!( + if let Some(info) = s.kind.as_document_mut() { + info.populate(&local) + } else { + bail!( style.span(), "document set rules are not allowed inside of containers" - ), + ); } } else if elem == PageElem::elem() { - let RealizationKind::LayoutDocument(_) = s.kind else { - let span = style.span(); - bail!(span, "page configuration is not allowed inside of containers"); - }; + if !matches!(s.kind, RealizationKind::LayoutDocument(_)) { + bail!( + style.span(), + "page configuration is not allowed inside of containers" + ); + } // When there are page styles, we "break free" from our show rule cage. pagebreak = true; @@ -635,7 +659,9 @@ fn visit_grouping_rules<'a>( } // If the element can be added to the active grouping, do it. - if (active.rule.trigger)(content, &s.kind) || (active.rule.inner)(content) { + if !active.interrupted + && ((active.rule.trigger)(content, &s.kind) || (active.rule.inner)(content)) + { s.sink.push((content, styles)); return Ok(true); } @@ -646,7 +672,7 @@ fn visit_grouping_rules<'a>( // Start a new grouping. if let Some(rule) = matching { let start = s.sink.len(); - s.groupings.push(Grouping { start, rule }); + s.groupings.push(Grouping { start, rule, interrupted: false }); s.sink.push((content, styles)); return Ok(true); } @@ -661,22 +687,24 @@ fn visit_filter_rules<'a>( content: &'a Content, styles: StyleChain<'a>, ) -> SourceResult { - if content.is::() - && !matches!(s.kind, RealizationKind::Math | RealizationKind::HtmlFragment) - { - // Outside of maths, spaces that were not collected by the paragraph - // grouper don't interest us. + if matches!(s.kind, RealizationKind::LayoutPar | RealizationKind::Math) { + return Ok(false); + } + + if content.is::() { + // Outside of maths and paragraph realization, spaces that were not + // collected by the paragraph grouper don't interest us. return Ok(true); } else if content.is::() { // Paragraph breaks are only a boundary for paragraph grouping, we don't // need to store them. s.may_attach = false; + s.saw_parbreak = true; return Ok(true); } else if !s.may_attach && content.to_packed::().is_some_and(|elem| elem.attach(styles)) { - // Delete attach spacing collapses if not immediately following a - // paragraph. + // Attach spacing collapses if not immediately following a paragraph. return Ok(true); } @@ -688,10 +716,21 @@ fn visit_filter_rules<'a>( /// Finishes all grouping. fn finish(s: &mut State) -> SourceResult<()> { - finish_grouping_while(s, |s| !s.groupings.is_empty())?; + finish_grouping_while(s, |s| { + // If this is a fragment realization and all we've got is inline + // content, don't turn it into a paragraph. + if is_fully_inline(s) { + *s.kind.as_fragment_mut().unwrap() = FragmentKind::Inline; + s.groupings.pop(); + collapse_spaces(&mut s.sink, 0); + false + } else { + !s.groupings.is_empty() + } + })?; - // In math, spaces are top-level. - if let RealizationKind::Math = s.kind { + // In paragraph and math realization, spaces are top-level. + if matches!(s.kind, RealizationKind::LayoutPar | RealizationKind::Math) { collapse_spaces(&mut s.sink, 0); } @@ -707,6 +746,12 @@ fn finish_interrupted(s: &mut State, local: &Styles) -> SourceResult<()> { } finish_grouping_while(s, |s| { s.groupings.iter().any(|grouping| (grouping.rule.interrupt)(elem)) + && if is_fully_inline(s) { + s.groupings[0].interrupted = true; + false + } else { + true + } })?; last = Some(elem); } @@ -714,9 +759,9 @@ fn finish_interrupted(s: &mut State, local: &Styles) -> SourceResult<()> { } /// Finishes groupings while `f` returns `true`. -fn finish_grouping_while(s: &mut State, f: F) -> SourceResult<()> +fn finish_grouping_while(s: &mut State, mut f: F) -> SourceResult<()> where - F: Fn(&State) -> bool, + F: FnMut(&mut State) -> bool, { // Finishing of a group may result in new content and new grouping. This // can, in theory, go on for a bit. To prevent it from becoming an infinite @@ -735,7 +780,7 @@ where /// Finishes the currently innermost grouping. fn finish_innermost_grouping(s: &mut State) -> SourceResult<()> { // The grouping we are interrupting. - let Grouping { start, rule } = s.groupings.pop().unwrap(); + 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)); @@ -779,14 +824,18 @@ const MAX_GROUP_NESTING: usize = 3; /// Grouping rules used in layout realization. static LAYOUT_RULES: &[&GroupingRule] = &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS]; +/// Grouping rules used in paragraph layout realization. +static LAYOUT_PAR_RULES: &[&GroupingRule] = &[&TEXTUAL, &CITES, &LIST, &ENUM, &TERMS]; + /// Grouping rules used in HTML root realization. static HTML_DOCUMENT_RULES: &[&GroupingRule] = &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS]; /// Grouping rules used in HTML fragment realization. -static HTML_FRAGMENT_RULES: &[&GroupingRule] = &[&TEXTUAL, &CITES, &LIST, &ENUM, &TERMS]; +static HTML_FRAGMENT_RULES: &[&GroupingRule] = + &[&TEXTUAL, &PAR, &CITES, &LIST, &ENUM, &TERMS]; -/// Grouping rules used in math realizatio. +/// Grouping rules used in math realization. static MATH_RULES: &[&GroupingRule] = &[&CITES, &LIST, &ENUM, &TERMS]; /// Groups adjacent textual elements for text show rule application. @@ -795,6 +844,9 @@ static TEXTUAL: GroupingRule = GroupingRule { tags: true, trigger: |content, _| { let elem = content.elem(); + // Note that `SymbolElem` converts into `TextElem` before textual show + // rules run, and we apply textual rules to elements manually during + // math realization, so we don't check for it here. elem == TextElem::elem() || elem == LinebreakElem::elem() || elem == SmartQuoteElem::elem() @@ -818,12 +870,10 @@ static PAR: GroupingRule = GroupingRule { || elem == SmartQuoteElem::elem() || elem == InlineElem::elem() || elem == BoxElem::elem() - || (matches!( - kind, - RealizationKind::HtmlDocument(_) | RealizationKind::HtmlFragment - ) && content - .to_packed::() - .is_some_and(|elem| tag::is_inline_by_default(elem.tag))) + || (kind.is_html() + && content + .to_packed::() + .is_some_and(|elem| tag::is_inline_by_default(elem.tag))) }, inner: |content| content.elem() == SpaceElem::elem(), interrupt: |elem| elem == ParElem::elem() || elem == AlignElem::elem(), @@ -896,17 +946,31 @@ fn finish_textual(Grouped { s, mut start }: Grouped) -> SourceResult<()> { // transparently become part of it. // 2. There is no group at all. In this case, we create one. if s.groupings.is_empty() && s.rules.iter().any(|&rule| std::ptr::eq(rule, &PAR)) { - s.groupings.push(Grouping { start, rule: &PAR }); + s.groupings.push(Grouping { start, rule: &PAR, interrupted: false }); } Ok(()) } /// Whether there is an active grouping, but it is not a `PAR` grouping. -fn in_non_par_grouping(s: &State) -> bool { - s.groupings - .last() - .is_some_and(|grouping| !std::ptr::eq(grouping.rule, &PAR)) +fn in_non_par_grouping(s: &mut State) -> bool { + s.groupings.last().is_some_and(|grouping| { + !std::ptr::eq(grouping.rule, &PAR) || grouping.interrupted + }) +} + +/// Whether there is exactly one active grouping, it is a `PAR` grouping, and it +/// spans the whole sink (with the exception of leading tags). +fn is_fully_inline(s: &State) -> bool { + s.kind.is_fragment() + && !s.saw_parbreak + && match s.groupings.as_slice() { + [grouping] => { + std::ptr::eq(grouping.rule, &PAR) + && s.sink[..grouping.start].iter().all(|(c, _)| c.is::()) + } + _ => false, + } } /// Builds the `ParElem` from inline-level elements. @@ -918,11 +982,11 @@ fn finish_par(mut grouped: Grouped) -> SourceResult<()> { // Collect the children. let elems = grouped.get(); let span = select_span(elems); - let (children, trunk) = StyleVec::create(elems); + let (body, trunk) = repack(elems); // Create and visit the paragraph. let s = grouped.end(); - let elem = ParElem::new(children).pack().spanned(span); + let elem = ParElem::new(body).pack().spanned(span); visit(s, s.store(elem), trunk) } @@ -1118,7 +1182,16 @@ fn visit_regex_match<'a>( m: RegexMatch<'a>, ) -> SourceResult<()> { let match_range = m.offset..m.offset + m.text.len(); - let piece = TextElem::packed(m.text); + + // Replace with the correct intuitive element kind: if matching against a + // lone symbol, return a `SymbolElem`, otherwise return a newly composed + // `TextElem`. We should only match against a `SymbolElem` during math + // realization (`RealizationKind::Math`). + let piece = match elems { + &[(lone, _)] if lone.is::() => lone.clone(), + _ => TextElem::packed(m.text), + }; + let context = Context::new(None, Some(m.styles)); let output = m.recipe.apply(s.engine, context.track(), piece)?; @@ -1141,10 +1214,16 @@ fn visit_regex_match<'a>( continue; } - // At this point, we can have a `TextElem`, `SpaceElem`, + // At this point, we can have a `TextElem`, `SymbolElem`, `SpaceElem`, // `LinebreakElem`, or `SmartQuoteElem`. We now determine the range of // the element. - let len = content.to_packed::().map_or(1, |elem| elem.text.len()); + let len = if let Some(elem) = content.to_packed::() { + elem.text.len() + } else if let Some(elem) = content.to_packed::() { + elem.text.len_utf8() + } else { + 1 // The rest are Ascii, so just one byte. + }; let elem_range = cursor..cursor + len; // If the element starts before the start of match, visit it fully or @@ -1244,3 +1323,26 @@ fn destruct_space(buf: &mut [Pair], end: &mut usize, state: &mut SpaceState) { fn select_span(children: &[Pair]) -> Span { Span::find(children.iter().map(|(c, _)| c.span())) } + +/// Turn realized content with styles back into owned content and a trunk style +/// chain. +fn repack<'a>(buf: &[Pair<'a>]) -> (Content, StyleChain<'a>) { + let trunk = StyleChain::trunk(buf.iter().map(|&(_, s)| s)).unwrap_or_default(); + let depth = trunk.links().count(); + + let mut seq = Vec::with_capacity(buf.len()); + + for (chain, group) in buf.group_by_key(|&(_, s)| s) { + let iter = group.iter().map(|&(c, _)| c.clone()); + let suffix = chain.suffix(depth); + if suffix.is_empty() { + seq.extend(iter); + } else if let &[(element, _)] = group { + seq.push(element.clone().styled_with_map(suffix)); + } else { + seq.push(Content::sequence(iter).styled_with_map(suffix)); + } + } + + (Content::sequence(seq), trunk) +} diff --git a/crates/typst-svg/src/text.rs b/crates/typst-svg/src/text.rs index a83d03ce4..36bd4c729 100644 --- a/crates/typst-svg/src/text.rs +++ b/crates/typst-svg/src/text.rs @@ -244,12 +244,9 @@ fn convert_bitmap_glyph_to_image(font: &Font, id: GlyphId) -> Option<(Image, f64 if raster.format != ttf_parser::RasterImageFormat::PNG { return None; } - let image = Image::new( - Bytes::from(raster.data).into(), - RasterFormat::Png.into(), - &Default::default(), - ) - .ok()?; + let image = + Image::new(Bytes::new(raster.data.to_vec()).into(), RasterFormat::Png.into(), &Default::default(),) + .ok()?; Some((image, raster.x as f64, raster.y as f64)) } diff --git a/crates/typst-syntax/src/ast.rs b/crates/typst-syntax/src/ast.rs index 19e123727..014e8392e 100644 --- a/crates/typst-syntax/src/ast.rs +++ b/crates/typst-syntax/src/ast.rs @@ -123,6 +123,8 @@ pub enum Expr<'a> { Equation(Equation<'a>), /// The contents of a mathematical equation: `x^2 + 1`. Math(Math<'a>), + /// A lone text fragment in math: `x`, `25`, `3.1415`, `=`, `[`. + MathText(MathText<'a>), /// An identifier in math: `pi`. MathIdent(MathIdent<'a>), /// A shorthand for a unicode codepoint in math: `a <= b`. @@ -233,6 +235,7 @@ impl<'a> AstNode<'a> for Expr<'a> { SyntaxKind::TermItem => node.cast().map(Self::Term), SyntaxKind::Equation => node.cast().map(Self::Equation), SyntaxKind::Math => node.cast().map(Self::Math), + SyntaxKind::MathText => node.cast().map(Self::MathText), SyntaxKind::MathIdent => node.cast().map(Self::MathIdent), SyntaxKind::MathShorthand => node.cast().map(Self::MathShorthand), SyntaxKind::MathAlignPoint => node.cast().map(Self::MathAlignPoint), @@ -297,6 +300,7 @@ impl<'a> AstNode<'a> for Expr<'a> { Self::Term(v) => v.to_untyped(), Self::Equation(v) => v.to_untyped(), Self::Math(v) => v.to_untyped(), + Self::MathText(v) => v.to_untyped(), Self::MathIdent(v) => v.to_untyped(), Self::MathShorthand(v) => v.to_untyped(), Self::MathAlignPoint(v) => v.to_untyped(), @@ -706,6 +710,34 @@ impl<'a> Math<'a> { } } +node! { + /// A lone text fragment in math: `x`, `25`, `3.1415`, `=`, `[`. + MathText +} + +/// The underlying text kind. +pub enum MathTextKind<'a> { + Character(char), + Number(&'a EcoString), +} + +impl<'a> MathText<'a> { + /// Return the underlying text. + pub fn get(self) -> MathTextKind<'a> { + let text = self.0.text(); + let mut chars = text.chars(); + let c = chars.next().unwrap(); + if c.is_numeric() { + // Numbers are potentially grouped as multiple characters. This is + // done in `Lexer::math_text()`. + MathTextKind::Number(text) + } else { + assert!(chars.next().is_none()); + MathTextKind::Character(c) + } + } +} + node! { /// An identifier in math: `pi`. MathIdent diff --git a/crates/typst-syntax/src/highlight.rs b/crates/typst-syntax/src/highlight.rs index de8ed65c9..cd815694d 100644 --- a/crates/typst-syntax/src/highlight.rs +++ b/crates/typst-syntax/src/highlight.rs @@ -171,6 +171,7 @@ pub fn highlight(node: &LinkedNode) -> Option { SyntaxKind::Equation => None, SyntaxKind::Math => None, + SyntaxKind::MathText => None, SyntaxKind::MathIdent => highlight_ident(node), SyntaxKind::MathShorthand => Some(Tag::Escape), SyntaxKind::MathAlignPoint => Some(Tag::MathOperator), @@ -287,6 +288,7 @@ pub fn highlight(node: &LinkedNode) -> Option { SyntaxKind::Destructuring => None, SyntaxKind::DestructAssignment => None, + SyntaxKind::Shebang => Some(Tag::Comment), SyntaxKind::LineComment => Some(Tag::Comment), SyntaxKind::BlockComment => Some(Tag::Comment), SyntaxKind::Error => Some(Tag::Error), diff --git a/crates/typst-syntax/src/kind.rs b/crates/typst-syntax/src/kind.rs index 0a7c160b4..c24b47fe7 100644 --- a/crates/typst-syntax/src/kind.rs +++ b/crates/typst-syntax/src/kind.rs @@ -9,6 +9,8 @@ pub enum SyntaxKind { /// An invalid sequence of characters. Error, + /// A shebang: `#! ...` + Shebang, /// A line comment: `// ...`. LineComment, /// A block comment: `/* ... */`. @@ -73,6 +75,8 @@ pub enum SyntaxKind { /// The contents of a mathematical equation: `x^2 + 1`. Math, + /// A lone text fragment in math: `x`, `25`, `3.1415`, `=`, `|`, `[`. + MathText, /// An identifier in math: `pi`. MathIdent, /// A shorthand for a unicode codepoint in math: `a <= b`. @@ -357,7 +361,11 @@ impl SyntaxKind { pub fn is_trivia(self) -> bool { matches!( self, - Self::LineComment | Self::BlockComment | Self::Space | Self::Parbreak + Self::Shebang + | Self::LineComment + | Self::BlockComment + | Self::Space + | Self::Parbreak ) } @@ -371,6 +379,7 @@ impl SyntaxKind { match self { Self::End => "end of tokens", Self::Error => "syntax error", + Self::Shebang => "shebang", Self::LineComment => "line comment", Self::BlockComment => "block comment", Self::Markup => "markup", @@ -401,6 +410,7 @@ impl SyntaxKind { Self::TermMarker => "term marker", Self::Equation => "equation", Self::Math => "math", + Self::MathText => "math text", Self::MathIdent => "math identifier", Self::MathShorthand => "math shorthand", Self::MathAlignPoint => "math alignment point", diff --git a/crates/typst-syntax/src/lexer.rs b/crates/typst-syntax/src/lexer.rs index b0cb5c464..b8f2bf25f 100644 --- a/crates/typst-syntax/src/lexer.rs +++ b/crates/typst-syntax/src/lexer.rs @@ -103,6 +103,7 @@ impl Lexer<'_> { self.newline = false; let kind = match self.s.eat() { Some(c) if is_space(c, self.mode) => self.whitespace(start, c), + Some('#') if start == 0 && self.s.eat_if('!') => self.shebang(), Some('/') if self.s.eat_if('/') => self.line_comment(), Some('/') if self.s.eat_if('*') => self.block_comment(), Some('*') if self.s.eat_if('/') => { @@ -151,6 +152,11 @@ impl Lexer<'_> { } } + fn shebang(&mut self) -> SyntaxKind { + self.s.eat_until(is_newline); + SyntaxKind::Shebang + } + fn line_comment(&mut self) -> SyntaxKind { self.s.eat_until(is_newline); SyntaxKind::LineComment @@ -616,6 +622,11 @@ impl Lexer<'_> { '~' if self.s.eat_if('>') => SyntaxKind::MathShorthand, '*' | '-' | '~' => SyntaxKind::MathShorthand, + '.' => SyntaxKind::Dot, + ',' => SyntaxKind::Comma, + ';' => SyntaxKind::Semicolon, + ')' => SyntaxKind::RightParen, + '#' => SyntaxKind::Hash, '_' => SyntaxKind::Underscore, '$' => SyntaxKind::Dollar, @@ -674,6 +685,7 @@ impl Lexer<'_> { if s.eat_if('.') && !s.eat_while(char::is_numeric).is_empty() { self.s = s; } + SyntaxKind::MathText } else { let len = self .s @@ -682,8 +694,53 @@ impl Lexer<'_> { .next() .map_or(0, str::len); self.s.jump(start + len); + if len > c.len_utf8() { + // Grapheme clusters are treated as normal text and stay grouped + // This may need to change in the future. + SyntaxKind::Text + } else { + SyntaxKind::MathText + } } - SyntaxKind::Text + } + + /// Handle named arguments in math function call. + pub fn maybe_math_named_arg(&mut self, start: usize) -> Option { + let cursor = self.s.cursor(); + self.s.jump(start); + if self.s.eat_if(is_id_start) { + self.s.eat_while(is_id_continue); + // Check that a colon directly follows the identifier, and not the + // `:=` or `::=` math shorthands. + if self.s.at(':') && !self.s.at(":=") && !self.s.at("::=") { + // Check that the identifier is not just `_`. + let node = if self.s.from(start) != "_" { + SyntaxNode::leaf(SyntaxKind::Ident, self.s.from(start)) + } else { + let msg = SyntaxError::new("expected identifier, found underscore"); + SyntaxNode::error(msg, self.s.from(start)) + }; + return Some(node); + } + } + self.s.jump(cursor); + None + } + + /// Handle spread arguments in math function call. + pub fn maybe_math_spread_arg(&mut self, start: usize) -> Option { + let cursor = self.s.cursor(); + self.s.jump(start); + if self.s.eat_if("..") { + // Check that neither a space nor a dot follows the spread syntax. + // A dot would clash with the `...` math shorthand. + if !self.space_or_end() && !self.s.at('.') { + let node = SyntaxNode::leaf(SyntaxKind::Dots, self.s.from(start)); + return Some(node); + } + } + self.s.jump(cursor); + None } } diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index 6c1778c4a..55d5550b6 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -93,6 +93,8 @@ fn markup_expr(p: &mut Parser, at_start: bool, nesting: &mut usize) { p.hint("try using a backslash escape: \\]"); } + SyntaxKind::Shebang => p.eat(), + SyntaxKind::Text | SyntaxKind::Linebreak | SyntaxKind::Escape @@ -160,7 +162,7 @@ fn list_item(p: &mut Parser) { p.with_nl_mode(AtNewline::RequireColumn(p.current_column()), |p| { let m = p.marker(); p.assert(SyntaxKind::ListMarker); - markup(p, false, false, syntax_set!(RightBracket, End)); + markup(p, true, false, syntax_set!(RightBracket, End)); p.wrap(m, SyntaxKind::ListItem); }); } @@ -170,7 +172,7 @@ fn enum_item(p: &mut Parser) { p.with_nl_mode(AtNewline::RequireColumn(p.current_column()), |p| { let m = p.marker(); p.assert(SyntaxKind::EnumMarker); - markup(p, false, false, syntax_set!(RightBracket, End)); + markup(p, true, false, syntax_set!(RightBracket, End)); p.wrap(m, SyntaxKind::EnumItem); }); } @@ -184,7 +186,7 @@ fn term_item(p: &mut Parser) { markup(p, false, false, syntax_set!(Colon, RightBracket, End)); }); p.expect(SyntaxKind::Colon); - markup(p, false, false, syntax_set!(RightBracket, End)); + markup(p, true, false, syntax_set!(RightBracket, End)); p.wrap(m, SyntaxKind::TermItem); }); } @@ -217,16 +219,20 @@ fn math(p: &mut Parser, stop_set: SyntaxSet) { p.wrap(m, SyntaxKind::Math); } -/// Parses a sequence of math expressions. -fn math_exprs(p: &mut Parser, stop_set: SyntaxSet) { +/// Parses a sequence of math expressions. Returns the number of expressions +/// parsed. +fn math_exprs(p: &mut Parser, stop_set: SyntaxSet) -> usize { debug_assert!(stop_set.contains(SyntaxKind::End)); + let mut count = 0; while !p.at_set(stop_set) { if p.at_set(set::MATH_EXPR) { math_expr(p); + count += 1; } else { p.unexpected(); } } + count } /// Parses a single math expression: This includes math elements like @@ -246,7 +252,9 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) { continuable = true; p.eat(); // Parse a function call for an identifier or field access. - if min_prec < 3 && p.directly_at(SyntaxKind::Text) && p.current_text() == "(" + if min_prec < 3 + && p.directly_at(SyntaxKind::MathText) + && p.current_text() == "(" { math_args(p); p.wrap(m, SyntaxKind::FuncCall); @@ -254,7 +262,14 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) { } } - SyntaxKind::Text | SyntaxKind::MathShorthand => { + SyntaxKind::Dot + | SyntaxKind::Comma + | SyntaxKind::Semicolon + | SyntaxKind::RightParen => { + p.convert_and_eat(SyntaxKind::MathText); + } + + SyntaxKind::Text | SyntaxKind::MathText | SyntaxKind::MathShorthand => { continuable = matches!( math_class(p.current_text()), None | Some(MathClass::Alphabetic) @@ -303,7 +318,7 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) { let mut primed = false; while !p.end() && !p.at(stop) { - if p.directly_at(SyntaxKind::Text) && p.current_text() == "!" { + if p.directly_at(SyntaxKind::MathText) && p.current_text() == "!" { p.eat(); p.wrap(m, SyntaxKind::Math); continue; @@ -398,7 +413,13 @@ fn math_delimited(p: &mut Parser) { while !p.at_set(syntax_set!(Dollar, End)) { if math_class(p.current_text()) == Some(MathClass::Closing) { p.wrap(m2, SyntaxKind::Math); - p.eat(); + // We could be at the shorthand `|]`, which shouldn't be converted + // to a `Text` kind. + if p.at(SyntaxKind::RightParen) { + p.convert_and_eat(SyntaxKind::MathText); + } else { + p.eat(); + } p.wrap(m, SyntaxKind::MathDelimited); return; } @@ -425,10 +446,10 @@ fn math_unparen(p: &mut Parser, m: Marker) { if first.text() == "(" && last.text() == ")" { first.convert_to_kind(SyntaxKind::LeftParen); last.convert_to_kind(SyntaxKind::RightParen); + // Only convert if we did have regular parens. + node.convert_to_kind(SyntaxKind::Math); } } - - node.convert_to_kind(SyntaxKind::Math); } /// The unicode math class of a string. Only returns `Some` if `text` has @@ -455,94 +476,90 @@ fn math_args(p: &mut Parser) { let m = p.marker(); p.convert_and_eat(SyntaxKind::LeftParen); - let mut namable = true; - let mut named = None; + let mut positional = true; let mut has_arrays = false; - let mut array = p.marker(); - let mut arg = p.marker(); - // The number of math expressions per argument. - let mut count = 0; - while !p.at_set(syntax_set!(Dollar, End)) { - if namable - && (p.at(SyntaxKind::MathIdent) || p.at(SyntaxKind::Text)) - && p.text[p.current_end()..].starts_with(':') - { - p.convert_and_eat(SyntaxKind::Ident); - p.convert_and_eat(SyntaxKind::Colon); - named = Some(arg); - arg = p.marker(); - array = p.marker(); - } + let mut maybe_array_start = p.marker(); + let mut seen = HashSet::new(); + while !p.at_set(syntax_set!(End, Dollar, RightParen)) { + positional = math_arg(p, &mut seen); - match p.current_text() { - ")" => break, - ";" => { - maybe_wrap_in_math(p, arg, count, named); - p.wrap(array, SyntaxKind::Array); - p.convert_and_eat(SyntaxKind::Semicolon); - array = p.marker(); - arg = p.marker(); - count = 0; - namable = true; - named = None; - has_arrays = true; - continue; - } - "," => { - maybe_wrap_in_math(p, arg, count, named); - p.convert_and_eat(SyntaxKind::Comma); - arg = p.marker(); - count = 0; - namable = true; - if named.is_some() { - array = p.marker(); - named = None; + match p.current() { + SyntaxKind::Comma => { + p.eat(); + if !positional { + maybe_array_start = p.marker(); } - continue; } - _ => {} - } + SyntaxKind::Semicolon => { + if !positional { + maybe_array_start = p.marker(); + } - if p.at_set(set::MATH_EXPR) { - math_expr(p); - count += 1; - } else { - p.unexpected(); - } - - namable = false; - } - - if arg != p.marker() { - maybe_wrap_in_math(p, arg, count, named); - if named.is_some() { - array = p.marker(); + // Parses an array: `a, b, c;`. + // The semicolon merges preceding arguments separated by commas + // into an array argument. + p.wrap(maybe_array_start, SyntaxKind::Array); + p.eat(); + maybe_array_start = p.marker(); + has_arrays = true; + } + SyntaxKind::End | SyntaxKind::Dollar | SyntaxKind::RightParen => {} + _ => p.expected("comma or semicolon"), } } - if has_arrays && array != p.marker() { - p.wrap(array, SyntaxKind::Array); - } - - if p.at(SyntaxKind::Text) && p.current_text() == ")" { - p.convert_and_eat(SyntaxKind::RightParen); - } else { - p.expected("closing paren"); - p.balanced = false; + // Check if we need to wrap the preceding arguments in an array. + if maybe_array_start != p.marker() && has_arrays && positional { + p.wrap(maybe_array_start, SyntaxKind::Array); } + p.expect_closing_delimiter(m, SyntaxKind::RightParen); p.wrap(m, SyntaxKind::Args); } -/// Wrap math function arguments to join adjacent math content or create an -/// empty 'Math' node for when we have 0 args. +/// Parses a single argument in a math argument list. /// -/// We don't wrap when `count == 1`, since wrapping would change the type of the -/// expression from potentially non-content to content. Ex: `$ func(#12pt) $` -/// would change the type from size to content if wrapped. -fn maybe_wrap_in_math(p: &mut Parser, arg: Marker, count: usize, named: Option) { +/// Returns whether the parsed argument was positional or not. +fn math_arg<'s>(p: &mut Parser<'s>, seen: &mut HashSet<&'s str>) -> bool { + let m = p.marker(); + let start = p.current_start(); + + if p.at(SyntaxKind::Dot) { + // Parses a spread argument: `..args`. + if let Some(spread) = p.lexer.maybe_math_spread_arg(start) { + p.token.node = spread; + p.eat(); + math_expr(p); + p.wrap(m, SyntaxKind::Spread); + return true; + } + } + + let mut positional = true; + if p.at_set(syntax_set!(MathText, MathIdent, Underscore)) { + // Parses a named argument: `thickness: #12pt`. + if let Some(named) = p.lexer.maybe_math_named_arg(start) { + p.token.node = named; + let text = p.current_text(); + p.eat(); + p.convert_and_eat(SyntaxKind::Colon); + if !seen.insert(text) { + p[m].convert_to_error(eco_format!("duplicate argument: {text}")); + } + positional = false; + } + } + + // Parses a normal positional argument. + let arg = p.marker(); + let count = math_exprs(p, syntax_set!(End, Dollar, Comma, Semicolon, RightParen)); if count == 0 { + // Named argument requires a value. + if !positional { + p.expected("expression"); + } + // Flush trivia so that the new empty Math node will be wrapped _inside_ // any `SyntaxKind::Array` elements created in `math_args`. // (And if we don't follow by wrapping in an array, it has no effect.) @@ -553,13 +570,19 @@ fn maybe_wrap_in_math(p: &mut Parser, arg: Marker, count: usize, named: Option true, }, AtNewline::StopParBreak => parbreak, - AtNewline::RequireColumn(min_col) => match column { - Some(column) => column <= min_col, - None => false, // Don't stop if we had no column. - }, + AtNewline::RequireColumn(min_col) => { + // Don't stop if this newline doesn't start a column (this may + // be checked on the boundary of lexer modes, since we only + // report a column in Markup). + column.is_some_and(|column| column <= min_col) + } } } } @@ -1684,10 +1709,13 @@ impl<'s> Parser<'s> { self.token.newline.is_some() } - /// The number of characters until the most recent newline from the current - /// token, or 0 if it did not follow a newline. + /// The number of characters until the most recent newline from the start of + /// the current token. Uses a cached value from the newline mode if present. fn current_column(&self) -> usize { - self.token.newline.and_then(|newline| newline.column).unwrap_or(0) + self.token + .newline + .and_then(|newline| newline.column) + .unwrap_or_else(|| self.lexer.column(self.token.start)) } /// The current token's text. diff --git a/crates/typst-syntax/src/set.rs b/crates/typst-syntax/src/set.rs index 014aaf2f7..a7b9a594a 100644 --- a/crates/typst-syntax/src/set.rs +++ b/crates/typst-syntax/src/set.rs @@ -59,7 +59,12 @@ pub const MATH_EXPR: SyntaxSet = syntax_set!( Hash, MathIdent, FieldAccess, + Dot, + Comma, + Semicolon, + RightParen, Text, + MathText, MathShorthand, Linebreak, MathAlignPoint, diff --git a/crates/typst-timing/Cargo.toml b/crates/typst-timing/Cargo.toml index 2d42269fc..dbc2813c7 100644 --- a/crates/typst-timing/Cargo.toml +++ b/crates/typst-timing/Cargo.toml @@ -17,5 +17,11 @@ parking_lot = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +[target.'cfg(target_arch = "wasm32")'.dependencies] +web-sys = { workspace = true, features = ["Window", "WorkerGlobalScope", "Performance"], optional = true } + +[features] +wasm = ["dep:web-sys"] + [lints] workspace = true diff --git a/crates/typst-timing/src/lib.rs b/crates/typst-timing/src/lib.rs index b4653170b..6da2cdf02 100644 --- a/crates/typst-timing/src/lib.rs +++ b/crates/typst-timing/src/lib.rs @@ -1,149 +1,13 @@ //! Performance timing for Typst. -#![cfg_attr(target_arch = "wasm32", allow(dead_code, unused_variables))] - -use std::hash::Hash; use std::io::Write; use std::num::NonZeroU64; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::Ordering::Relaxed; -use std::thread::ThreadId; -use std::time::{Duration, SystemTime}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use parking_lot::Mutex; use serde::ser::SerializeSeq; use serde::{Serialize, Serializer}; -/// Whether the timer is enabled. Defaults to `false`. -static ENABLED: AtomicBool = AtomicBool::new(false); - -/// The global event recorder. -static RECORDER: Mutex = Mutex::new(Recorder::new()); - -/// The recorder of events. -struct Recorder { - /// The events that have been recorded. - events: Vec, - /// The discriminator of the next event. - discriminator: u64, -} - -impl Recorder { - /// Create a new recorder. - const fn new() -> Self { - Self { events: Vec::new(), discriminator: 0 } - } -} - -/// An event that has been recorded. -#[derive(Clone, Copy, Eq, PartialEq, Hash)] -struct Event { - /// Whether this is a start or end event. - kind: EventKind, - /// The start time of this event. - timestamp: SystemTime, - /// The discriminator of this event. - id: u64, - /// The name of this event. - name: &'static str, - /// The raw value of the span of code that this event was recorded in. - span: Option, - /// The thread ID of this event. - thread_id: ThreadId, -} - -/// Whether an event marks the start or end of a scope. -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] -enum EventKind { - Start, - End, -} - -/// Enable the timer. -#[inline] -pub fn enable() { - // We only need atomicity and no synchronization of other - // operations, so `Relaxed` is fine. - ENABLED.store(true, Relaxed); -} - -/// Whether the timer is enabled. -#[inline] -pub fn is_enabled() -> bool { - ENABLED.load(Relaxed) -} - -/// Clears the recorded events. -#[inline] -pub fn clear() { - RECORDER.lock().events.clear(); -} - -/// A scope that records an event when it is dropped. -pub struct TimingScope { - name: &'static str, - span: Option, - id: u64, - thread_id: ThreadId, -} - -impl TimingScope { - /// Create a new scope if timing is enabled. - #[inline] - pub fn new(name: &'static str) -> Option { - Self::with_span(name, None) - } - - /// Create a new scope with a span if timing is enabled. - /// - /// The span is a raw number because `typst-timing` can't depend on - /// `typst-syntax` (or else `typst-syntax` couldn't depend on - /// `typst-timing`). - #[inline] - pub fn with_span(name: &'static str, span: Option) -> Option { - #[cfg(not(target_arch = "wasm32"))] - if is_enabled() { - return Some(Self::new_impl(name, span)); - } - None - } - - /// Create a new scope without checking if timing is enabled. - fn new_impl(name: &'static str, span: Option) -> Self { - let timestamp = SystemTime::now(); - let thread_id = std::thread::current().id(); - - let mut recorder = RECORDER.lock(); - let id = recorder.discriminator; - recorder.discriminator += 1; - recorder.events.push(Event { - kind: EventKind::Start, - timestamp, - id, - name, - span, - thread_id, - }); - - Self { name, span, id, thread_id } - } -} - -impl Drop for TimingScope { - fn drop(&mut self) { - let event = Event { - kind: EventKind::End, - timestamp: SystemTime::now(), - id: self.id, - name: self.name, - span: self.span, - thread_id: self.thread_id, - }; - - RECORDER.lock().events.push(event); - } -} - /// Creates a timing scope around an expression. /// /// The output of the expression is returned. @@ -179,6 +43,46 @@ macro_rules! timed { }}; } +thread_local! { + /// Data that is initialized once per thread. + static THREAD_DATA: ThreadData = ThreadData { + id: { + // We only need atomicity and no synchronization of other + // operations, so `Relaxed` is fine. + static COUNTER: AtomicU64 = AtomicU64::new(1); + COUNTER.fetch_add(1, Ordering::Relaxed) + }, + #[cfg(all(target_arch = "wasm32", feature = "wasm"))] + timer: WasmTimer::new(), + }; +} + +/// Whether the timer is enabled. Defaults to `false`. +static ENABLED: AtomicBool = AtomicBool::new(false); + +/// The list of collected events. +static EVENTS: Mutex> = Mutex::new(Vec::new()); + +/// Enable the timer. +#[inline] +pub fn enable() { + // We only need atomicity and no synchronization of other + // operations, so `Relaxed` is fine. + ENABLED.store(true, Ordering::Relaxed); +} + +/// Whether the timer is enabled. +#[inline] +pub fn is_enabled() -> bool { + ENABLED.load(Ordering::Relaxed) +} + +/// Clears the recorded events. +#[inline] +pub fn clear() { + EVENTS.lock().clear(); +} + /// Export data as JSON for Chrome's tracing tool. /// /// The `source` function is called for each span to get the source code @@ -205,19 +109,15 @@ pub fn export_json( line: u32, } - let recorder = RECORDER.lock(); - let run_start = recorder - .events - .first() - .map(|event| event.timestamp) - .unwrap_or_else(SystemTime::now); + let lock = EVENTS.lock(); + let events = lock.as_slice(); let mut serializer = serde_json::Serializer::new(writer); let mut seq = serializer - .serialize_seq(Some(recorder.events.len())) + .serialize_seq(Some(events.len())) .map_err(|e| format!("failed to serialize events: {e}"))?; - for event in recorder.events.iter() { + for event in events.iter() { seq.serialize_element(&Entry { name: event.name, cat: "typst", @@ -225,17 +125,9 @@ pub fn export_json( EventKind::Start => "B", EventKind::End => "E", }, - ts: event - .timestamp - .duration_since(run_start) - .unwrap_or(Duration::ZERO) - .as_nanos() as f64 - / 1_000.0, + ts: event.timestamp.micros_since(events[0].timestamp), pid: 1, - tid: unsafe { - // Safety: `thread_id` is a `ThreadId` which is a `u64`. - std::mem::transmute_copy(&event.thread_id) - }, + tid: event.thread_id, args: event.span.map(&mut source).map(|(file, line)| Args { file, line }), }) .map_err(|e| format!("failed to serialize event: {e}"))?; @@ -245,3 +137,173 @@ pub fn export_json( Ok(()) } + +/// A scope that records an event when it is dropped. +pub struct TimingScope { + name: &'static str, + span: Option, + thread_id: u64, +} + +impl TimingScope { + /// Create a new scope if timing is enabled. + #[inline] + pub fn new(name: &'static str) -> Option { + Self::with_span(name, None) + } + + /// Create a new scope with a span if timing is enabled. + /// + /// The span is a raw number because `typst-timing` can't depend on + /// `typst-syntax` (or else `typst-syntax` couldn't depend on + /// `typst-timing`). + #[inline] + pub fn with_span(name: &'static str, span: Option) -> Option { + if is_enabled() { + return Some(Self::new_impl(name, span)); + } + None + } + + /// Create a new scope without checking if timing is enabled. + fn new_impl(name: &'static str, span: Option) -> Self { + let (thread_id, timestamp) = + THREAD_DATA.with(|data| (data.id, Timestamp::now_with(data))); + EVENTS.lock().push(Event { + kind: EventKind::Start, + timestamp, + name, + span, + thread_id, + }); + Self { name, span, thread_id } + } +} + +impl Drop for TimingScope { + fn drop(&mut self) { + let timestamp = Timestamp::now(); + EVENTS.lock().push(Event { + kind: EventKind::End, + timestamp, + name: self.name, + span: self.span, + thread_id: self.thread_id, + }); + } +} + +/// An event that has been recorded. +struct Event { + /// Whether this is a start or end event. + kind: EventKind, + /// The time at which this event occurred. + timestamp: Timestamp, + /// The name of this event. + name: &'static str, + /// The raw value of the span of code that this event was recorded in. + span: Option, + /// The thread ID of this event. + thread_id: u64, +} + +/// Whether an event marks the start or end of a scope. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +enum EventKind { + Start, + End, +} + +/// A cross-platform way to get the current time. +#[derive(Copy, Clone)] +struct Timestamp { + #[cfg(not(target_arch = "wasm32"))] + inner: std::time::SystemTime, + #[cfg(target_arch = "wasm32")] + inner: f64, +} + +impl Timestamp { + fn now() -> Self { + #[cfg(target_arch = "wasm32")] + return THREAD_DATA.with(Self::now_with); + + #[cfg(not(target_arch = "wasm32"))] + Self { inner: std::time::SystemTime::now() } + } + + #[allow(unused_variables)] + fn now_with(data: &ThreadData) -> Self { + #[cfg(all(target_arch = "wasm32", feature = "wasm"))] + return Self { inner: data.timer.now() }; + + #[cfg(all(target_arch = "wasm32", not(feature = "wasm")))] + return Self { inner: 0.0 }; + + #[cfg(not(target_arch = "wasm32"))] + Self::now() + } + + fn micros_since(self, start: Self) -> f64 { + #[cfg(target_arch = "wasm32")] + return (self.inner - start.inner) * 1000.0; + + #[cfg(not(target_arch = "wasm32"))] + (self + .inner + .duration_since(start.inner) + .unwrap_or(std::time::Duration::ZERO) + .as_nanos() as f64 + / 1_000.0) + } +} + +/// Per-thread data. +struct ThreadData { + /// The thread's ID. + /// + /// In contrast to `std::thread::current().id()`, this is wasm-compatible + /// and also a bit cheaper to access because the std version does a bit more + /// stuff (including cloning an `Arc`). + id: u64, + /// A way to get the time from WebAssembly. + #[cfg(all(target_arch = "wasm32", feature = "wasm"))] + timer: WasmTimer, +} + +/// A way to get the time from WebAssembly. +#[cfg(all(target_arch = "wasm32", feature = "wasm"))] +struct WasmTimer { + /// The cached JS performance handle for the thread. + perf: web_sys::Performance, + /// The cached JS time origin. + time_origin: f64, +} + +#[cfg(all(target_arch = "wasm32", feature = "wasm"))] +impl WasmTimer { + fn new() -> Self { + // Retrieve `performance` from global object, either the window or + // globalThis. + let perf = web_sys::window() + .and_then(|window| window.performance()) + .or_else(|| { + use web_sys::wasm_bindgen::JsCast; + web_sys::js_sys::global() + .dyn_into::() + .ok() + .and_then(|scope| scope.performance()) + }) + .expect("failed to get JS performance handle"); + + // Every thread gets its own time origin. To make the results consistent + // across threads, we need to add this to each `now()` call. + let time_origin = perf.time_origin(); + + Self { perf, time_origin } + } + + fn now(&self) -> f64 { + self.time_origin + self.perf.now() + } +} diff --git a/crates/typst-utils/src/hash.rs b/crates/typst-utils/src/hash.rs index 3dbadbe20..9687da20b 100644 --- a/crates/typst-utils/src/hash.rs +++ b/crates/typst-utils/src/hash.rs @@ -162,3 +162,74 @@ impl Debug for LazyHash { self.value.fmt(f) } } + +/// A wrapper type with a manually computed hash. +/// +/// This can be used to turn an unhashable type into a hashable one where the +/// hash is provided manually. Typically, the hash is derived from the data +/// which was used to construct to the unhashable type. +/// +/// For instance, you could hash the bytes that were parsed into an unhashable +/// data structure. +/// +/// # Equality +/// Because Typst uses high-quality 128 bit hashes in all places, the risk of a +/// hash collision is reduced to an absolute minimum. Therefore, this type +/// additionally provides `PartialEq` and `Eq` implementations that compare by +/// hash instead of by value. For this to be correct, your hash implementation +/// **must feed all information relevant to the `PartialEq` impl to the +/// hasher.** +#[derive(Clone)] +pub struct ManuallyHash { + /// A manually computed hash. + hash: u128, + /// The underlying value. + value: T, +} + +impl ManuallyHash { + /// Wraps an item with a pre-computed hash. + /// + /// The hash should be computed with `typst_utils::hash128`. + #[inline] + pub fn new(value: T, hash: u128) -> Self { + Self { hash, value } + } + + /// Returns the wrapped value. + #[inline] + pub fn into_inner(self) -> T { + self.value + } +} + +impl Hash for ManuallyHash { + #[inline] + fn hash(&self, state: &mut H) { + state.write_u128(self.hash); + } +} + +impl Eq for ManuallyHash {} + +impl PartialEq for ManuallyHash { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.hash == other.hash + } +} + +impl Deref for ManuallyHash { + type Target = T; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl Debug for ManuallyHash { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.value.fmt(f) + } +} diff --git a/crates/typst-utils/src/lib.rs b/crates/typst-utils/src/lib.rs index 61703250a..b59fe2f73 100644 --- a/crates/typst-utils/src/lib.rs +++ b/crates/typst-utils/src/lib.rs @@ -15,7 +15,7 @@ mod scalar; pub use self::bitset::{BitSet, SmallBitSet}; pub use self::deferred::Deferred; pub use self::duration::format_duration; -pub use self::hash::LazyHash; +pub use self::hash::{LazyHash, ManuallyHash}; pub use self::pico::{PicoStr, ResolvedPicoStr}; pub use self::round::{round_int_with_precision, round_with_precision}; pub use self::scalar::Scalar; @@ -128,6 +128,20 @@ pub trait SliceExt { where F: FnMut(&T) -> K, K: PartialEq; + + /// Computes two indices which split a slice into three parts. + /// + /// - A prefix which matches `f` + /// - An inner portion + /// - A suffix which matches `f` and does not overlap with the prefix + /// + /// If all elements match `f`, the prefix becomes `self` and the suffix + /// will be empty. + /// + /// Returns the indices at which the inner portion and the suffix start. + fn split_prefix_suffix(&self, f: F) -> (usize, usize) + where + F: FnMut(&T) -> bool; } impl SliceExt for [T] { @@ -157,6 +171,19 @@ impl SliceExt for [T] { fn group_by_key(&self, f: F) -> GroupByKey<'_, T, F> { GroupByKey { slice: self, f } } + + fn split_prefix_suffix(&self, mut f: F) -> (usize, usize) + where + F: FnMut(&T) -> bool, + { + let start = self.iter().position(|v| !f(v)).unwrap_or(self.len()); + let end = self + .iter() + .skip(start) + .rposition(|v| !f(v)) + .map_or(start, |i| start + i + 1); + (start, end) + } } /// This struct is created by [`SliceExt::group_by_key`]. @@ -276,6 +303,15 @@ pub trait Get { fn set(&mut self, index: Index, component: Self::Component) { *self.get_mut(index) = component; } + + /// Builder-style method for setting a component. + fn with(mut self, index: Index, component: Self::Component) -> Self + where + Self: Sized, + { + self.set(index, component); + self + } } /// A numeric type. diff --git a/crates/typst/src/lib.rs b/crates/typst/src/lib.rs index 7d02aa426..580ba9e80 100644 --- a/crates/typst/src/lib.rs +++ b/crates/typst/src/lib.rs @@ -333,8 +333,6 @@ pub static ROUTINES: Routines = Routines { realize: typst_realize::realize, layout_fragment: typst_layout::layout_fragment, layout_frame: typst_layout::layout_frame, - layout_inline: typst_layout::layout_inline, - layout_box: typst_layout::layout_box, layout_list: typst_layout::layout_list, layout_enum: typst_layout::layout_enum, layout_grid: typst_layout::layout_grid, diff --git a/docs/guides/tables.md b/docs/guides/tables.md index 5c9cf11da..5b7efdc48 100644 --- a/docs/guides/tables.md +++ b/docs/guides/tables.md @@ -886,8 +886,8 @@ everything else by providing an array in the `align` argument: stroke: none, table.header[Day][Location][Hotel or Apartment][Activities], - [1], [Paris, France], [Hotel de L'Europe], [Arrival, Evening River Cruise], - [2], [Paris, France], [Hotel de L'Europe], [Louvre Museum, Eiffel Tower], + [1], [Paris, France], [Hôtel de l'Europe], [Arrival, Evening River Cruise], + [2], [Paris, France], [Hôtel de l'Europe], [Louvre Museum, Eiffel Tower], [3], [Lyon, France], [Lyon City Hotel], [City Tour, Local Cuisine Tasting], [4], [Geneva, Switzerland], [Lakeview Inn], [Lake Geneva, Red Cross Museum], [5], [Zermatt, Switzerland], [Alpine Lodge], [Visit Matterhorn, Skiing], @@ -911,8 +911,8 @@ bottom-aligned. Let's use a function instead to do so: stroke: none, table.header[Day][Location][Hotel or Apartment][Activities], - [1], [Paris, France], [Hotel de L'Europe], [Arrival, Evening River Cruise], - [2], [Paris, France], [Hotel de L'Europe], [Louvre Museum, Eiffel Tower], + [1], [Paris, France], [Hôtel de l'Europe], [Arrival, Evening River Cruise], + [2], [Paris, France], [Hôtel de l'Europe], [Louvre Museum, Eiffel Tower], <<< // ... remaining days omitted >>> [3], [Lyon, France], [Lyon City Hotel], [City Tour, Local Cuisine Tasting], >>> [4], [Geneva, Switzerland], [Lakeview Inn], [Lake Geneva, Red Cross Museum], diff --git a/docs/reference/scripting.md b/docs/reference/scripting.md index 89508eee0..6c7a7b338 100644 --- a/docs/reference/scripting.md +++ b/docs/reference/scripting.md @@ -120,7 +120,7 @@ You can use the underscore to discard elements in a destructuring pattern: The y coordinate is #y. ``` -Destructuring also work in argument lists of functions ... +Destructuring also works in argument lists of functions ... ```example #let left = (2, 4, 5) @@ -145,7 +145,7 @@ swap variables among other things. ## Conditionals With a conditional, you can display or compute different things depending on whether some condition is fulfilled. Typst supports `{if}`, `{else if}` and -`{else}` expression. When the condition evaluates to `{true}`, the conditional +`{else}` expressions. When the condition evaluates to `{true}`, the conditional yields the value resulting from the if's body, otherwise yields the value resulting from the else's body. @@ -269,7 +269,7 @@ the following two equivalent ways: The structure of a method call is `{value.method(..args)}` and its equivalent full function call is `{type(value).method(value, ..args)}`. The documentation -of each type lists it's scoped functions. You cannot currently define your own +of each type lists its scoped functions. You cannot currently define your own methods. ```example diff --git a/docs/src/html.rs b/docs/src/html.rs index a1206032d..4eb3954c3 100644 --- a/docs/src/html.rs +++ b/docs/src/html.rs @@ -486,7 +486,7 @@ impl World for DocWorld { fn file(&self, id: FileId) -> FileResult { assert!(id.package().is_none()); - Ok(Bytes::from_static( + Ok(Bytes::new( typst_dev_assets::get_by_name( &id.vpath().as_rootless_path().to_string_lossy(), ) diff --git a/docs/src/lib.rs b/docs/src/lib.rs index 5ca3724ab..2751500e3 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -25,6 +25,7 @@ use typst::layout::{Abs, Margin, PageElem, PagedDocument, LAYOUT}; use typst::loading::DATA_LOADING; use typst::math::MATH; use typst::model::MODEL; +use typst::pdf::PDF; use typst::symbols::SYMBOLS; use typst::text::{Font, FontBook, TEXT}; use typst::utils::LazyHash; @@ -77,7 +78,7 @@ static LIBRARY: LazyLock> = LazyLock::new(|| { static FONTS: LazyLock<(LazyHash, Vec)> = LazyLock::new(|| { let fonts: Vec<_> = typst_assets::fonts() .chain(typst_dev_assets::fonts()) - .flat_map(|data| Font::iter(Bytes::from_static(data))) + .flat_map(|data| Font::iter(Bytes::new(data))) .collect(); let book = FontBook::from_fonts(&fonts); (LazyHash::new(book), fonts) @@ -163,6 +164,7 @@ fn reference_pages(resolver: &dyn Resolver) -> PageModel { category_page(resolver, VISUALIZE), category_page(resolver, INTROSPECTION), category_page(resolver, DATA_LOADING), + category_page(resolver, PDF), ]; page } diff --git a/docs/src/main.rs b/docs/src/main.rs index 064bf9a8f..d14c1347a 100644 --- a/docs/src/main.rs +++ b/docs/src/main.rs @@ -46,11 +46,11 @@ impl Resolver for CliResolver<'_> { if let Some(code) = source { let code_safe = code.as_str(); Html::new(format!( - r#"

{code_safe}
Preview
"# + r#"
{code_safe}
Preview
"# )) } else { Html::new(format!( - r#"
Preview
"# + r#"
Preview
"# )) } } diff --git a/tests/fuzz/src/compile.rs b/tests/fuzz/src/compile.rs index 37e21deb9..3dedfb737 100644 --- a/tests/fuzz/src/compile.rs +++ b/tests/fuzz/src/compile.rs @@ -19,7 +19,7 @@ struct FuzzWorld { impl FuzzWorld { fn new(text: &str) -> Self { let data = typst_assets::fonts().next().unwrap(); - let font = Font::new(Bytes::from_static(data), 0).unwrap(); + let font = Font::new(Bytes::new(data), 0).unwrap(); let book = FontBook::from_fonts([&font]); Self { library: LazyHash::new(Library::default()), diff --git a/tests/ref/bibliography-grid-par.png b/tests/ref/bibliography-grid-par.png new file mode 100644 index 000000000..5befbcc54 Binary files /dev/null and b/tests/ref/bibliography-grid-par.png differ diff --git a/tests/ref/bibliography-indent-par.png b/tests/ref/bibliography-indent-par.png new file mode 100644 index 000000000..98a3c4d04 Binary files /dev/null and b/tests/ref/bibliography-indent-par.png differ diff --git a/tests/ref/cases-content-symbol.png b/tests/ref/cases-content-symbol.png new file mode 100644 index 000000000..b0b8a65e3 Binary files /dev/null and b/tests/ref/cases-content-symbol.png differ diff --git a/tests/ref/cases-content-text.png b/tests/ref/cases-content-text.png new file mode 100644 index 000000000..d97675829 Binary files /dev/null and b/tests/ref/cases-content-text.png differ diff --git a/tests/ref/enum-par.png b/tests/ref/enum-par.png new file mode 100644 index 000000000..ca923a526 Binary files /dev/null and b/tests/ref/enum-par.png differ diff --git a/tests/ref/figure-par.png b/tests/ref/figure-par.png new file mode 100644 index 000000000..d70bbcb12 Binary files /dev/null and b/tests/ref/figure-par.png differ diff --git a/tests/ref/heading-hanging-indent-auto.png b/tests/ref/heading-hanging-indent-auto.png new file mode 100644 index 000000000..823feb145 Binary files /dev/null and b/tests/ref/heading-hanging-indent-auto.png differ diff --git a/tests/ref/heading-hanging-indent-length.png b/tests/ref/heading-hanging-indent-length.png new file mode 100644 index 000000000..e371674ed Binary files /dev/null and b/tests/ref/heading-hanging-indent-length.png differ diff --git a/tests/ref/heading-hanging-indent-zero.png b/tests/ref/heading-hanging-indent-zero.png new file mode 100644 index 000000000..659ddbefb Binary files /dev/null and b/tests/ref/heading-hanging-indent-zero.png differ diff --git a/tests/ref/heading-par.png b/tests/ref/heading-par.png new file mode 100644 index 000000000..affc0df47 Binary files /dev/null and b/tests/ref/heading-par.png differ diff --git a/tests/ref/html/basic-table.html b/tests/ref/html/basic-table.html new file mode 100644 index 000000000..189a5b314 --- /dev/null +++ b/tests/ref/html/basic-table.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Thefirstand
thesecondrow
FooBazBar
12
34
Thelastrow
+ + diff --git a/tests/ref/html/block-html.html b/tests/ref/html/block-html.html new file mode 100644 index 000000000..d1716c6d7 --- /dev/null +++ b/tests/ref/html/block-html.html @@ -0,0 +1,11 @@ + + + + + + + +

Paragraph

+
Div
+ + diff --git a/tests/ref/html/box-html.html b/tests/ref/html/box-html.html new file mode 100644 index 000000000..b2a26533b --- /dev/null +++ b/tests/ref/html/box-html.html @@ -0,0 +1,10 @@ + + + + + + + +

Text Span.

+ + diff --git a/tests/ref/html/enum-par.html b/tests/ref/html/enum-par.html new file mode 100644 index 000000000..60d4592b7 --- /dev/null +++ b/tests/ref/html/enum-par.html @@ -0,0 +1,36 @@ + + + + + + + +
+
    +
  1. Hello
  2. +
  3. World
  4. +
+
+
+
    +
  1. +

    Hello

    +

    From

    +
  2. +
  3. World
  4. +
+
+
+
    +
  1. +

    Hello

    +

    From

    +

    The

    +
  2. +
  3. +

    World

    +
  4. +
+
+ + diff --git a/tests/ref/html/enum-start.html b/tests/ref/html/enum-start.html new file mode 100644 index 000000000..fc9b3c061 --- /dev/null +++ b/tests/ref/html/enum-start.html @@ -0,0 +1,13 @@ + + + + + + + +
    +
  1. Skipping
  2. +
  3. Ahead
  4. +
+ + diff --git a/tests/ref/html/heading-html-basic.html b/tests/ref/html/heading-html-basic.html new file mode 100644 index 000000000..54a22faf4 --- /dev/null +++ b/tests/ref/html/heading-html-basic.html @@ -0,0 +1,16 @@ + + + + + + + +

Level 1

+

Level 2

+

Level 3

+
Level 4
+
Level 5
+
Level 6
+
Level 7
+ + diff --git a/tests/ref/html/link-basic.html b/tests/ref/html/link-basic.html index 1f4e02e12..89cb54db5 100644 --- a/tests/ref/html/link-basic.html +++ b/tests/ref/html/link-basic.html @@ -5,17 +5,9 @@ -

- https://example.com/ -

-

- Some text text text -

-

- This link appears in the middle of a paragraph. -

-

- Contact hi@typst.app or call 123 for more information. -

+

https://example.com/

+

Some text text text

+

This link appears in the middle of a paragraph.

+

Contact hi@typst.app or call 123 for more information.

- \ No newline at end of file + diff --git a/tests/ref/html/list-par.html b/tests/ref/html/list-par.html new file mode 100644 index 000000000..7c747ff44 --- /dev/null +++ b/tests/ref/html/list-par.html @@ -0,0 +1,36 @@ + + + + + + + +
+
    +
  • Hello
  • +
  • World
  • +
+
+
+
    +
  • +

    Hello

    +

    From

    +
  • +
  • World
  • +
+
+
+
    +
  • +

    Hello

    +

    From

    +

    The

    +
  • +
  • +

    World

    +
  • +
+
+ + diff --git a/tests/ref/html/par-semantic-html.html b/tests/ref/html/par-semantic-html.html new file mode 100644 index 000000000..09c7d2fd0 --- /dev/null +++ b/tests/ref/html/par-semantic-html.html @@ -0,0 +1,16 @@ + + + + + + + +

Heading is no paragraph

+

I'm a paragraph.

+
I'm not.
+
+

We are two.

+

So we are paragraphs.

+
+ + diff --git a/tests/ref/html/quote-attribution-link.html b/tests/ref/html/quote-attribution-link.html new file mode 100644 index 000000000..c12d2ae2d --- /dev/null +++ b/tests/ref/html/quote-attribution-link.html @@ -0,0 +1,11 @@ + + + + + + + +
Compose papers faster
+

typst.com

+ + diff --git a/tests/ref/html/quote-nesting-html.html b/tests/ref/html/quote-nesting-html.html new file mode 100644 index 000000000..6b05a94a0 --- /dev/null +++ b/tests/ref/html/quote-nesting-html.html @@ -0,0 +1,10 @@ + + + + + + + +

When you said that “he surely meant that ‘she intended to say “I'm sorry”’”, I was quite confused.

+ + diff --git a/tests/ref/html/quote-plato.html b/tests/ref/html/quote-plato.html new file mode 100644 index 000000000..039835082 --- /dev/null +++ b/tests/ref/html/quote-plato.html @@ -0,0 +1,13 @@ + + + + + + + +
… ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι.
+

— Plato

+
… I seem, then, in just this little thing to be wiser than this man at any rate, that what I do not know I do not think I know either.
+

— from the Henry Cary literal translation of 1897

+ + diff --git a/tests/ref/html/terms-par.html b/tests/ref/html/terms-par.html new file mode 100644 index 000000000..78bc5df16 --- /dev/null +++ b/tests/ref/html/terms-par.html @@ -0,0 +1,42 @@ + + + + + + + +
+
+
Hello
+
A
+
World
+
B
+
+
+
+
+
Hello
+
+

A

+

From

+
+
World
+
B
+
+
+
+
+
Hello
+
+

A

+

From

+

The

+
+
World
+
+

B

+
+
+
+ + diff --git a/tests/ref/issue-1041-smartquotes-in-outline.png b/tests/ref/issue-1041-smartquotes-in-outline.png index 19a78ac69..00c276c11 100644 Binary files a/tests/ref/issue-1041-smartquotes-in-outline.png and b/tests/ref/issue-1041-smartquotes-in-outline.png differ diff --git a/tests/ref/issue-2048-outline-multiline.png b/tests/ref/issue-2048-outline-multiline.png new file mode 100644 index 000000000..0ecc2d80f Binary files /dev/null and b/tests/ref/issue-2048-outline-multiline.png differ diff --git a/tests/ref/issue-4476-outline-rtl-title-ending-in-ltr-text.png b/tests/ref/issue-4476-outline-rtl-title-ending-in-ltr-text.png new file mode 100644 index 000000000..c7c359a1b Binary files /dev/null and b/tests/ref/issue-4476-outline-rtl-title-ending-in-ltr-text.png differ diff --git a/tests/ref/issue-4476-rtl-title-ending-in-ltr-text.png b/tests/ref/issue-4476-rtl-title-ending-in-ltr-text.png deleted file mode 100644 index 94d06f1a4..000000000 Binary files a/tests/ref/issue-4476-rtl-title-ending-in-ltr-text.png and /dev/null differ diff --git a/tests/ref/issue-4859-outline-entry-show-set.png b/tests/ref/issue-4859-outline-entry-show-set.png new file mode 100644 index 000000000..33ff442d9 Binary files /dev/null and b/tests/ref/issue-4859-outline-entry-show-set.png differ diff --git a/tests/ref/issue-5176-cjk-title.png b/tests/ref/issue-5176-cjk-title.png deleted file mode 100644 index e904fbd77..000000000 Binary files a/tests/ref/issue-5176-cjk-title.png and /dev/null differ diff --git a/tests/ref/issue-5176-outline-cjk-title.png b/tests/ref/issue-5176-outline-cjk-title.png new file mode 100644 index 000000000..a206f92ca Binary files /dev/null and b/tests/ref/issue-5176-outline-cjk-title.png differ diff --git a/tests/ref/issue-5370-figure-caption-separator-outline.png b/tests/ref/issue-5370-figure-caption-separator-outline.png deleted file mode 100644 index a9b0d06e1..000000000 Binary files a/tests/ref/issue-5370-figure-caption-separator-outline.png and /dev/null differ diff --git a/tests/ref/issue-5503-enum-in-align.png b/tests/ref/issue-5503-enum-in-align.png new file mode 100644 index 000000000..4857e731b Binary files /dev/null and b/tests/ref/issue-5503-enum-in-align.png differ diff --git a/tests/ref/issue-5503-enum-interrupted-by-par-align.png b/tests/ref/issue-5503-enum-interrupted-by-par-align.png deleted file mode 100644 index 9cc942b4c..000000000 Binary files a/tests/ref/issue-5503-enum-interrupted-by-par-align.png and /dev/null differ diff --git a/tests/ref/issue-5503-list-interrupted-by-par-align.png b/tests/ref/issue-5503-list-in-align.png similarity index 100% rename from tests/ref/issue-5503-list-interrupted-by-par-align.png rename to tests/ref/issue-5503-list-in-align.png diff --git a/tests/ref/issue-5503-terms-interrupted-by-par-align.png b/tests/ref/issue-5503-terms-in-align.png similarity index 100% rename from tests/ref/issue-5503-terms-interrupted-by-par-align.png rename to tests/ref/issue-5503-terms-in-align.png diff --git a/tests/ref/issue-5719-enum-nested.png b/tests/ref/issue-5719-enum-nested.png new file mode 100644 index 000000000..767045449 Binary files /dev/null and b/tests/ref/issue-5719-enum-nested.png differ diff --git a/tests/ref/issue-5719-heading-nested.png b/tests/ref/issue-5719-heading-nested.png new file mode 100644 index 000000000..95bea42b2 Binary files /dev/null and b/tests/ref/issue-5719-heading-nested.png differ diff --git a/tests/ref/issue-5719-list-nested.png b/tests/ref/issue-5719-list-nested.png new file mode 100644 index 000000000..9c9a7cc62 Binary files /dev/null and b/tests/ref/issue-5719-list-nested.png differ diff --git a/tests/ref/issue-5719-terms-nested.png b/tests/ref/issue-5719-terms-nested.png new file mode 100644 index 000000000..8428ae4ee Binary files /dev/null and b/tests/ref/issue-5719-terms-nested.png differ diff --git a/tests/ref/issue-622-hide-meta-outline.png b/tests/ref/issue-622-hide-meta-outline.png index 6d8702b48..d627e0881 100644 Binary files a/tests/ref/issue-622-hide-meta-outline.png and b/tests/ref/issue-622-hide-meta-outline.png differ diff --git a/tests/ref/issue-758-link-repeat.png b/tests/ref/issue-758-link-repeat.png new file mode 100644 index 000000000..aaec20d23 Binary files /dev/null and b/tests/ref/issue-758-link-repeat.png differ diff --git a/tests/ref/issue-785-cite-locate.png b/tests/ref/issue-785-cite-locate.png index 5240aa772..d387ed0d5 100644 Binary files a/tests/ref/issue-785-cite-locate.png and b/tests/ref/issue-785-cite-locate.png differ diff --git a/tests/ref/link-empty-block.png b/tests/ref/link-empty-block.png new file mode 100644 index 000000000..ae10bdcf6 Binary files /dev/null and b/tests/ref/link-empty-block.png differ diff --git a/tests/ref/link-on-block.png b/tests/ref/link-on-block.png index 8fb7f6c66..eeeb264b9 100644 Binary files a/tests/ref/link-on-block.png and b/tests/ref/link-on-block.png differ diff --git a/tests/ref/list-par.png b/tests/ref/list-par.png new file mode 100644 index 000000000..5f0003d17 Binary files /dev/null and b/tests/ref/list-par.png differ diff --git a/tests/ref/math-call-named-args.png b/tests/ref/math-call-named-args.png new file mode 100644 index 000000000..bbe293e9f Binary files /dev/null and b/tests/ref/math-call-named-args.png differ diff --git a/tests/ref/math-call-spread-shorthand-clash.png b/tests/ref/math-call-spread-shorthand-clash.png new file mode 100644 index 000000000..4129ef5d2 Binary files /dev/null and b/tests/ref/math-call-spread-shorthand-clash.png differ diff --git a/tests/ref/math-equation-auto-wrapping.png b/tests/ref/math-equation-auto-wrapping.png index 9c600172e..2476d668c 100644 Binary files a/tests/ref/math-equation-auto-wrapping.png and b/tests/ref/math-equation-auto-wrapping.png differ diff --git a/tests/ref/math-lr-unparen.png b/tests/ref/math-lr-unparen.png new file mode 100644 index 000000000..d418b14ea Binary files /dev/null and b/tests/ref/math-lr-unparen.png differ diff --git a/tests/ref/math-mat-align-explicit-alternating.png b/tests/ref/math-mat-align-explicit-alternating.png index 37e8dc06a..1ebcc7b68 100644 Binary files a/tests/ref/math-mat-align-explicit-alternating.png and b/tests/ref/math-mat-align-explicit-alternating.png differ diff --git a/tests/ref/math-mat-align-explicit-left.png b/tests/ref/math-mat-align-explicit-left.png index 09ce93982..cb9819248 100644 Binary files a/tests/ref/math-mat-align-explicit-left.png and b/tests/ref/math-mat-align-explicit-left.png differ diff --git a/tests/ref/math-mat-align-explicit-right.png b/tests/ref/math-mat-align-explicit-right.png index 3592c0cf5..b537e6571 100644 Binary files a/tests/ref/math-mat-align-explicit-right.png and b/tests/ref/math-mat-align-explicit-right.png differ diff --git a/tests/ref/math-mat-align-implicit.png b/tests/ref/math-mat-align-implicit.png index 0c508efc4..b184d9140 100644 Binary files a/tests/ref/math-mat-align-implicit.png and b/tests/ref/math-mat-align-implicit.png differ diff --git a/tests/ref/math-mat-gaps.png b/tests/ref/math-mat-gaps.png index 5c954766c..405358776 100644 Binary files a/tests/ref/math-mat-gaps.png and b/tests/ref/math-mat-gaps.png differ diff --git a/tests/ref/math-mat-spread-1d.png b/tests/ref/math-mat-spread-1d.png new file mode 100644 index 000000000..0ac4e620b Binary files /dev/null and b/tests/ref/math-mat-spread-1d.png differ diff --git a/tests/ref/math-mat-spread-2d.png b/tests/ref/math-mat-spread-2d.png new file mode 100644 index 000000000..85cb4a6ad Binary files /dev/null and b/tests/ref/math-mat-spread-2d.png differ diff --git a/tests/ref/math-mat-spread.png b/tests/ref/math-mat-spread.png new file mode 100644 index 000000000..dc8b2bf7e Binary files /dev/null and b/tests/ref/math-mat-spread.png differ diff --git a/tests/ref/math-par.png b/tests/ref/math-par.png new file mode 100644 index 000000000..30d64794c Binary files /dev/null and b/tests/ref/math-par.png differ diff --git a/tests/ref/math-vec-align-explicit-alternating.png b/tests/ref/math-vec-align-explicit-alternating.png index 37e8dc06a..1ebcc7b68 100644 Binary files a/tests/ref/math-vec-align-explicit-alternating.png and b/tests/ref/math-vec-align-explicit-alternating.png differ diff --git a/tests/ref/outline-bookmark.png b/tests/ref/outline-bookmark.png index 66e5329d8..83c74444a 100644 Binary files a/tests/ref/outline-bookmark.png and b/tests/ref/outline-bookmark.png differ diff --git a/tests/ref/outline-entry-complex.png b/tests/ref/outline-entry-complex.png index d0491179b..d2ad49e79 100644 Binary files a/tests/ref/outline-entry-complex.png and b/tests/ref/outline-entry-complex.png differ diff --git a/tests/ref/outline-entry-inner.png b/tests/ref/outline-entry-inner.png new file mode 100644 index 000000000..5376c9961 Binary files /dev/null and b/tests/ref/outline-entry-inner.png differ diff --git a/tests/ref/outline-entry.png b/tests/ref/outline-entry.png index a46e483ce..acaa87d41 100644 Binary files a/tests/ref/outline-entry.png and b/tests/ref/outline-entry.png differ diff --git a/tests/ref/outline-first-line-indent.png b/tests/ref/outline-first-line-indent.png index e40b44094..e3341295c 100644 Binary files a/tests/ref/outline-first-line-indent.png and b/tests/ref/outline-first-line-indent.png differ diff --git a/tests/ref/outline-heading-start-of-page.png b/tests/ref/outline-heading-start-of-page.png new file mode 100644 index 000000000..e6dbbb5f1 Binary files /dev/null and b/tests/ref/outline-heading-start-of-page.png differ diff --git a/tests/ref/outline-indent-auto-mixed-prefix-short.png b/tests/ref/outline-indent-auto-mixed-prefix-short.png new file mode 100644 index 000000000..4b8c71079 Binary files /dev/null and b/tests/ref/outline-indent-auto-mixed-prefix-short.png differ diff --git a/tests/ref/outline-indent-auto-mixed-prefix.png b/tests/ref/outline-indent-auto-mixed-prefix.png new file mode 100644 index 000000000..097e0bf88 Binary files /dev/null and b/tests/ref/outline-indent-auto-mixed-prefix.png differ diff --git a/tests/ref/outline-indent-auto-no-prefix.png b/tests/ref/outline-indent-auto-no-prefix.png new file mode 100644 index 000000000..e746b35b6 Binary files /dev/null and b/tests/ref/outline-indent-auto-no-prefix.png differ diff --git a/tests/ref/outline-indent-auto.png b/tests/ref/outline-indent-auto.png new file mode 100644 index 000000000..53517abd8 Binary files /dev/null and b/tests/ref/outline-indent-auto.png differ diff --git a/tests/ref/outline-indent-fixed.png b/tests/ref/outline-indent-fixed.png new file mode 100644 index 000000000..16df5d88c Binary files /dev/null and b/tests/ref/outline-indent-fixed.png differ diff --git a/tests/ref/outline-indent-func.png b/tests/ref/outline-indent-func.png new file mode 100644 index 000000000..b9a4948c3 Binary files /dev/null and b/tests/ref/outline-indent-func.png differ diff --git a/tests/ref/outline-indent-no-numbering.png b/tests/ref/outline-indent-no-numbering.png deleted file mode 100644 index 7c3a0ec0b..000000000 Binary files a/tests/ref/outline-indent-no-numbering.png and /dev/null differ diff --git a/tests/ref/outline-indent-numbering.png b/tests/ref/outline-indent-numbering.png deleted file mode 100644 index e3195f766..000000000 Binary files a/tests/ref/outline-indent-numbering.png and /dev/null differ diff --git a/tests/ref/outline-indent-zero.png b/tests/ref/outline-indent-zero.png new file mode 100644 index 000000000..e85cba484 Binary files /dev/null and b/tests/ref/outline-indent-zero.png differ diff --git a/tests/ref/outline-par.png b/tests/ref/outline-par.png new file mode 100644 index 000000000..04c63f62c Binary files /dev/null and b/tests/ref/outline-par.png differ diff --git a/tests/ref/outline-spacing.png b/tests/ref/outline-spacing.png new file mode 100644 index 000000000..897a5f746 Binary files /dev/null and b/tests/ref/outline-spacing.png differ diff --git a/tests/ref/outline-styled-text.png b/tests/ref/outline-styled-text.png index 89f48070a..e4520d122 100644 Binary files a/tests/ref/outline-styled-text.png and b/tests/ref/outline-styled-text.png differ diff --git a/tests/ref/outline.png b/tests/ref/outline.png deleted file mode 100644 index aeb4ef32e..000000000 Binary files a/tests/ref/outline.png and /dev/null differ diff --git a/tests/ref/par-contains-block.png b/tests/ref/par-contains-block.png new file mode 100644 index 000000000..27ca0cf6b Binary files /dev/null and b/tests/ref/par-contains-block.png differ diff --git a/tests/ref/par-contains-parbreak.png b/tests/ref/par-contains-parbreak.png new file mode 100644 index 000000000..27ca0cf6b Binary files /dev/null and b/tests/ref/par-contains-parbreak.png differ diff --git a/tests/ref/par-explicit-trim-space.png b/tests/ref/par-explicit-trim-space.png new file mode 100644 index 000000000..ff542274a Binary files /dev/null and b/tests/ref/par-explicit-trim-space.png differ diff --git a/tests/ref/par-first-line-indent-all-enum.png b/tests/ref/par-first-line-indent-all-enum.png new file mode 100644 index 000000000..38cdea792 Binary files /dev/null and b/tests/ref/par-first-line-indent-all-enum.png differ diff --git a/tests/ref/par-first-line-indent-all-list.png b/tests/ref/par-first-line-indent-all-list.png new file mode 100644 index 000000000..cf731e79f Binary files /dev/null and b/tests/ref/par-first-line-indent-all-list.png differ diff --git a/tests/ref/par-first-line-indent-all-terms.png b/tests/ref/par-first-line-indent-all-terms.png new file mode 100644 index 000000000..4d5c8a69c Binary files /dev/null and b/tests/ref/par-first-line-indent-all-terms.png differ diff --git a/tests/ref/par-first-line-indent-all.png b/tests/ref/par-first-line-indent-all.png new file mode 100644 index 000000000..f283d1a46 Binary files /dev/null and b/tests/ref/par-first-line-indent-all.png differ diff --git a/tests/ref/par-hanging-indent-semantic.png b/tests/ref/par-hanging-indent-semantic.png new file mode 100644 index 000000000..e05795c7f Binary files /dev/null and b/tests/ref/par-hanging-indent-semantic.png differ diff --git a/tests/ref/par-semantic-align.png b/tests/ref/par-semantic-align.png new file mode 100644 index 000000000..eda496411 Binary files /dev/null and b/tests/ref/par-semantic-align.png differ diff --git a/tests/ref/par-semantic-tag.png b/tests/ref/par-semantic-tag.png new file mode 100644 index 000000000..2b26e5aee Binary files /dev/null and b/tests/ref/par-semantic-tag.png differ diff --git a/tests/ref/par-semantic.png b/tests/ref/par-semantic.png new file mode 100644 index 000000000..c2eca74d9 Binary files /dev/null and b/tests/ref/par-semantic.png differ diff --git a/tests/ref/par-show-children.png b/tests/ref/par-show-children.png new file mode 100644 index 000000000..bc81c5bdc Binary files /dev/null and b/tests/ref/par-show-children.png differ diff --git a/tests/ref/par-show-styles.png b/tests/ref/par-show-styles.png new file mode 100644 index 000000000..13a8065ad Binary files /dev/null and b/tests/ref/par-show-styles.png differ diff --git a/tests/ref/query-running-header.png b/tests/ref/query-running-header.png index 395bc2ae8..b19eec22b 100644 Binary files a/tests/ref/query-running-header.png and b/tests/ref/query-running-header.png differ diff --git a/tests/ref/quote-par.png b/tests/ref/quote-par.png new file mode 100644 index 000000000..89d15ddf9 Binary files /dev/null and b/tests/ref/quote-par.png differ diff --git a/tests/ref/smallcaps-all.png b/tests/ref/smallcaps-all.png new file mode 100644 index 000000000..f3be53f82 Binary files /dev/null and b/tests/ref/smallcaps-all.png differ diff --git a/tests/ref/table-cell-par.png b/tests/ref/table-cell-par.png new file mode 100644 index 000000000..24a2a55f2 Binary files /dev/null and b/tests/ref/table-cell-par.png differ diff --git a/tests/ref/terms-par.png b/tests/ref/terms-par.png new file mode 100644 index 000000000..910b5e5da Binary files /dev/null and b/tests/ref/terms-par.png differ diff --git a/tests/src/world.rs b/tests/src/world.rs index a08f1efa8..5c2678328 100644 --- a/tests/src/world.rs +++ b/tests/src/world.rs @@ -98,7 +98,7 @@ impl Default for TestBase { fn default() -> Self { let fonts: Vec<_> = typst_assets::fonts() .chain(typst_dev_assets::fonts()) - .flat_map(|data| Font::iter(Bytes::from_static(data))) + .flat_map(|data| Font::iter(Bytes::new(data))) .collect(); Self { @@ -140,8 +140,8 @@ impl FileSlot { self.file .get_or_init(|| { read(&system_path(self.id)?).map(|cow| match cow { - Cow::Owned(buf) => buf.into(), - Cow::Borrowed(buf) => Bytes::from_static(buf), + Cow::Owned(buf) => Bytes::new(buf), + Cow::Borrowed(buf) => Bytes::new(buf), }) }) .clone() diff --git a/tests/suite/scripting/arguments.typ b/tests/suite/foundations/arguments.typ similarity index 60% rename from tests/suite/scripting/arguments.typ rename to tests/suite/foundations/arguments.typ index e82f49624..1439b6be5 100644 --- a/tests/suite/scripting/arguments.typ +++ b/tests/suite/foundations/arguments.typ @@ -16,3 +16,12 @@ #let args = arguments(0, 1, a: 2, 3) // Error: 2-14 arguments do not contain key "b" and no default value was specified #args.at("b") + +--- arguments-plus-sum-join --- +#let lhs = arguments(0, "1", key: "value", 3) +#let rhs = arguments(other-key: 4, key: "other value", 3) +#let result = arguments(0, "1", 3, other-key: 4, key: "other value", 3) +#test(lhs + rhs, result) +#test({lhs; rhs}, result) +#test((lhs, rhs).sum(), result) +#test((lhs, rhs).join(), result) diff --git a/tests/suite/foundations/content.typ b/tests/suite/foundations/content.typ index 31ef1c54c..c3c119e33 100644 --- a/tests/suite/foundations/content.typ +++ b/tests/suite/foundations/content.typ @@ -50,12 +50,14 @@ `raw` --- content-fields-complex --- -// Integrated test for content fields. +// Integrated test for content fields. The idea is to parse a normal looking +// equation and symbolically evaluate it with the given variable values. + #let compute(equation, ..vars) = { let vars = vars.named() let f(elem) = { let func = elem.func() - if func == text { + if elem.has("text") { let text = elem.text if regex("^\d+$") in text { int(text) @@ -74,7 +76,7 @@ elem .children .filter(v => v != [ ]) - .split[+] + .split($+$.body) .map(xs => xs.fold(1, (prod, v) => prod * f(v))) .fold(0, (sum, v) => sum + v) } @@ -83,13 +85,15 @@ [With ] vars .pairs() - .map(p => $#p.first() = #p.last()$) + .map(((name, value)) => $#symbol(name) = value$) .join(", ", last: " and ") [ we have:] $ equation = result $ } #compute($x y + y^2$, x: 2, y: 3) +// This should generate the same output as: +// With $x = 2$ and $y = 3$ we have: $ x y + y^2 = 15 $ --- content-label-has-method --- // Test whether the label is accessible through the `has` method. diff --git a/tests/suite/layout/container.typ b/tests/suite/layout/container.typ index bb53a0411..f15ddfe4a 100644 --- a/tests/suite/layout/container.typ +++ b/tests/suite/layout/container.typ @@ -264,6 +264,13 @@ First! image("/assets/images/rhino.png", width: 30pt) ) +--- box-html html --- +Text #box[Span]. + +--- block-html html --- +Paragraph +#block[Div] + --- container-layoutable-child --- // Test box/block sizing with directly layoutable child. // diff --git a/tests/suite/layout/grid/html.typ b/tests/suite/layout/grid/html.typ new file mode 100644 index 000000000..2a7dfc2ce --- /dev/null +++ b/tests/suite/layout/grid/html.typ @@ -0,0 +1,32 @@ +--- basic-table html --- +#table( + columns: 3, + rows: 3, + + table.header( + [The], + [first], + [and], + [the], + [second], + [row], + table.hline(stroke: red) + ), + + table.cell(x: 1, rowspan: 2)[Baz], + [Foo], + [Bar], + + [1], + // Baz spans into the next cell + [2], + + table.cell(colspan: 2)[3], + [4], + + table.footer( + [The], + [last], + [row], + ), +) diff --git a/tests/suite/layout/table.typ b/tests/suite/layout/table.typ index f59d8b424..5c2b07492 100644 --- a/tests/suite/layout/table.typ +++ b/tests/suite/layout/table.typ @@ -310,6 +310,17 @@ ) } +--- table-cell-par --- +// Ensure that table cells aren't considered paragraphs by default. +#show par: highlight + +#table( + columns: 3, + [A], + block[B], + par[C], +) + --- grid-cell-in-table --- // Error: 8-19 cannot use `grid.cell` as a table cell // Hint: 8-19 use `table.cell` instead diff --git a/tests/suite/loading/csv.typ b/tests/suite/loading/csv.typ index 415488fcc..93545fc49 100644 --- a/tests/suite/loading/csv.typ +++ b/tests/suite/loading/csv.typ @@ -25,3 +25,7 @@ // Test error numbering with dictionary rows. // Error: 6-28 failed to parse CSV (found 3 instead of 2 fields in line 3) #csv("/assets/data/bad.csv", row-type: dictionary) + +--- csv-invalid-delimiter --- +// Error: 41-51 delimiter must be an ASCII character +#csv("/assets/data/zoo.csv", delimiter: "\u{2008}") diff --git a/tests/suite/math/alignment.typ b/tests/suite/math/alignment.typ index 63033ef5c..941c20556 100644 --- a/tests/suite/math/alignment.typ +++ b/tests/suite/math/alignment.typ @@ -4,10 +4,10 @@ // Test alignment step functions. #set page(width: 225pt) $ -"a" &= c \ -&= c + 1 & "By definition" \ -&= d + 100 + 1000 \ -&= x && "Even longer" \ +a &= c \ + &= c + 1 & "By definition" \ + &= d + 100 + 1000 \ + &= x && "Even longer" \ $ --- math-align-post-fix --- diff --git a/tests/suite/math/call.typ b/tests/suite/math/call.typ index 2477d9b6d..5caacfac6 100644 --- a/tests/suite/math/call.typ +++ b/tests/suite/math/call.typ @@ -8,14 +8,120 @@ $ pi(a,) $ $ pi(a,b) $ $ pi(a,b,) $ +--- math-call-unclosed-func --- +#let func(x) = x +// Error: 6-7 unclosed delimiter +$func(a$ + +--- math-call-unclosed-non-func --- +// Error: 5-6 unclosed delimiter +$sin(x$ + +--- math-call-named-args --- +#let func1(my: none) = my +#let func2(_my: none) = _my +#let func3(my-body: none) = my-body +#let func4(_my-body: none) = _my-body +#let func5(m: none) = m +$ func1(my: a) $ +$ func2(_my: a) $ +$ func3(my-body: a) $ +$ func4(_my-body: a) $ +$ func5(m: a) $ +$ func5(m: sigma : f) $ +$ func5(m: sigma:pi) $ + +--- math-call-named-args-no-expr --- +#let func(m: none) = m +// Error: 10 expected expression +$ func(m: ) $ + +--- math-call-named-args-duplicate --- +#let func(my: none) = my +// Error: 15-17 duplicate argument: my +$ func(my: a, my: b) $ + +--- math-call-named-args-shorthand-clash-1 --- +#let func(m: none) = m +// Error: 18-21 unexpected argument +$func(m: =) func(m:=)$ + +--- math-call-named-args-shorthand-clash-2 --- +#let func(m: none) = m +// Error: 41-45 unexpected argument +$func(m::) func(m: :=) func(m:: =) func(m::=)$ + +--- math-call-named-single-underscore --- +#let func(x) = x +// Error: 8-9 expected identifier, found underscore +$ func(_: a) $ + +--- math-call-named-single-char-error --- +#let func(m: none) = m +// Error: 8-13 unexpected argument +$ func(m : a) $ + +--- math-call-named-args-repr --- +#let args(..body) = body +#let check(it, r) = test-repr(it.body.text, r) +#check($args(_a: a)$, "arguments(_a: [a])") +#check($args(_a-b: a)$, "arguments(_a-b: [a])") +#check($args(a-b: a)$, "arguments(a-b: [a])") +#check($args(a-b-c: a)$, "arguments(a-b-c: [a])") +#check($args(a--c: a)$, "arguments(a--c: [a])") +#check($args(a: a-b)$, "arguments(a: sequence([a], [−], [b]))") +#check($args(a-b: a-b)$, "arguments(a-b: sequence([a], [−], [b]))") +#check($args(a-b)$, "arguments(sequence([a], [−], [b]))") + +--- math-call-spread-content-error --- +#let args(..body) = body +// Error: 7-16 cannot spread content +$args(..(a + b))$ + +--- math-call-spread-multiple-exprs --- +#let args(..body) = body +// Error: 10 expected comma or semicolon +$args(..a + b)$ + +--- math-call-spread-unexpected-dots --- +#let args(..body) = body +// Error: 8-10 unexpected dots +$args(#..range(1, 5).chunks(2))$ + +--- math-call-spread-shorthand-clash --- +#let func(body) = body +$func(...)$ + +--- math-call-spread-repr --- +#let args(..body) = body +#let check(it, r) = test-repr(it.body.text, r) +#check($args(..#range(0, 4).chunks(2))$, "arguments((0, 1), (2, 3))") +#check($#args(range(1, 5).chunks(2))$, "arguments(((1, 2), (3, 4)))") +#check($#args(..range(1, 5).chunks(2))$, "arguments((1, 2), (3, 4))") +#check($args(#(..range(2, 6).chunks(2)))$, "arguments(((2, 3), (4, 5)))") +#let nums = range(0, 4).chunks(2) +#check($args(..nums)$, "arguments((0, 1), (2, 3))") +#check($args(..nums;)$, "arguments(((0, 1), (2, 3)))") +#check($args(..nums, ..nums)$, "arguments((0, 1), (2, 3), (0, 1), (2, 3))") +#check($args(..nums, 4, 5)$, "arguments((0, 1), (2, 3), [4], [5])") +#check($args(..nums, ..#range(4, 6))$, "arguments((0, 1), (2, 3), 4, 5)") +#check($args(..nums, #range(4, 6))$, "arguments((0, 1), (2, 3), (4, 5))") +#check($args(..nums, 1, 2; 3, 4)$, "arguments(((0, 1), (2, 3), [1], [2]), ([3], [4]))") +#check($args(1, 2; ..nums)$, "arguments(([1], [2]), ((0, 1), (2, 3)))") +#check($args(1, 2; 3, 4)$, "arguments(([1], [2]), ([3], [4]))") +#check($args(1, 2; 3, 4; ..#range(5, 7))$, "arguments(([1], [2]), ([3], [4]), (5, 6))") +#check($args(1, 2; 3, 4, ..#range(5, 7))$, "arguments(([1], [2]), ([3], [4], 5, 6))") +#check($args(1, 2; 3, 4, ..#range(5, 7);)$, "arguments(([1], [2]), ([3], [4], 5, 6))") +#check($args(1, 2; 3, 4, ..#range(5, 7),)$, "arguments(([1], [2]), ([3], [4], 5, 6))") + --- math-call-repr --- #let args(..body) = body #let check(it, r) = test-repr(it.body.text, r) -#check($args(a)$, "([a])") -#check($args(a,)$, "([a])") -#check($args(a,b)$, "([a], [b])") -#check($args(a,b,)$, "([a], [b])") -#check($args(,a,b,,,)$, "([], [a], [b], [], [])") +#check($args(a)$, "arguments([a])") +#check($args(a,)$, "arguments([a])") +#check($args(a,b)$, "arguments([a], [b])") +#check($args(a,b,)$, "arguments([a], [b])") +#check($args(,a,b,,,)$, "arguments([], [a], [b], [], [])") --- math-call-2d-non-func --- // Error: 6-7 expected content, found array @@ -31,21 +137,49 @@ $ mat(#"code"; "wins") $ --- math-call-2d-repr --- #let args(..body) = body #let check(it, r) = test-repr(it.body.text, r) -#check($args(a;b)$, "(([a],), ([b],))") -#check($args(a,b;c)$, "(([a], [b]), ([c],))") -#check($args(a,b;c,d;e,f)$, "(([a], [b]), ([c], [d]), ([e], [f]))") +#check($args(a;b)$, "arguments(([a],), ([b],))") +#check($args(a,b;c)$, "arguments(([a], [b]), ([c],))") +#check($args(a,b;c,d;e,f)$, "arguments(([a], [b]), ([c], [d]), ([e], [f]))") + +--- math-call-2d-named-repr --- +#let args(..body) = (body.pos(), body.named()) +#let check(it, r) = test-repr(it.body.text, r) +#check($args(a: b)$, "((), (a: [b]))") +#check($args(1, 2; 3, 4)$, "((([1], [2]), ([3], [4])), (:))") +#check($args(a: b, 1, 2; 3, 4)$, "((([1], [2]), ([3], [4])), (a: [b]))") +#check($args(1, a: b, 2; 3, 4)$, "(([1], ([2],), ([3], [4])), (a: [b]))") +#check($args(1, 2, a: b; 3, 4)$, "(([1], [2], (), ([3], [4])), (a: [b]))") +#check($args(1, 2; a: b, 3, 4)$, "((([1], [2]), ([3], [4])), (a: [b]))") +#check($args(1, 2; 3, a: b, 4)$, "((([1], [2]), [3], ([4],)), (a: [b]))") +#check($args(1, 2; 3, 4, a: b)$, "((([1], [2]), [3], [4]), (a: [b]))") +#check($args(a: b, 1, 2, 3, c: d)$, "(([1], [2], [3]), (a: [b], c: [d]))") +#check($args(1, 2, 3; a: b)$, "((([1], [2], [3]),), (a: [b]))") +#check($args(a-b: a,, e:f;; d)$, "(([], (), ([],), ([d],)), (a-b: [a], e: [f]))") +#check($args(a: b, ..#range(0, 4))$, "((0, 1, 2, 3), (a: [b]))") + +--- math-call-2d-escape-repr --- +#let args(..body) = body +#let check(it, r) = test-repr(it.body.text, r) +#check($args(a\;b)$, "arguments(sequence([a], [;], [b]))") +#check($args(a\,b;c)$, "arguments((sequence([a], [,], [b]),), ([c],))") +#check($args(b\;c\,d;e)$, "arguments((sequence([b], [;], [c], [,], [d]),), ([e],))") +#check($args(a\: b)$, "arguments(sequence([a], [:], [ ], [b]))") +#check($args(a : b)$, "arguments(sequence([a], [ ], [:], [ ], [b]))") +#check($args(\..a)$, "arguments(sequence([.], [.], [a]))") +#check($args(.. a)$, "arguments(sequence([.], [.], [ ], [a]))") +#check($args(a..b)$, "arguments(sequence([a], [.], [.], [b]))") --- math-call-2d-repr-structure --- #let args(..body) = body #let check(it, r) = test-repr(it.body.text, r) -#check($args( a; b; )$, "(([a],), ([b],))") -#check($args(a; ; c)$, "(([a],), ([],), ([c],))") -#check($args(a b,/**/; b)$, "((sequence([a], [ ], [b]), []), ([b],))") -#check($args(a/**/b, ; b)$, "((sequence([a], [b]), []), ([b],))") -#check($args( ;/**/a/**/b/**/; )$, "(([],), (sequence([a], [b]),))") -#check($args( ; , ; )$, "(([],), ([], []))") +#check($args( a; b; )$, "arguments(([a],), ([b],))") +#check($args(a; ; c)$, "arguments(([a],), ([],), ([c],))") +#check($args(a b,/**/; b)$, "arguments((sequence([a], [ ], [b]), []), ([b],))") +#check($args(a/**/b, ; b)$, "arguments((sequence([a], [b]), []), ([b],))") +#check($args( ;/**/a/**/b/**/; )$, "arguments(([],), (sequence([a], [b]),))") +#check($args( ; , ; )$, "arguments(([],), ([], []))") #check($args(/**/; // funky whitespace/trivia - , /**/ ;/**/)$, "(([],), ([], []))") + , /**/ ;/**/)$, "arguments(([],), ([], []))") --- math-call-empty-args-non-func --- // Trailing commas and empty args introduce blank content in math @@ -56,9 +190,9 @@ $ sin( ,/**/x/**/, , /**/y, ,/**/, ) $ --- math-call-empty-args-repr --- #let args(..body) = body #let check(it, r) = test-repr(it.body.text, r) -#check($args(,x,,y,,)$, "([], [x], [], [y], [])") +#check($args(,x,,y,,)$, "arguments([], [x], [], [y], [])") // with whitespace/trivia: -#check($args( ,/**/x/**/, , /**/y, ,/**/, )$, "([], [x], [], [y], [], [])") +#check($args( ,/**/x/**/, , /**/y, ,/**/, )$, "arguments([], [x], [], [y], [], [])") --- math-call-value-non-func --- $ sin(1) $ diff --git a/tests/suite/math/delimited.typ b/tests/suite/math/delimited.typ index 226740501..794ffd8aa 100644 --- a/tests/suite/math/delimited.typ +++ b/tests/suite/math/delimited.typ @@ -41,8 +41,8 @@ $floor(x/2), ceil(x/2), abs(x), norm(x)$ --- math-lr-color --- // Test colored delimiters $ lr( - text("(", fill: #green) a/b - text(")", fill: #blue) + text(\(, fill: #green) a/b + text(\), fill: #blue) ) $ --- math-lr-mid --- @@ -125,3 +125,11 @@ $ lr(size: #3em, |)_a^b lr(size: #3em, zws|)_a^b --- issue-4188-lr-corner-brackets --- // Test positioning of U+231C to U+231F $⌜a⌟⌞b⌝$ = $⌜$$a$$⌟$$⌞$$b$$⌝$ + +--- math-lr-unparen --- +// Test that unparen with brackets stays as an LrElem. +#let item = $limits(sum)_i$ +$ + 1 / ([item]) quad + 1 / [item] +$ diff --git a/tests/suite/math/mat.typ b/tests/suite/math/mat.typ index 391ff1677..b7d6a6871 100644 --- a/tests/suite/math/mat.typ +++ b/tests/suite/math/mat.typ @@ -54,6 +54,30 @@ $ a + mat(delim: #none, 1, 2; 3, 4) + b $ $ mat(1, 2; 3, 4; delim: "[") $, ) +--- math-mat-spread --- +// Test argument spreading in matrix. +$ mat(..#range(1, 5).chunks(2)) + mat(#(..range(2).map(_ => range(2)))) $ + +#let nums = ((1,) * 5).intersperse(0).chunks(3) +$ mat(..nums, delim: "[") $ + +--- math-mat-spread-1d --- +$ mat(..#range(1, 5) ; 1, ..#range(2, 5)) + mat(..#range(1, 3), ..#range(3, 5) ; ..#range(1, 4), 4) $ + +--- math-mat-spread-2d --- +#let nums = range(0, 2).map(i => (i, i+1)) +$ mat(..nums, delim: "|",) + mat(..nums; delim: "|",) $ +$ mat(..nums) mat(..nums;) \ + mat(..nums;,) mat(..nums,) $ + +--- math-mat-spread-expected-array-error --- +#let nums = range(0, 2).map(i => (i, i+1)) +// Error: 15-16 expected array, found content +$ mat(..nums, 0, 1) $ + --- math-mat-gap --- #set math.mat(gap: 1em) $ mat(1, 2; 3, 4) $ @@ -61,6 +85,8 @@ $ mat(1, 2; 3, 4) $ --- math-mat-gaps --- #set math.mat(row-gap: 1em, column-gap: 2em) $ mat(1, 2; 3, 4) $ +$ mat(column-gap: #1em, 1, 2; 3, 4) + mat(row-gap: #2em, 1, 2; 3, 4) $ --- math-mat-augment --- // Test matrix line drawing (augmentation). diff --git a/tests/suite/math/stretch.typ b/tests/suite/math/stretch.typ index 1377f4d21..d145f72a1 100644 --- a/tests/suite/math/stretch.typ +++ b/tests/suite/math/stretch.typ @@ -63,8 +63,8 @@ $ ext(bar.v) quad ext(bar.v.double) quad // Test stretch when base is given with shorthand. $stretch(||, size: #2em)$ $stretch(\(, size: #2em)$ -$stretch("⟧", size: #2em)$ -$stretch("|", size: #2em)$ +$stretch(⟧, size: #2em)$ +$stretch(|, size: #2em)$ $stretch(->, size: #2em)$ $stretch(↣, size: #2em)$ @@ -87,7 +87,7 @@ $ body^"text" $ #{ let body = $stretch(=)$ for i in range(24) { - body = $body$ + body = $body$ } $body^"long text"$ } diff --git a/tests/suite/math/symbols.typ b/tests/suite/math/symbols.typ index 65a483162..6dd9db622 100644 --- a/tests/suite/math/symbols.typ +++ b/tests/suite/math/symbols.typ @@ -2,7 +2,7 @@ --- math-symbol-basic --- #let sym = symbol("s", ("basic", "s")) -#test($sym.basic$, $#"s"$) +#test($sym.basic$, $s$) --- math-symbol-underscore --- #let sym = symbol("s", ("test_underscore", "s")) @@ -16,7 +16,7 @@ $sym.test-dash$ --- math-symbol-double --- #let sym = symbol("s", ("test.basic", "s")) -#test($sym.test.basic$, $#"s"$) +#test($sym.test.basic$, $s$) --- math-symbol-double-underscore --- #let sym = symbol("s", ("one.test_underscore", "s")) diff --git a/tests/suite/math/text.typ b/tests/suite/math/text.typ index 760910f4d..8c7611114 100644 --- a/tests/suite/math/text.typ +++ b/tests/suite/math/text.typ @@ -43,3 +43,8 @@ $sum_(k in NN)^prime 1/k^2$ // Test script-script in a fraction. $ 1/(x^A) $ #[#set text(size:18pt); $1/(x^A)$] vs. #[#set text(size:14pt); $x^A$] + +--- math-par --- +// Ensure that math does not produce paragraphs. +#show par: highlight +$ a + "bc" + #[c] + #box[d] + #block[e] $ diff --git a/tests/suite/model/bibliography.typ b/tests/suite/model/bibliography.typ index 20eb8acd9..6de44e240 100644 --- a/tests/suite/model/bibliography.typ +++ b/tests/suite/model/bibliography.typ @@ -53,6 +53,24 @@ Now we have multiple bibliographies containing @glacier-melt @keshav2007read @Zee04 #bibliography("/assets/bib/works_too.bib", style: "mla") +--- bibliography-grid-par --- +// Ensure that a grid-based bibliography does not produce paragraphs. +#show par: highlight + +@Zee04 +@keshav2007read + +#bibliography("/assets/bib/works_too.bib") + +--- bibliography-indent-par --- +// Ensure that an indent-based bibliography does not produce paragraphs. +#show par: highlight + +@Zee04 +@keshav2007read + +#bibliography("/assets/bib/works_too.bib", style: "mla") + --- issue-4618-bibliography-set-heading-level --- // Test that the bibliography block's heading is set to 2 by the show rule, // and therefore should be rendered like a level-2 heading. Notably, this diff --git a/tests/suite/model/enum.typ b/tests/suite/model/enum.typ index 258c6f6bc..7176b04e2 100644 --- a/tests/suite/model/enum.typ +++ b/tests/suite/model/enum.typ @@ -101,6 +101,13 @@ a + 0. [Red], [Green], [Blue], [Red], ) +--- enum-start html --- +#enum( + start: 3, + [Skipping], + [Ahead], +) + --- enum-numbering-closure-nested --- // Test numbering with closure and nested lists. #set enum(numbering: n => super[#n]) @@ -176,19 +183,51 @@ a + 0. #set enum(number-align: horizon) #set enum(number-align: bottom) +--- enum-par render html --- +// Check whether the contents of enum items become paragraphs. +#show par: it => if target() != "html" { highlight(it) } else { it } + +// No paragraphs. +#block[ + + Hello + + World +] + +#block[ + + Hello // Paragraphs + + From + + World // No paragraph because it's a tight enum +] + +#block[ + + Hello // Paragraphs + + From + + The + + + World // Paragraph because it's a wide enum +] + --- issue-2530-enum-item-panic --- // Enum item (pre-emptive) #enum.item(none)[Hello] #enum.item(17)[Hello] ---- issue-5503-enum-interrupted-by-par-align --- -// `align` is block-level and should interrupt an enum -// but not a `par` +--- issue-5503-enum-in-align --- +// `align` is block-level and should interrupt an enum. + a + b -#par(leading: 5em)[+ par] +#align(right)[+ c] + d -#par[+ par] -+ f -#align(right)[+ align] -+ h + +--- issue-5719-enum-nested --- +// Enums can be immediately nested. +1. A +2. 1. B + 2. C ++ + D + + E ++ = F + G diff --git a/tests/suite/model/figure.typ b/tests/suite/model/figure.typ index 0e5db4d07..37fb4ecda 100644 --- a/tests/suite/model/figure.typ +++ b/tests/suite/model/figure.typ @@ -180,6 +180,17 @@ We can clearly see that @fig-cylinder and caption: [Underlined], ) +--- figure-par --- +// Ensure that a figure body is considered a paragraph. +#show par: highlight + +#figure[Text] + +#figure( + [Text], + caption: [A caption] +) + --- figure-and-caption-show --- // Test creating custom figure and custom caption @@ -289,9 +300,3 @@ HI#footnote.entry(clearance: 2.5em)[There] ) #c - ---- issue-5370-figure-caption-separator-outline --- -// Test that language-dependant caption separator is respected in outline. -#outline(title: none, target: figure) -#set text(lang: "ru") -#figure(rect(), caption: [Rectangle]) diff --git a/tests/suite/model/heading.typ b/tests/suite/model/heading.typ index d182724c8..4e04e5c56 100644 --- a/tests/suite/model/heading.typ +++ b/tests/suite/model/heading.typ @@ -97,6 +97,18 @@ comment spans lines = Fake level 2 == Fake level 3 +--- heading-hanging-indent-auto --- +#set heading(numbering: "1.1.a.") += State of the Art + +--- heading-hanging-indent-zero --- +#set heading(numbering: "1.1.a.", hanging-indent: 0pt) += State of the Art + +--- heading-hanging-indent-length --- +#set heading(numbering: "1.1.a.", hanging-indent: 2em) += State of the Art In Multi-Line + --- heading-offset-and-level --- // Passing level directly still overrides all other set values #set heading(numbering: "1.1", offset: 1) @@ -115,3 +127,33 @@ Not in heading // Error: 1:19-1:25 cannot reference heading without numbering // Hint: 1:19-1:25 you can enable heading numbering with `#set heading(numbering: "1.")` Cannot be used as @intro + +--- heading-par --- +// Ensure that heading text isn't considered a paragraph. +#show par: highlight += Heading + +--- heading-html-basic html --- +// level 1 => h2 +// ... +// level 5 => h6 +// level 6 => div with role=heading and aria-level=7 +// ... + += Level 1 +== Level 2 +=== Level 3 +==== Level 4 +===== Level 5 +// Warning: 1-15 heading of level 6 was transformed to
, which is not supported by all assistive technology +// Hint: 1-15 HTML only supports

to

, not +// Hint: 1-15 you may want to restructure your document so that it doesn't contain deep headings +====== Level 6 +// Warning: 1-16 heading of level 7 was transformed to
, which is not supported by all assistive technology +// Hint: 1-16 HTML only supports

to

, not +// Hint: 1-16 you may want to restructure your document so that it doesn't contain deep headings +======= Level 7 + +--- issue-5719-heading-nested --- +// Headings may not be nested like this. += = A diff --git a/tests/suite/model/link.typ b/tests/suite/model/link.typ index 7cced8560..bd6c8a307 100644 --- a/tests/suite/model/link.typ +++ b/tests/suite/model/link.typ @@ -75,3 +75,14 @@ Text Text // Error: 2-20 label `` occurs multiple times in the document #link()[Nope.] + +--- link-empty-block --- +#link("", block(height: 10pt, width: 100%)) + +--- issue-758-link-repeat --- +#let url = "https://typst.org/" +#let body = [Hello #box(width: 1fr, repeat[.])] + +Inline: #link(url, body) + +#link(url, block(inset: 4pt, [Block: ] + body)) diff --git a/tests/suite/model/list.typ b/tests/suite/model/list.typ index 138abf70e..9bed930bb 100644 --- a/tests/suite/model/list.typ +++ b/tests/suite/model/list.typ @@ -77,6 +77,49 @@ _Shopping list_ #test(indented, manual) +--- list-indent-bracket-nesting --- +// Test list indent nesting behavior when directly at a starting bracket. + +#let indented = { + [- indented + - less + ] + [- indented + - same + - then less + - then same + ] + [- indented + - more + - then same + - then less + ] +} + +#let item = list.item +#let manual = { + { + item[indented]; [ ] + item[less]; [ ] + } + { + item[indented]; [ ] + item[same]; [ ] + item[then less #{ + item[then same] + }]; [ ] + } + { + item[indented #{ + item[more] + }]; [ ] + item[then same]; [ ] + item[then less]; [ ] + } +} + +#test(indented, manual) + --- list-tabs --- // This works because tabs are used consistently. - A with 1 tab @@ -195,6 +238,33 @@ World #text(red)[- World] #text(green)[- What up?] +--- list-par render html --- +// Check whether the contents of list items become paragraphs. +#show par: it => if target() != "html" { highlight(it) } else { it } + +#block[ + // No paragraphs. + - Hello + - World +] + +#block[ + - Hello // Paragraphs + + From + - World // No paragraph because it's a tight list. +] + +#block[ + - Hello // Paragraphs either way + + From + + The + + - World // Paragraph because it's a wide list. +] + --- issue-2530-list-item-panic --- // List item (pre-emptive) #list.item[Hello] @@ -219,17 +289,18 @@ World part($ x $ + parbreak() + parbreak() + list[A]) } ---- issue-5503-list-interrupted-by-par-align --- -// `align` is block-level and should interrupt a list -// but not a `par` +--- issue-5503-list-in-align --- +// `align` is block-level and should interrupt a list. #show list: [List] - a - b -#par(leading: 5em)[- c] -- d -- e -#par[- f] -- g -- h #align(right)[- i] - j + +--- issue-5719-list-nested --- +// Lists can be immediately nested. +- A +- - B + - C +- = D + E diff --git a/tests/suite/model/outline.typ b/tests/suite/model/outline.typ index a8426d6c6..49fd7d7cb 100644 --- a/tests/suite/model/outline.typ +++ b/tests/suite/model/outline.typ @@ -1,10 +1,195 @@ ---- outline --- -#set page(height: 200pt, margin: (bottom: 20pt), numbering: "1") +--- outline-spacing --- +#set heading(numbering: "1.a.") +#set outline.entry(fill: none) +#show outline.entry.where(level: 1): set block(above: 1.2em) + +#outline() + +#show heading: none += A +== B +== C += D +== E + +--- outline-indent-auto --- +#set heading(numbering: "I.i.") +#set page(width: 150pt) +#show heading: none + +#context test(outline.indent, auto) +#outline() + += A +== B +== C +== D +=== Title that breaks across lines += E +== F +=== Aligned + +--- outline-indent-auto-mixed-prefix --- +#show heading: none +#show outline.entry.where(level: 1): strong + +#outline() + +#set heading(numbering: "I.i.") += A +== B +=== Title that breaks += C +== D += E +#[ + #set heading(numbering: none) + = F + == Numberless title that breaks + === G +] += H + +--- outline-indent-auto-mixed-prefix-short --- +#show heading: none + +#outline() + +#set heading(numbering: "I.i.") += A +#set heading(numbering: none) += B + +--- outline-indent-auto-no-prefix --- +#show heading: none + +#outline() + += A +== B +=== Title that breaks across lines += C +== D +=== E + +--- outline-indent-zero --- +#set heading(numbering: "1.a.") +#show heading: none + +#outline(indent: 0pt) + += A +== B +=== C +==== Title that breaks across lines +#set heading(numbering: none) +== E += F + +--- outline-indent-fixed --- +#set heading(numbering: "1.a.") +#show heading: none + +#outline(indent: 1em) + += A +== B +=== C +==== Title that breaks +#set heading(numbering: none) +== E += F + +--- outline-indent-func --- +#set heading(numbering: "1.a.") +#show heading: none + +#outline(indent: n => (0pt, 1em, 2.5em, 3em).at(n)) + += A +== B +=== C +==== Title breaks +#set heading(numbering: none) +== E += F + +--- outline-indent-bad-type --- +// Error: 2-35 expected relative length, found dictionary +#outline(indent: n => (a: "dict")) + += Heading + +--- outline-entry --- +#set page(width: 150pt) +#set heading(numbering: "1.") + +#show outline.entry.where(level: 1): set block(above: 12pt) +#show outline.entry.where(level: 1): strong + +#outline(indent: auto) + +#show heading: none += Introduction += Background +== History +== State of the Art += Analysis +== Setup + +--- outline-entry-complex --- +#set page(width: 150pt, numbering: "I", margin: (bottom: 20pt)) +#set heading(numbering: "1.") + +#set outline.entry(fill: repeat[--]) +#show outline.entry.where(level: 1): it => link( + it.element.location(), + it.indented(it.prefix(), { + emph(it.body()) + [ ] + text(luma(100), box(width: 1fr, repeat[--·--])) + [ ] + it.page() + }) +) + +#counter(page).update(3) +#outline() + +#show heading: none + += Top heading +== Not top heading +=== Lower heading +=== Lower too +== Also not top + +#pagebreak() +#set page(numbering: "1") + += Another top heading +== Middle heading +=== Lower heading + +--- outline-entry-inner --- +#set heading(numbering: "1.") +#show outline.entry: it => block(it.inner()) +#show heading: none + +#set outline.entry(fill: repeat[ -- ]) +#outline() + += A += B + +--- outline-heading-start-of-page --- +#set page(width: 140pt, height: 200pt, margin: (bottom: 20pt), numbering: "1") #set heading(numbering: "(1/a)") #show heading.where(level: 1): set text(12pt) #show heading.where(level: 2): set text(10pt) -#outline(fill: none) +#set outline.entry(fill: none) +#outline() = A = B @@ -23,66 +208,28 @@ A == F ==== G +--- outline-bookmark --- +// Ensure that `bookmarked` option doesn't affect the outline +#set heading(numbering: "(I)", bookmarked: false) +#set outline.entry(fill: none) +#show heading: none +#outline() + += A + --- outline-styled-text --- #outline(title: none) = #text(blue)[He]llo ---- outline-bookmark --- -#outline(title: none, fill: none) - -// Ensure 'bookmarked' option doesn't affect the outline -#set heading(numbering: "(I)", bookmarked: false) -= A - ---- outline-indent-numbering --- -// With heading numbering -#set page(width: 200pt) -#set heading(numbering: "1.a.") -#show heading: none -#set outline(fill: none) - -#context test(outline.indent, none) -#outline(indent: none) -#outline(indent: auto) -#outline(indent: 2em) -#outline(indent: n => ([-], [], [==], [====]).at(n)) - -= A -== B -== C -=== D -==== E - ---- outline-indent-no-numbering --- -// Without heading numbering -#set page(width: 200pt) -#show heading: none -#set outline(fill: none) - -#outline(indent: none) -#outline(indent: auto) -#outline(indent: n => 2em * n) - -= About -== History - ---- outline-indent-bad-type --- -// Error: 2-35 expected relative length or content, found dictionary -#outline(indent: n => (a: "dict")) - -= Heading - --- outline-first-line-indent --- #set par(first-line-indent: 1.5em) #set heading(numbering: "1.1.a.") -#show outline.entry.where(level: 1): it => { - v(0.5em, weak: true) - strong(it) -} +#show outline.entry.where(level: 1): strong #outline() +#show heading: none = Introduction = Background == History @@ -90,85 +237,63 @@ A = Analysis == Setup ---- outline-entry --- -#set page(width: 150pt) -#set heading(numbering: "1.") - -#show outline.entry.where( - level: 1 -): it => { - v(12pt, weak: true) - strong(it) -} - -#outline(indent: auto) -#v(1.2em, weak: true) - -#set text(8pt) -#show heading: set block(spacing: 0.65em) - -= Introduction -= Background -== History -== State of the Art -= Analysis -== Setup - ---- outline-entry-complex --- -#set page(width: 150pt, numbering: "I", margin: (bottom: 20pt)) -#set heading(numbering: "1.") -#show outline.entry.where(level: 1): it => [ - #let loc = it.element.location() - #let num = numbering(loc.page-numbering(), ..counter(page).at(loc)) - #emph(link(loc, it.body)) - #text(luma(100), box(width: 1fr, repeat[#it.fill.body;·])) - #link(loc, num) -] - -#counter(page).update(3) -#outline(indent: auto, fill: repeat[--]) -#v(1.2em, weak: true) - -#set text(8pt) -#show heading: set block(spacing: 0.65em) - -= Top heading -== Not top heading -=== Lower heading -=== Lower too -== Also not top - -#pagebreak() -#set page(numbering: "1") - -= Another top heading -== Middle heading -=== Lower heading - --- outline-bad-element --- // Error: 2-27 cannot outline metadata #outline(target: metadata) #metadata("hello") +--- outline-par --- +// Ensure that an outline does not produce paragraphs. +#show par: highlight + +#outline() + += A += B += C + +--- issue-2048-outline-multiline --- +// Without the word joiner between the dots and the page number, +// the page number would be alone in its line. +#set page(width: 125pt) +#set heading(numbering: "1.a.") +#show heading: none + +#outline() + += A +== This just fits here + --- issue-2530-outline-entry-panic-text --- // Outline entry (pre-emptive) -// Error: 2-48 cannot outline text -#outline.entry(1, [Hello], [World!], none, [1]) +// Error: 2-27 cannot outline text +#outline.entry(1, [Hello]) --- issue-2530-outline-entry-panic-heading --- // Outline entry (pre-emptive, improved error) -// Error: 2-55 heading must have a location -// Hint: 2-55 try using a query or a show rule to customize the outline.entry instead -#outline.entry(1, heading[Hello], [World!], none, [1]) +// Error: 2-34 heading must have a location +// Hint: 2-34 try using a show rule to customize the outline.entry instead +#outline.entry(1, heading[Hello]) ---- issue-4476-rtl-title-ending-in-ltr-text --- +--- issue-4476-outline-rtl-title-ending-in-ltr-text --- #set text(lang: "he") #outline() +#show heading: none = הוקוס Pocus = זוהי כותרת שתורגמה על ידי מחשב ---- issue-5176-cjk-title --- +--- issue-4859-outline-entry-show-set --- +#set heading(numbering: "1.a.") +#show outline.entry.where(level: 1): set outline.entry(fill: none) +#show heading: none + +#outline() + += A +== B + +--- issue-5176-outline-cjk-title --- #set text(font: "Noto Serif CJK SC") #show heading: none diff --git a/tests/suite/model/par.typ b/tests/suite/model/par.typ index 0c2b5cb54..e76690064 100644 --- a/tests/suite/model/par.typ +++ b/tests/suite/model/par.typ @@ -19,6 +19,105 @@ heaven Would through the airy region stream so bright That birds would sing and think it were not night. See, how she leans her cheek upon her hand! O, that I were a glove upon that hand, That I might touch that cheek! +--- par-semantic --- +#show par: highlight + +I'm a paragraph. + +#align(center, table( + columns: 3, + + // No paragraphs. + [A], + block[B], + block[C *D*], + + // Paragraphs. + par[E], + [ + + F + ], + [ + G + + ], + + // Paragraphs. + parbreak() + [H], + [I] + parbreak(), + parbreak() + [J] + parbreak(), + + // Paragraphs. + [K #v(10pt)], + [#v(10pt) L], + [#place[] M], + + // Paragraphs. + [ + N + + O + ], + [#par[P]#par[Q]], + // No paragraphs. + [#block[R]#block[S]], +)) + +--- par-semantic-html html --- += Heading is no paragraph + +I'm a paragraph. + +#html.elem("div")[I'm not.] + +#html.elem("div")[ + We are two. + + So we are paragraphs. +] + +--- par-semantic-tag --- +#show par: highlight +#block[ + #metadata(none) + A + #metadata(none) +] + +#block(width: 100%, metadata(none) + align(center)[A]) +#block(width: 100%, align(center)[A] + metadata(none)) + +--- par-semantic-align --- +#show par: highlight +#show bibliography: none +#set block(width: 100%, stroke: 1pt, inset: 5pt) + +#bibliography("/assets/bib/works.bib") + +#block[ + #set align(right) + Hello +] + +#block[ + #set align(right) + Hello + @netwok +] + +#block[ + Hello + #align(right)[World] + You +] + +#block[ + Hello + #align(right)[@netwok] + You +] + --- par-leading-and-spacing --- // Test changing leading and spacing. #set par(spacing: 1em, leading: 2pt) @@ -57,6 +156,57 @@ starts a paragraph, also with indent. ثم يصبح النص رطبًا وقابل للطرق ويبدو المستند رائعًا. +--- par-first-line-indent-all --- +#set par( + first-line-indent: (amount: 12pt, all: true), + spacing: 5pt, + leading: 5pt, +) +#set block(spacing: 1.2em) +#show heading: set text(size: 10pt) + += Heading +All paragraphs are indented. + +Even the first. + +--- par-first-line-indent-all-list --- +#show list.where(tight: false): set list(spacing: 1.2em) +#set par( + first-line-indent: (amount: 12pt, all: true), + spacing: 5pt, + leading: 5pt, +) + +- A #parbreak() B #line(length: 100%) C + +- D + +--- par-first-line-indent-all-enum --- +#show enum.where(tight: false): set enum(spacing: 1.2em) +#set par( + first-line-indent: (amount: 12pt, all: true), + spacing: 5pt, + leading: 5pt, +) + ++ A #parbreak() B #line(length: 100%) C + ++ D + +--- par-first-line-indent-all-terms --- +#show terms.where(tight: false): set terms(spacing: 1.2em) +#set terms(hanging-indent: 10pt) +#set par( + first-line-indent: (amount: 12pt, all: true), + spacing: 5pt, + leading: 5pt, +) + +/ Term A: B \ C #parbreak() D #line(length: 100%) E + +/ Term F: G + --- par-spacing-and-first-line-indent --- // This is madness. #set par(first-line-indent: 12pt) @@ -69,6 +219,12 @@ Why would anybody ever ... #set par(hanging-indent: 15pt, justify: true) #lorem(10) +--- par-hanging-indent-semantic --- +#set par(hanging-indent: 15pt) += I am not affected + +I am affected by hanging indent. + --- par-hanging-indent-manual-linebreak --- #set par(hanging-indent: 1em) Welcome \ here. Does this work well? @@ -83,6 +239,22 @@ Welcome \ here. Does this work well? // Ensure that trailing whitespace layouts as intended. #box(fill: aqua, " ") +--- par-contains-parbreak --- +#par[ + Hello + // Warning: 4-14 parbreak may not occur inside of a paragraph and was ignored + #parbreak() + World +] + +--- par-contains-block --- +#par[ + Hello + // Warning: 4-11 block may not occur inside of a paragraph and was ignored + #block[] + World +] + --- par-empty-metadata --- // Check that metadata still works in a zero length paragraph. #block(height: 0pt)[#""#metadata(false)] @@ -94,6 +266,49 @@ Welcome \ here. Does this work well? #set text(hyphenate: false) Lorem ipsum dolor #metadata(none) nonumy eirmod tempor. +--- par-show-children --- +// Variant 1: Prevent recursion by checking the children. +#let p = counter("p") +#let step = p.step() +#let nr = context p.display() +#show par: it => { + if it.body.at("children", default: ()).at(0, default: none) == step { + return it + } + par(step + [§#nr ] + it.body) +} + += A + +B + +C #parbreak() D + +#block[E] + +#block[F #parbreak() G] + +--- par-show-styles --- +// Variant 2: Prevent recursion by observing a style. +#let revoke = metadata("revoke") +#show par: it => { + if bibliography.title == revoke { return it } + set bibliography(title: revoke) + let p = counter("p") + par[#p.step()§#context p.display() #it.body] +} + += A + +B + +C + +--- par-explicit-trim-space --- +A + +#par[ B ] + --- issue-4278-par-trim-before-equation --- #set par(justify: true) #lorem(6) aa $a = c + b$ diff --git a/tests/suite/model/quote.typ b/tests/suite/model/quote.typ index 2c93f92cd..51c4bba59 100644 --- a/tests/suite/model/quote.typ +++ b/tests/suite/model/quote.typ @@ -84,3 +84,37 @@ And I quote: #quote(attribution: [René Descartes])[cogito, ergo sum]. // With custom quotes. #set smartquote(quotes: (single: ("<", ">"), double: ("(", ")"))) #quote[A #quote[nested] quote] + +--- quote-plato html --- +#set quote(block: true) + +#quote(attribution: [Plato])[ + ... ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι + ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι. +] +#quote(attribution: [from the Henry Cary literal translation of 1897])[ + ... I seem, then, in just this little thing to be wiser than this man at + any rate, that what I do not know I do not think I know either. +] + +--- quote-nesting-html html --- +When you said that #quote[he surely meant that #quote[she intended to say #quote[I'm sorry]]], I was quite confused. + +--- quote-attribution-link html --- +#quote( + block: true, + attribution: link("https://typst.app/home")[typst.com] +)[ + Compose papers faster +] + +--- quote-par --- +// Ensure that an inline quote is part of a paragraph, but a block quote +// does not result in paragraphs. +#show par: highlight + +An inline #quote[quote.] + +#quote(block: true, attribution: [The Test Author])[ + A block-level quote. +] diff --git a/tests/suite/model/terms.typ b/tests/suite/model/terms.typ index 61fe20b0d..103a8033e 100644 --- a/tests/suite/model/terms.typ +++ b/tests/suite/model/terms.typ @@ -59,6 +59,34 @@ Not in list // Error: 8 expected colon / Hello +--- terms-par render html --- +// Check whether the contents of term list items become paragraphs. +#show par: it => if target() != "html" { highlight(it) } else { it } + +// No paragraphs. +#block[ + / Hello: A + / World: B +] + +#block[ + / Hello: A // Paragraphs + + From + / World: B // No paragraphs because it's a tight term list. +] + +#block[ + / Hello: A // Paragraphs + + From + + The + + / World: B // Paragraph because it's a wide term list. +] + + --- issue-1050-terms-indent --- #set page(width: 110pt) #set par(first-line-indent: 0.5cm) @@ -76,17 +104,15 @@ Not in list // Term item (pre-emptive) #terms.item[Hello][World!] ---- issue-5503-terms-interrupted-by-par-align --- -// `align` is block-level and should interrupt a `terms` -// but not a `par` +--- issue-5503-terms-in-align --- +// `align` is block-level and should interrupt a `terms`. #show terms: [Terms] / a: a -/ b: b -#par(leading: 5em)[/ c: c] -/ d: d -/ e: e -#par[/ f: f] -/ g: g -/ h: h #align(right)[/ i: i] / j: j + +--- issue-5719-terms-nested --- +// Term lists can be immediately nested. +/ Term A: 1 +/ Term B: / Term C: 2 + / Term D: 3 diff --git a/tests/suite/pdf/embed.typ b/tests/suite/pdf/embed.typ new file mode 100644 index 000000000..83f006d63 --- /dev/null +++ b/tests/suite/pdf/embed.typ @@ -0,0 +1,30 @@ +// Test file embeddings. The tests here so far are unsatisfactory because we +// have no PDF testing infrastructure. That should be improved in the future. + +--- pdf-embed --- +#pdf.embed("/assets/text/hello.txt") +#pdf.embed( + "/assets/data/details.toml", + relationship: "supplement", + mime-type: "application/toml", + description: "Information about a secret project", +) + +--- pdf-embed-bytes --- +#pdf.embed("hello.txt", read("/assets/text/hello.txt", encoding: none)) +#pdf.embed( + "a_file_name.txt", + read("/assets/text/hello.txt", encoding: none), + relationship: "supplement", + mime-type: "text/plain", + description: "A description", +) + +--- pdf-embed-invalid-relationship --- +#pdf.embed( + "/assets/text/hello.txt", + // Error: 17-23 expected "source", "data", "alternative", "supplement", or none + relationship: "test", + mime-type: "text/plain", + description: "A test file", +) diff --git a/tests/suite/scripting/call.typ b/tests/suite/scripting/call.typ index 5a5fb326d..af5f5eaab 100644 --- a/tests/suite/scripting/call.typ +++ b/tests/suite/scripting/call.typ @@ -141,7 +141,7 @@ #{ let save(..args) = { test(type(args), arguments) - test(repr(args), "(three: true, 1, 2)") + test(repr(args), "arguments(three: true, 1, 2)") } save(1, 2, three: true) @@ -159,7 +159,7 @@ #{ let more = (c: 3, d: 4) let tostr(..args) = repr(args) - test(tostr(a: 1, ..more, b: 2), "(a: 1, c: 3, d: 4, b: 2)") + test(tostr(a: 1, ..more, b: 2), "arguments(a: 1, c: 3, d: 4, b: 2)") } --- call-args-spread-none --- diff --git a/tests/suite/scripting/methods.typ b/tests/suite/scripting/methods.typ index 5deea2cfa..566e9d9a5 100644 --- a/tests/suite/scripting/methods.typ +++ b/tests/suite/scripting/methods.typ @@ -31,7 +31,7 @@ #numbers.fun() --- method-unknown-but-field-exists --- -// Error: 2:4-2:10 type content has no method `stroke` +// Error: 2:4-2:10 element line has no method `stroke` // Hint: 2:4-2:10 did you mean to access the field `stroke`? #let l = line(stroke: red) #l.stroke() diff --git a/tests/suite/scripting/params.typ b/tests/suite/scripting/params.typ index 688124f20..0f14fc3ee 100644 --- a/tests/suite/scripting/params.typ +++ b/tests/suite/scripting/params.typ @@ -29,17 +29,17 @@ // Spread at beginning. #{ let f(..a, b) = (a, b) - test(repr(f(1)), "((), 1)") - test(repr(f(1, 2, 3)), "((1, 2), 3)") - test(repr(f(1, 2, 3, 4, 5)), "((1, 2, 3, 4), 5)") + test(repr(f(1)), "(arguments(), 1)") + test(repr(f(1, 2, 3)), "(arguments(1, 2), 3)") + test(repr(f(1, 2, 3, 4, 5)), "(arguments(1, 2, 3, 4), 5)") } --- params-sink-in-middle --- // Spread in the middle. #{ let f(a, ..b, c) = (a, b, c) - test(repr(f(1, 2)), "(1, (), 2)") - test(repr(f(1, 2, 3, 4, 5)), "(1, (2, 3, 4), 5)") + test(repr(f(1, 2)), "(1, arguments(), 2)") + test(repr(f(1, 2, 3, 4, 5)), "(1, arguments(2, 3, 4), 5)") } --- params-sink-unnamed-empty --- diff --git a/tests/suite/syntax/shebang.typ b/tests/suite/syntax/shebang.typ new file mode 100644 index 000000000..c2eb2e43c --- /dev/null +++ b/tests/suite/syntax/shebang.typ @@ -0,0 +1,7 @@ +// Test shebang support. + +--- shebang --- +#!typst compile + +// Error: 2-3 the character `!` is not valid in code +#!not-a-shebang diff --git a/tests/suite/text/case.typ b/tests/suite/text/case.typ index 2bf68bc33..964ff28b6 100644 --- a/tests/suite/text/case.typ +++ b/tests/suite/text/case.typ @@ -6,6 +6,14 @@ #test(upper(memes), "ARE MEMES GREAT?") #test(upper("Ελλάδα"), "ΕΛΛΆΔΑ") +--- cases-content-text --- +// Check that cases are applied to text nested in content +#lower(box("HI!")) + +--- cases-content-symbol --- +// Check that cases are applied to symbols nested in content +#lower($H I !$.body) + --- upper-bad-type --- // Error: 8-9 expected string or content, found integer #upper(1) diff --git a/tests/suite/text/smallcaps.typ b/tests/suite/text/smallcaps.typ index 6f36a028b..981e41c4c 100644 --- a/tests/suite/text/smallcaps.typ +++ b/tests/suite/text/smallcaps.typ @@ -10,3 +10,7 @@ #show smallcaps: set text(fill: red) #smallcaps[Smallcaps] + +--- smallcaps-all --- +#smallcaps(all: false)[Test 012] \ +#smallcaps(all: true)[Test 012] diff --git a/tools/test-helper/src/extension.ts b/tools/test-helper/src/extension.ts index 2e2b7d218..b98b4bad4 100644 --- a/tools/test-helper/src/extension.ts +++ b/tools/test-helper/src/extension.ts @@ -474,7 +474,7 @@ function getWebviewContent( data-vscode-context='{"webviewSection":"png"}' src="${webViewSrcs.png}" alt="Placeholder" - /> + >
@@ -484,7 +484,7 @@ function getWebviewContent( data-vscode-context='{"webviewSection":"ref"}' src="${webViewSrcs.ref}" alt="Placeholder" - /> + >
${stdoutHtml}