From a4ac4e656267e718a5cf60d1e959f74b2b7346f3 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 13 Jan 2025 20:19:37 +0100 Subject: [PATCH 01/34] Make `typst-timing` WASM-compatible (#5689) --- Cargo.lock | 11 + Cargo.toml | 1 + crates/typst-timing/Cargo.toml | 6 + crates/typst-timing/src/lib.rs | 372 +++++++++++++++++++-------------- 4 files changed, 235 insertions(+), 155 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c0bfe138..8aa7c0ec1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3093,6 +3093,7 @@ dependencies = [ "parking_lot", "serde", "serde_json", + "web-sys", ] [[package]] @@ -3418,6 +3419,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 b4f704f80..1be7816a7 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/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() + } +} From 63c4720ed2b9e034fda6810a9c0e521355a24c44 Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski <133046678+wrzian@users.noreply.github.com> Date: Thu, 16 Jan 2025 08:40:29 -0500 Subject: [PATCH 02/34] Fix list indent when starting at an open bracket (#5677) --- crates/typst-syntax/src/parser.rs | 19 +++++++++----- tests/suite/model/list.typ | 43 +++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index 335b8f1a2..a65e5ff6b 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -1605,10 +1605,12 @@ impl AtNewline { _ => 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) + } } } } @@ -1703,10 +1705,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/tests/suite/model/list.typ b/tests/suite/model/list.typ index 138abf70e..b3d9a830b 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 From c22c47b9c97062309ed841679bb49fc15bb6a398 Mon Sep 17 00:00:00 2001 From: Eric Biedert Date: Thu, 16 Jan 2025 14:40:57 +0100 Subject: [PATCH 03/34] Add font exception for NewCM Sans Math (#5682) --- crates/typst-library/src/text/font/exceptions.rs | 2 ++ 1 file changed, 2 insertions(+) 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() From b90ad470d60f4a90e3ba2e78aa4746fbe08783ab Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 21 Jan 2025 12:10:06 +0100 Subject: [PATCH 04/34] Remove redundant doc comments on standard arguments (#5725) --- crates/typst-library/src/foundations/args.rs | 2 -- crates/typst-library/src/foundations/array.rs | 25 ------------------- crates/typst-library/src/foundations/calc.rs | 11 -------- .../typst-library/src/foundations/datetime.rs | 1 - crates/typst-library/src/foundations/func.rs | 4 --- crates/typst-library/src/foundations/mod.rs | 1 - .../typst-library/src/foundations/plugin.rs | 1 - crates/typst-library/src/foundations/str.rs | 2 -- .../typst-library/src/foundations/symbol.rs | 1 - .../typst-library/src/foundations/target.rs | 5 +--- .../src/introspection/counter.rs | 14 ----------- .../typst-library/src/introspection/here.rs | 5 +--- .../typst-library/src/introspection/locate.rs | 2 -- .../typst-library/src/introspection/query.rs | 2 -- .../typst-library/src/introspection/state.rs | 10 -------- crates/typst-library/src/layout/layout.rs | 1 - crates/typst-library/src/layout/measure.rs | 3 --- crates/typst-library/src/loading/cbor.rs | 2 -- crates/typst-library/src/loading/csv.rs | 2 -- crates/typst-library/src/loading/json.rs | 2 -- crates/typst-library/src/loading/read.rs | 1 - crates/typst-library/src/loading/toml.rs | 2 -- crates/typst-library/src/loading/xml.rs | 2 -- crates/typst-library/src/loading/yaml.rs | 2 -- crates/typst-library/src/math/root.rs | 1 - crates/typst-library/src/model/numbering.rs | 2 -- crates/typst-library/src/visualize/color.rs | 19 -------------- .../typst-library/src/visualize/gradient.rs | 4 --- .../typst-library/src/visualize/image/mod.rs | 1 - crates/typst-library/src/visualize/polygon.rs | 2 +- crates/typst-library/src/visualize/stroke.rs | 2 -- crates/typst-library/src/visualize/tiling.rs | 1 - 32 files changed, 3 insertions(+), 132 deletions(-) diff --git a/crates/typst-library/src/foundations/args.rs b/crates/typst-library/src/foundations/args.rs index 44aa9dd6d..430c4e9ad 100644 --- a/crates/typst-library/src/foundations/args.rs +++ b/crates/typst-library/src/foundations/args.rs @@ -305,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] diff --git a/crates/typst-library/src/foundations/array.rs b/crates/typst-library/src/foundations/array.rs index e79a4e930..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. 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/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/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/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/plugin.rs b/crates/typst-library/src/foundations/plugin.rs index adf23a47c..d41261edc 100644 --- a/crates/typst-library/src/foundations/plugin.rs +++ b/crates/typst-library/src/foundations/plugin.rs @@ -152,7 +152,6 @@ impl Plugin { /// Creates a new plugin from a WebAssembly file. #[func(constructor)] pub fn construct( - /// The engine. engine: &mut Engine, /// A path to a WebAssembly file or raw WebAssembly bytes. /// diff --git a/crates/typst-library/src/foundations/str.rs b/crates/typst-library/src/foundations/str.rs index 2e90b3071..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, diff --git a/crates/typst-library/src/foundations/symbol.rs b/crates/typst-library/src/foundations/symbol.rs index 72800f311..3045970de 100644 --- a/crates/typst-library/src/foundations/symbol.rs +++ b/crates/typst-library/src/foundations/symbol.rs @@ -187,7 +187,6 @@ impl Symbol { /// ``` #[func(constructor)] pub fn construct( - /// The callsite span. span: Span, /// The variants of the symbol. /// 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/introspection/counter.rs b/crates/typst-library/src/introspection/counter.rs index d26a9f9f5..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 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 e6ab926bf..cc3f566b5 100644 --- a/crates/typst-library/src/introspection/state.rs +++ b/crates/typst-library/src/introspection/state.rs @@ -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/layout.rs b/crates/typst-library/src/layout/layout.rs index 05e4f6d9b..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. 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/loading/cbor.rs b/crates/typst-library/src/loading/cbor.rs index 13d551201..2bdeb80ef 100644 --- a/crates/typst-library/src/loading/cbor.rs +++ b/crates/typst-library/src/loading/cbor.rs @@ -19,7 +19,6 @@ use crate::loading::{DataSource, Load}; /// floating point numbers, which may result in an approximative value. #[func(scope, title = "CBOR")] pub fn cbor( - /// The engine. engine: &mut Engine, /// A path to a CBOR file or raw CBOR bytes. /// @@ -40,7 +39,6 @@ impl cbor { /// directly. #[func(title = "Decode CBOR")] pub fn decode( - /// The engine. engine: &mut Engine, /// CBOR data. data: Spanned, diff --git a/crates/typst-library/src/loading/csv.rs b/crates/typst-library/src/loading/csv.rs index 8171c4832..e5dabfaa6 100644 --- a/crates/typst-library/src/loading/csv.rs +++ b/crates/typst-library/src/loading/csv.rs @@ -25,7 +25,6 @@ use crate::loading::{DataSource, Load, Readable}; /// ``` #[func(scope, title = "CSV")] pub fn csv( - /// The engine. engine: &mut Engine, /// Path to a CSV file or raw CSV bytes. /// @@ -102,7 +101,6 @@ impl csv { /// directly. #[func(title = "Decode CSV")] pub fn decode( - /// The engine. engine: &mut Engine, /// CSV data. data: Spanned, diff --git a/crates/typst-library/src/loading/json.rs b/crates/typst-library/src/loading/json.rs index 3128d77da..035c5e4a7 100644 --- a/crates/typst-library/src/loading/json.rs +++ b/crates/typst-library/src/loading/json.rs @@ -50,7 +50,6 @@ use crate::loading::{DataSource, Load, Readable}; /// ``` #[func(scope, title = "JSON")] pub fn json( - /// The engine. engine: &mut Engine, /// Path to a JSON file or raw JSON bytes. /// @@ -71,7 +70,6 @@ impl json { /// directly. #[func(title = "Decode JSON")] pub fn decode( - /// The engine. engine: &mut Engine, /// JSON data. data: Spanned, diff --git a/crates/typst-library/src/loading/read.rs b/crates/typst-library/src/loading/read.rs index bf363f846..32dadc799 100644 --- a/crates/typst-library/src/loading/read.rs +++ b/crates/typst-library/src/loading/read.rs @@ -24,7 +24,6 @@ use crate::World; /// ``` #[func] pub fn read( - /// The engine. engine: &mut Engine, /// Path to a file. /// diff --git a/crates/typst-library/src/loading/toml.rs b/crates/typst-library/src/loading/toml.rs index e3a01cdd5..402207b02 100644 --- a/crates/typst-library/src/loading/toml.rs +++ b/crates/typst-library/src/loading/toml.rs @@ -28,7 +28,6 @@ use crate::loading::{DataSource, Load, Readable}; /// ``` #[func(scope, title = "TOML")] pub fn toml( - /// The engine. engine: &mut Engine, /// A path to a TOML file or raw TOML bytes. /// @@ -50,7 +49,6 @@ impl toml { /// directly. #[func(title = "Decode TOML")] pub fn decode( - /// The engine. engine: &mut Engine, /// TOML data. data: Spanned, diff --git a/crates/typst-library/src/loading/xml.rs b/crates/typst-library/src/loading/xml.rs index 53ec3d93b..ca467c238 100644 --- a/crates/typst-library/src/loading/xml.rs +++ b/crates/typst-library/src/loading/xml.rs @@ -57,7 +57,6 @@ use crate::loading::{DataSource, Load, Readable}; /// ``` #[func(scope, title = "XML")] pub fn xml( - /// The engine. engine: &mut Engine, /// A path to an XML file or raw XML bytes. /// @@ -83,7 +82,6 @@ impl xml { /// directly. #[func(title = "Decode XML")] pub fn decode( - /// The engine. engine: &mut Engine, /// XML data. data: Spanned, diff --git a/crates/typst-library/src/loading/yaml.rs b/crates/typst-library/src/loading/yaml.rs index 2eb26be8f..5767cb640 100644 --- a/crates/typst-library/src/loading/yaml.rs +++ b/crates/typst-library/src/loading/yaml.rs @@ -40,7 +40,6 @@ use crate::loading::{DataSource, Load, Readable}; /// ``` #[func(scope, title = "YAML")] pub fn yaml( - /// The engine. engine: &mut Engine, /// A path to a YAML file or raw YAML bytes. /// @@ -61,7 +60,6 @@ impl yaml { /// directly. #[func(title = "Decode YAML")] pub fn decode( - /// The engine. engine: &mut Engine, /// YAML data. data: Spanned, 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/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/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 0f0602011..77f8426e4 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -112,7 +112,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. data: Readable, diff --git a/crates/typst-library/src/visualize/polygon.rs b/crates/typst-library/src/visualize/polygon.rs index 465f2c1a7..42b083431 100644 --- a/crates/typst-library/src/visualize/polygon.rs +++ b/crates/typst-library/src/visualize/polygon.rs @@ -67,8 +67,8 @@ impl PolygonElem { /// ``` #[func(title = "Regular Polygon")] pub fn regular( - /// The call span of this function. span: Span, + /// How to fill the polygon. See the general /// [polygon's documentation]($polygon.fill) for more details. #[named] diff --git a/crates/typst-library/src/visualize/stroke.rs b/crates/typst-library/src/visualize/stroke.rs index 97a1535db..a0830cf19 100644 --- a/crates/typst-library/src/visualize/stroke.rs +++ b/crates/typst-library/src/visualize/stroke.rs @@ -97,8 +97,6 @@ impl Stroke { /// ``` #[func(constructor)] pub fn construct( - /// 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 color or gradient to use for the stroke. diff --git a/crates/typst-library/src/visualize/tiling.rs b/crates/typst-library/src/visualize/tiling.rs index d699d3b6d..98a71f927 100644 --- a/crates/typst-library/src/visualize/tiling.rs +++ b/crates/typst-library/src/visualize/tiling.rs @@ -138,7 +138,6 @@ impl Tiling { #[func(constructor)] pub fn construct( engine: &mut Engine, - /// The callsite span. span: Span, /// The bounding box of each cell of the tiling. #[named] From b45f574703f674c962e8678b4af0aabe081216a1 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 22 Jan 2025 13:58:57 +0100 Subject: [PATCH 05/34] Move no-hyphenation style in link from show to show-set rule (#5731) --- crates/typst-library/src/model/link.rs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/crates/typst-library/src/model/link.rs b/crates/typst-library/src/model/link.rs index 4558cb394..5df6bead4 100644 --- a/crates/typst-library/src/model/link.rs +++ b/crates/typst-library/src/model/link.rs @@ -6,8 +6,8 @@ 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 +16,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 +31,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. @@ -119,20 +124,26 @@ 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)))) + } }) } } +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() From 6fcc4322845482c1810c26ee7f6fc8f6fed20d7d Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 22 Jan 2025 14:24:14 +0100 Subject: [PATCH 06/34] Don't link items if container is already linked (#5732) --- crates/typst-layout/src/flow/collect.rs | 32 ++--- crates/typst-layout/src/inline/collect.rs | 20 ++-- crates/typst-layout/src/inline/line.rs | 25 ++-- crates/typst-layout/src/inline/mod.rs | 2 +- crates/typst-layout/src/inline/shaping.rs | 2 + crates/typst-layout/src/lib.rs | 1 + crates/typst-layout/src/math/fragment.rs | 15 +-- crates/typst-layout/src/math/stretch.rs | 3 +- crates/typst-layout/src/modifiers.rs | 110 ++++++++++++++++++ .../typst-library/src/foundations/content.rs | 3 +- crates/typst-library/src/layout/frame.rs | 52 +-------- crates/typst-library/src/model/link.rs | 5 +- tests/ref/issue-758-link-repeat.png | Bin 0 -> 1836 bytes tests/ref/link-empty-block.png | Bin 0 -> 96 bytes tests/ref/link-on-block.png | Bin 2422 -> 2355 bytes tests/suite/model/link.typ | 11 ++ 16 files changed, 184 insertions(+), 97 deletions(-) create mode 100644 crates/typst-layout/src/modifiers.rs create mode 100644 tests/ref/issue-758-link-repeat.png create mode 100644 tests/ref/link-empty-block.png diff --git a/crates/typst-layout/src/flow/collect.rs b/crates/typst-layout/src/flow/collect.rs index 12cfa152e..76d7b7433 100644 --- a/crates/typst-layout/src/flow/collect.rs +++ b/crates/typst-layout/src/flow/collect.rs @@ -22,6 +22,7 @@ use typst_library::text::TextElem; use typst_library::World; use super::{layout_multi_block, layout_single_block}; +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. @@ -377,8 +378,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 +475,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 +578,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/inline/collect.rs b/crates/typst-layout/src/inline/collect.rs index fcf7508e9..6023f5c63 100644 --- a/crates/typst-layout/src/inline/collect.rs +++ b/crates/typst-layout/src/inline/collect.rs @@ -13,6 +13,7 @@ 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. @@ -36,7 +37,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 +68,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 +84,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(), } @@ -210,8 +211,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)); } } } @@ -222,8 +225,11 @@ 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)); diff --git a/crates/typst-layout/src/inline/line.rs b/crates/typst-layout/src/inline/line.rs index ef7e26c3c..fba4bef80 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 = '-'; @@ -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( @@ -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()); diff --git a/crates/typst-layout/src/inline/mod.rs b/crates/typst-layout/src/inline/mod.rs index 658e30846..bedc54d63 100644 --- a/crates/typst-layout/src/inline/mod.rs +++ b/crates/typst-layout/src/inline/mod.rs @@ -23,7 +23,7 @@ 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::{ diff --git a/crates/typst-layout/src/inline/shaping.rs b/crates/typst-layout/src/inline/shaping.rs index d6b7632b6..2ed95f14f 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. /// @@ -326,6 +327,7 @@ impl<'a> ShapedText<'a> { offset += width; } + frame.modify(&FrameModifiers::get_in(self.styles)); frame } diff --git a/crates/typst-layout/src/lib.rs b/crates/typst-layout/src/lib.rs index 2e8c1129b..56d7afe11 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; 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/stretch.rs b/crates/typst-layout/src/math/stretch.rs index 6379bdb2e..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; @@ -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/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-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/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/model/link.rs b/crates/typst-library/src/model/link.rs index 5df6bead4..24b746b7e 100644 --- a/crates/typst-library/src/model/link.rs +++ b/crates/typst-library/src/model/link.rs @@ -1,7 +1,6 @@ use std::ops::Deref; use ecow::{eco_format, EcoString}; -use smallvec::SmallVec; use crate::diag::{bail, warning, At, SourceResult, StrResult}; use crate::engine::Engine; @@ -90,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 { diff --git a/tests/ref/issue-758-link-repeat.png b/tests/ref/issue-758-link-repeat.png new file mode 100644 index 0000000000000000000000000000000000000000..aaec20d23cb31d58004b500b11d5ea28635ae274 GIT binary patch literal 1836 zcmV+{2h;e8P)XxL|x*>o^{nk*BEk@pKlbgx8XU?6O`=6OJ_s)O1{iUQ+q?>|3 z0j+>mqJUOFE1;DqpcT+cIt%EEii#&so{;`e&HDK93?hEZhFPV#RaG3%a`leO-)Vd0Apig^u2rcl6G7yEG(u@ zooa1uJ!Hs`?%lgPI5-R&Hf-|b$=W_-4IMfZWzwWcqLy!6{{0<#_3A~+pgwZs$fZk{ z)~s2Rk&z*DLqkKcpd34P?AoVrOUf;K75nYuBz=vBJm4N8A45 z#S0V$X;@g;fddD|j2V-VkdU37ee>o`hN_2$$IF*5+ZV`FL9O!a*)w6edi5&lfB^$e zo;=y!|IndB8#iuTwQAL#J$o7(8x0od9zA-D9zEK$RX%j@JkEL^yd4v>$Cj`_Y-7A#mGEOBvh)22e;jB@ZrN*Dh&;2CU{9n2}*5kZCza*3Msm+3~1D5MBsdhp=EnVFfQB6D+d6r`%EN*Lqg<4Kc~lWA~n zZfB+ux=T2l~ z?c2AltgJ?i7{PZQYi3AD2oUVrwd?%(^Syib-nVbx_3PI`!N$gB>C&Y{6&BA?qefAt zMw9oOy9Z-`{rdI5cjd|zUVs9}jvagX^5sE;1}$2&h*zcjxpU_-c)@Gkx^?~f_2UzW zz%pmfoN?pEiRR_xEj^?Zrw_xBI40n<`%Ek)YQZj^z-v0Qk^()f{;Z_TefT&y-4j0o#^Q3 zl`B`CJ9lpX{{1Xkgj3#*`D!=MRu7WuheiDvDxj4ppcT*xXa#`+S^=#@0j+>mKr87A zK$nzOs;h(7e*zkYMqOWt%2LuPqJZvtK(o~y-YRe2yb%^Qhy@#>M?5_}+3NjVE=LI{ zkX?4M^^VT=w+{1g`t)gd19qi1Z{Ez7x8Vala^%R}yLV5SG6l|wJ#Q>CX3QukDA4~D z{?Hbm%VjJ5@ZrOdWa!WWaJw)v+79poFb0q~?Bzp_u$^!CKtmuwh`@j>S+bXKy}&x(FdsrSq}e8 zM;a#35*ip%SW$TvK*B+U<@VjXcMM@U+3{y!a)bk27TWTjuD-Y8FVbRwzjOdkn02=7QpmYp%8*TRJ60RGh{ZSo|89@j*haZ zg9i_?5^&0gW>6G!4kLA8ynp}xKN_ipsUlr~44$rjF6*2UngIE&9=ZSk literal 0 HcmV?d00001 diff --git a/tests/ref/link-on-block.png b/tests/ref/link-on-block.png index 8fb7f6c6690680bda5cb700dd4718ab3e0ea043d..eeeb264b9b6696d744a72483832d0a6a0615f748 100644 GIT binary patch delta 2345 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 engsoGZ2tj9SW#bQ3K2K}0000 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)) From 1bd8ff0e0fa7966f4bd2a4426241781bed168df7 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 23 Jan 2025 11:16:04 +0100 Subject: [PATCH 07/34] Methods on elements (#5733) --- crates/typst-eval/src/call.rs | 24 ++++++++++++++++++++++-- tests/suite/scripting/methods.typ | 2 +- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs index 0a9e1c486..69b274bbc 100644 --- a/crates/typst-eval/src/call.rs +++ b/crates/typst-eval/src/call.rs @@ -325,6 +325,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 +348,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 +379,7 @@ fn missing_field_call_error(target: Value, field: Ident) -> SourceDiagnostic { } _ => {} } + error } 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() From 52ee33a275063369673d8802fb820db3825a661f Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 23 Jan 2025 12:50:51 +0100 Subject: [PATCH 08/34] Rework outline (#5735) --- crates/typst-library/src/layout/repeat.rs | 2 +- crates/typst-library/src/math/equation.rs | 37 +- crates/typst-library/src/model/figure.rs | 61 +- crates/typst-library/src/model/heading.rs | 54 +- crates/typst-library/src/model/outline.rs | 817 +++++++++++------- crates/typst-library/src/model/terms.rs | 11 +- crates/typst-utils/src/lib.rs | 9 + tests/ref/heading-hanging-indent-auto.png | Bin 0 -> 849 bytes tests/ref/heading-hanging-indent-length.png | Bin 0 -> 1396 bytes tests/ref/heading-hanging-indent-zero.png | Bin 0 -> 859 bytes .../ref/issue-1041-smartquotes-in-outline.png | Bin 3467 -> 3412 bytes tests/ref/issue-2048-outline-multiline.png | Bin 0 -> 1634 bytes ...6-outline-rtl-title-ending-in-ltr-text.png | Bin 0 -> 3341 bytes ...ssue-4476-rtl-title-ending-in-ltr-text.png | Bin 6307 -> 0 bytes .../ref/issue-4859-outline-entry-show-set.png | Bin 0 -> 749 bytes tests/ref/issue-5176-cjk-title.png | Bin 1246 -> 0 bytes tests/ref/issue-5176-outline-cjk-title.png | Bin 0 -> 1218 bytes ...-5370-figure-caption-separator-outline.png | Bin 2078 -> 0 bytes tests/ref/issue-622-hide-meta-outline.png | Bin 2109 -> 2061 bytes tests/ref/issue-785-cite-locate.png | Bin 9191 -> 9441 bytes tests/ref/outline-bookmark.png | Bin 1030 -> 474 bytes tests/ref/outline-entry-complex.png | Bin 14460 -> 8461 bytes tests/ref/outline-entry-inner.png | Bin 0 -> 462 bytes tests/ref/outline-entry.png | Bin 10099 -> 5890 bytes tests/ref/outline-first-line-indent.png | Bin 10837 -> 5539 bytes tests/ref/outline-heading-start-of-page.png | Bin 0 -> 6935 bytes ...outline-indent-auto-mixed-prefix-short.png | Bin 0 -> 1045 bytes .../ref/outline-indent-auto-mixed-prefix.png | Bin 0 -> 5712 bytes tests/ref/outline-indent-auto-no-prefix.png | Bin 0 -> 3101 bytes tests/ref/outline-indent-auto.png | Bin 0 -> 5176 bytes tests/ref/outline-indent-fixed.png | Bin 0 -> 3018 bytes tests/ref/outline-indent-func.png | Bin 0 -> 2884 bytes tests/ref/outline-indent-no-numbering.png | Bin 2924 -> 0 bytes tests/ref/outline-indent-numbering.png | Bin 7101 -> 0 bytes tests/ref/outline-indent-zero.png | Bin 0 -> 3465 bytes tests/ref/outline-spacing.png | Bin 0 -> 2553 bytes tests/ref/outline-styled-text.png | Bin 1481 -> 1416 bytes tests/ref/outline.png | Bin 6743 -> 0 bytes tests/ref/query-running-header.png | Bin 9302 -> 9064 bytes tests/suite/model/figure.typ | 6 - tests/suite/model/heading.typ | 12 + tests/suite/model/outline.typ | 344 +++++--- 42 files changed, 831 insertions(+), 522 deletions(-) create mode 100644 tests/ref/heading-hanging-indent-auto.png create mode 100644 tests/ref/heading-hanging-indent-length.png create mode 100644 tests/ref/heading-hanging-indent-zero.png create mode 100644 tests/ref/issue-2048-outline-multiline.png create mode 100644 tests/ref/issue-4476-outline-rtl-title-ending-in-ltr-text.png delete mode 100644 tests/ref/issue-4476-rtl-title-ending-in-ltr-text.png create mode 100644 tests/ref/issue-4859-outline-entry-show-set.png delete mode 100644 tests/ref/issue-5176-cjk-title.png create mode 100644 tests/ref/issue-5176-outline-cjk-title.png delete mode 100644 tests/ref/issue-5370-figure-caption-separator-outline.png create mode 100644 tests/ref/outline-entry-inner.png create mode 100644 tests/ref/outline-heading-start-of-page.png create mode 100644 tests/ref/outline-indent-auto-mixed-prefix-short.png create mode 100644 tests/ref/outline-indent-auto-mixed-prefix.png create mode 100644 tests/ref/outline-indent-auto-no-prefix.png create mode 100644 tests/ref/outline-indent-auto.png create mode 100644 tests/ref/outline-indent-fixed.png create mode 100644 tests/ref/outline-indent-func.png delete mode 100644 tests/ref/outline-indent-no-numbering.png delete mode 100644 tests/ref/outline-indent-numbering.png create mode 100644 tests/ref/outline-indent-zero.png create mode 100644 tests/ref/outline-spacing.png delete mode 100644 tests/ref/outline.png 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/math/equation.rs b/crates/typst-library/src/math/equation.rs index a9173c433..1e346280a 100644 --- a/crates/typst-library/src/math/equation.rs +++ b/crates/typst-library/src/math/equation.rs @@ -229,35 +229,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/model/figure.rs b/crates/typst-library/src/model/figure.rs index 52dca966d..ce7460c9b 100644 --- a/crates/typst-library/src/model/figure.rs +++ b/crates/typst-library/src/model/figure.rs @@ -156,6 +156,7 @@ pub struct FigureElem { pub scope: PlacementScope, /// The figure's caption. + #[borrowed] pub caption: Option>, /// The kind of figure this is. @@ -305,7 +306,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()); @@ -331,7 +332,7 @@ impl Show for Packed { 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()), @@ -423,46 +424,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.clone(); - } - - Ok(Some(realized)) + fn body(&self) -> Content { + self.caption(StyleChain::default()) + .as_ref() + .map(|caption| caption.body.clone()) + .unwrap_or_default() } } diff --git a/crates/typst-library/src/model/heading.rs b/crates/typst-library/src/model/heading.rs index db131afec..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. @@ -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/outline.rs b/crates/typst-library/src/model/outline.rs index 84661c1c2..0db056e40 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,79 +257,52 @@ 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()); }; - if depth < entry.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() >= entry.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 } } @@ -269,93 +311,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), + } } } @@ -365,46 +343,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. @@ -412,90 +377,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 with 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 to `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 to `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 @@ -511,32 +592,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/terms.rs b/crates/typst-library/src/model/terms.rs index 1261ea4f4..c91eeb17a 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,7 +7,7 @@ use crate::foundations::{ Styles, TargetElem, }; use crate::html::{tag, HtmlElem}; -use crate::layout::{Dir, Em, HElem, Length, Sides, StackChild, StackElem, VElem}; +use crate::layout::{Em, HElem, Length, Sides, StackChild, StackElem, VElem}; use crate::model::{ListItemLike, ListLike, ParElem}; use crate::text::TextElem; @@ -160,12 +160,7 @@ impl Show for Packed { 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())) diff --git a/crates/typst-utils/src/lib.rs b/crates/typst-utils/src/lib.rs index d392e4093..f3fe79d2c 100644 --- a/crates/typst-utils/src/lib.rs +++ b/crates/typst-utils/src/lib.rs @@ -276,6 +276,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/tests/ref/heading-hanging-indent-auto.png b/tests/ref/heading-hanging-indent-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..823feb145eac4422dec40e50ce855ff14cb7346f GIT binary patch literal 849 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 delta 3465 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c7c359a1b990069b38118603790d635046de9fee GIT binary patch literal 3341 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ⅈL;bFt8&b1d-{@o7R?=mZ~aNZtg@E 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 JR|hc-`5%sQ_ox5> 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 0000000000000000000000000000000000000000..33ff442d95767d3c5b26e0c038b8b41f728cb06c GIT binary patch literal 749 zcmVh9?2?dRz1X=`&TEHr6qaEy(Uhlq}kkd~>dv)bF? zl$D#8n4Yt=x^#AaF*7^6y~TcigMEI2!o$aljg>1cHNC&b&(PLvZFAJs*@}ygsH(C_ zOH)@^UOGHP%+A(+e}!&ubANz@qou7~UuW9f|r^P4i^Bv0Hm|Xrhx(vC>1D8Xmx;2TmCAPqClR-67B8lpP%cYl8?y6@X#O;~=5=)5tynch(>;3)x=H}+Pxw-iG_~_{9z{1MY)6=rDvMTo0^)M zIyyQnEiGAFV?jYd;NalS&dzprcAlP|-QC}Ugp8q~p@oN!tE;PheSQ1;`>n05uCA`G zuCP#0Qha`fe}RdCfq^M0DZRbDb8~aFw6r!hHsa#qx3{-BIXUU+=~-D>jg5`!>g+#1 zLBhhqSXfw-lat%q+rq@m;o;%U&CL!D4&2<_$;r#h%gwH^xNU81d3kxv&em;jcdxOz zQdC@ohK|(K)UdL;WM*#8(Acf7x3#yws;snTXmGy3$Y*JBXlim^U}%nzn3~cpp;}yIqNJ>Se}_m&O2ELvu&}a`lbgA_!+3gvUS437mY&GU(#*`v z8X6iQBP+49yVlm$K0ZEygNtctY4P##xVX5%!NFHoSG>N)DJwIeprE3nqH1btnw+Fg zP*_S#RGy%yOG`_%w!X^B%+S!%I66X5Qd*CZnYg;b!^O?m+Tz;W!2ng-%?J_bl>gwwJ{QTwRHAC3-YwThxuP(g8md+)vXmWq4ty>Kh;y%&fI zZ2`fe;#N?ER~{|*rrSu3n- zvzj7&DBHSj_0sT%?FWUkz$tpG7XBC_EDngXIp^jHB^=!bPi+ma-_R!sSFNAB)%5Ja zhxZasE7-D`_^xpM8bhbDeJ93IqZyI$mz11@p-!4oFX1b_cR$WTFyvFG&&VZn=S-Ri z7}~;ji4KuOwa54a?$pq&JKlFtUCGvMc%tEU1o6|@u?t4(m^q`NG+7^zsCsTPnR*c% zNxMY4=-F~76tY~dvQ^i!_(WDMY*_f;-n+YVWEWck0_tBbQg@XW;1>Qb&}^FsdI8b!sjf!a^WIDF{H*kiiHOc0k;c&fnJABuT10I z_`g9E#7ZPoLZotTd5~h2*NHI=C0D%!9=%@g)h=lb4UI@~07*qo IM6N<$f|jtSP)+9{Zw7jvi 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 eO0*LF_QXH+?ZR+&DOFGa0000B|D zB#a^@3>Xa-qD(#pk+NW-7*G_2qcLa-esT(A zD|olt?e%&(osLiTRd6i0quMjht%<*_Em&=Jn z0JRXnXaERU3>osaNo6TS_@Or&An{76m zefWhX5C~8xl%KO-tyZa2DpI@IY|`oUdcA%=pZR>gR4VQF`%0x!AP}Go-eGw74W^gc*99A+|^&%nj zf>D5o9v6$nAOOm6iVlb4bUJ-_bjam$glsOCBb3Qxg5M*WcemSN{1M_b8jZzb{TviL zaSX&0#ebK}MWIljNs&234MB({*c0UY`!BoZ^O8Um#PM@zkc8UJLr_`x4=CQfDPFxq zU___>1CcIax2|666bc?hq)s7qQ7rrgS|rgb!fqi81{u|d9>}ul$-3Bk$TH5(e&4*= z+3|hH9Wpz~o=hgWRrBBL$Ye5hyPb^(&p4@?NPi^6Uc&8mQ;R7JE|-gxr0m*kHm}#q zZ3u-zB(f+4)DWM~mrA859S(=X>2!v};aDs-7z`qj2#H)OmD1@nLNppBbID?h#S)Ll z3AD%K;YWgFp-}i2`)Go~mMfRbsj6z+hY!7pa_xtJCLD=Mf+2bP^48qQS;dKM3M1P~vpkoIakzxI-_d0sL9&L?4AV515W;oK5 z;Nl2rq=SKARKlL=j?xS_M0Y!X;d2JO%7D z5S%e7aTvG9LB!w^*zBx!A{3P_nVv!J&>58I2vfV@Wt`>|tE$)Q0t9N#Jwr)fX#~=0wOFJ8;fRa| z5SjsiV2P5YLe@J!1A(ss1Ov=4o(KXsQ!tSLVQL(}4V=Lx97BndfZ#A7IL{JThydZp zShOzOw;EfUYhR!5@&%gjcFx|lsedpE?5xY6miU@UZ5(lYY z;Kw3Z!m?MF7-K-%k8;` z_n!Qk+rCeqL$r?;XoWx@QqZ;9XZ~rtyZd!-kG~dw#LE{jfsB2H0{wDh`+v;Y3*LvF zlc!Fjft3FB*+nk^u~ko12>`rEE4^sZ<(`20k~+ zn@b3-L?VI2Di({_tcZBN<$sqHmr#!w-bgHz6(cw(G?KO;PYlluGK||{x1;Xt($>}% z`pxq^tXrNfrVr?ZOQ`2$GKspQ!(1+hCZ-=-iYE`}9*M}@6GKwnZr3%8^0tPX&1Nc< z>TQ_EpN4s*G|WwV8%9P38+M4Wo^9|-%24}gfmR5#Kr2F;vrkvo*MDAY+`e;fIld|t z9yDja|7d&fKZZ4D*VYs9Cu?TWF0VXC(F?Bcjd=fIdX{5j&QXKdYX~!jZY{}?bLz&cMkYRG!-xc_>NR)0^o!Fpa--+H`9U%f+Q&T?4LCypPzd1I+o-NB{hJQ6uV(tKs! z>HLctSxkl__alSG#s9XRXxHeIp`Ko8$klUBhJ5&XGt2*7k7T#{cDrp3J>Al?tLL?l zF^ioVU%mCyTkp)-X5(AzSXAb$_R#{Z5a`1T`d!vby?XODoqzrL>o=pM5-h%8ju|ZW z6$+HGn7ptGXTv@t>GSt{^P#HdWv~ub4wXn(%XLfXSbZl&Fad8owgFTR>hcBk5 zr`h}s^Hahw!trcG))$dTWM5?iciHZo9sAVOl*sm_6!slmtyWP%rBVqebYWq^<;}Rn zkmZ99>h-$wK!3R<&L&E%P$+nwht_hQDb8jllQAck)ev275e!*uSr$S5x<$<1Wf6YB zy^COymdj<_)xL{h%l0l}mTNwr?_H$R>DVF+D00000NkvXXu0mjfZ@mP7 diff --git a/tests/ref/issue-785-cite-locate.png b/tests/ref/issue-785-cite-locate.png index 5240aa772cce985eaa73e85b8f9c9c30c856792e..d387ed0d58686504f8956e8f8df20793558fee8a 100644 GIT binary patch literal 9441 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 F@*hG5mlps4 diff --git a/tests/ref/outline-bookmark.png b/tests/ref/outline-bookmark.png index 66e5329d88516a4cb6c8c2229682b8119df0bfcf..83c74444ae86a35f8d23115e43d9e784bb73b456 100644 GIT binary patch delta 441 zcmV;q0Y?6Y2-*XX7gQSv0{{R3fZDqh0002zP)t-s|NsBp-{1ymfbf!NbeByTjk#-`{7+NB{r;u1Q2eRCwC$ z)HMOx>6h(?W+l!bE#X>9^F?9T# zFObUb0}&~gfGE(!*XVYIOzVJG9In)X1JLT@V>mcLDz`@*t|oc7-2CWXY)8oC8i~WHG~H?|YN6bGHl<5#L*;Qe#}guy$+Ba! j67%KQ?COsyit?v7W=as7=W>hw00000NkvXXu0mjfbC%;1 delta 1021 zcmV+9=kYHDh0Z?CYl$H~!hbbQUv*KKciWM*!%w7jUQvVMSw zn3|$@d4V=KKsPrx0s;aO5)$t2?tFZFGBPscP=W?EcisjIVgcz}zIm4t?lfP#v7d3m0mo|u@JWMpJ# zXJ^RB$i&3Ny1Kgb^z`uX@M&pjXK8VRg^jhhzueyDPf=NEYjbLBbgi$qtgg1k$k4sN z$15#0qNJ?m=YQ?<^Y+5S$8K+PZg6;%mY$E1ndFMz_6`mX6ciM2aB!WSoyy9}(9qDNq@;Lwcsx8j zhK7bOFE1P%92y)X*xKTooSgtk`lD@va z#>U2(nVD>CY!43)UteFEnwq7hrTY5%&(F`Yvaa&x8-r*)8Z5kljZu)%5h2kiX#`O$e18K4K^vLI7m!2;W(Ce92SO&@*#`pq z_X3NxVSnF86h%=c8o++?Q6QxkE)@qfs6_8}X_gF)==3R(Dv>(z{3C^*43cnxjGb}2 zdt(a>8Uonr+jX)v+wv;G8vrN^z{rGDB4ww=+2o$Pt%00^x3szD`&2iMf!p8M+a_BV z{eOX!NS)K=&(d)VCO_%rxLf{3cFsy8uF}oudEni9@^=B4iE}iHq9}?Im&O2IH3(13 za$S_Lhkb=56ZVb32R+qouJ=mCx=czx)&*~TRSy_fp#~Mr(NQbUH9Z-&aczDcCNN-4 z(cyn;Iq-R_{}mQpZ<>zEce9VMjb{*xgmy{XlMWuP+zK1)F2O1uuGuF^c29cqj6nE(OR+qPgQs?2H9J<00000NkvXXu0mjfeBuqD diff --git a/tests/ref/outline-entry-complex.png b/tests/ref/outline-entry-complex.png index d0491179b803f8ada1894fea31a233c02ae19af4..d2ad49e79963ab19427e29312089345e4727bbcc 100644 GIT binary patch literal 8461 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 Yc)SGlL+9VOZw>$%Nd<`-h(X~006Aa`Hvj+t literal 14460 zcmYkj18`+Q+btX?CqA)xV%xTD+cqb*t%+^h&cv8#VkeV{Inh7g``-V%w`x`IUDZ`x zyQ{jNUTgK*(aMTaNbq>@U|?WKGScFz|DG9OVBqL*5dYrvY^-g;z_`U^#6{G-H!riT ztTkjXCY(&gwMrI^Z zQvCl12|@P5)6>c6sh7KZr3!uk>iGD$<3_W^TD<`>GV(+saYu8r@9SOJV#DR-rM;sg z=Vo7VvFOl~9V!eAOaeaF)sJ*KRdifjK=F9r{I6fXwzjsWr>D7}6>>QpwmMaYo)A!< zo}ZcYJCj7NFD{mT3I+UCe{S#W?5wDe(pp|yTU%HVF6rs%DKD42xZdhAGc`RpJQSms zrN|IEveGx7xkW0TrIS}s=y2SQX;RnJ(BRf)NC*ec)HXH0w5t7wArgIPZbQkHupD`#>}QlNnt9)4A_*tk3uNBi_6H z0sN8Jx8U%@`^P_Pv=9&WMVC84_bJ)A9K(rGSu-yH#bm6Vh=Pv;9N0=_;2Hvdhs_|L%s8HDa2lmExZ zN7u*AzL=ccbSyU0IDJ@ojtD74XL)%!@~`P(PHt{#j3vr}?Ck6&=i+Q$4_droXbdW) ze14}L{-y8Er0%7BuM2BynTegAe{>}!Vb(OU7aSaNZTArFpy_F>sVy1_lOgEHpyV!?@%dn>~0D7nCRQiGW)8z&W3kwku(WUizxuld- zN#@mXCS8m83=R>ae=)q6atVb{#JG<#CF-PU??d_=y zSQH}jzO5cFCl3#fbHz)(^~J^$9$sDuwhq&dl%yo#e2`&-VPA}&v-8dS>wP%Oxw`e_ zj`jL<&}sw{zV*H5s;=(_%Wdo9@3o4Gif?}(qP1F7Op7R>etMpqic%*vMEt3)tu58s zL2EFU-w-|m%C`d3iZk0CafN;985qXoHIcbpV+9PvK@Rn27MQ_?tWS#kO z+U0svC}~?`W8shr;tD0w84%x&?hwE!)1uNV_g=cbu_3}fGAUf9SRhFFUOCZ((Kk0Y z7akr?K|#^YDE_D0<2UuZot<4c%vJoIek+fKyuAFV9IM;$@i8PMWN*%dE4@T~L3Z|3 zD>=9^eX)>`5OGkpSmv7qqk>@G(3fkTjmBfQ-xZaBpvr!C>-Vu`=N!U5-{>D0EmDV(3ddoi}OV4-0S28MV>e+%r?T!Q<*k9M@Uf0$=a1Y*LAhM^JPn!t})*9bdTtZ$1uWQ3qi8 zY6~^q)=s91T}PB?#oh=k}t8(pe=}TlDZ)yj#XXpC~4&dp}}HKiK@sT zt&gN&3!Teu3Wc)9MAV(Rs~PhhFHg&z=e{lRLRdWnZFpb(8Uq1arWK|=HAf33E5;Dp zc|+(J%N-#z#w4&sx8C+io;`*QTJ%`6Iz@;~Y6H6+#8D0a9qTPYc%kd! z3=It9VsCkNa*Je=nY2JUzph5WdxFN;Li58Mr6Q)hNfv8FE%EaCib{}7CK4x0Mg)QI z*=5!w{k9&QT7-TTb()EU#}8=`rO%)bsMX9-6#^$s*_wwHahGvOGT4rv^am^EKv>i4 zlCK@pd%$=&Vcukrl{E&W6+Ha(7wj4lbDQoTjPWhown2N&DCLvO8JU>l+%HF%MJ?KP zNxt94%s1Vqs3$36S#roGj1@@x{-3{ZPWj(X?{_-lXQlc_j;Ft`<)RP|eBDF`h7`Pw z#Ou}ZO;o!&-4uGfFYC%%1;Qj$aioUSl=n*C%!J#lJ9=n zu=wW5&G*6>Uu)PfvCUdxqAuLCExAUywXRqZdg7VsbR zK*hKO87>v(m;;=XDq8ssd(I}FlEuj{cC@^@Ht@*EBsz@GRxhunb`34UL8F{<%Hy#} zQ$*BZI#F-ZTW~cDAK{}AS-7$eXtY}RkSsFgWF?w$*xPo9@fdaV6C8w$YzZ2c75TxrLcK$iNj=>u9^0hC$&A*n9TV46u`YJ8(T> zUUxXsSWM~0SjKK?f5aD@wgkQ_?&8|=9RO9HaD6kVgz;}O^Dx!mDm|5L%y7{Pxs^^Q z#yB3W&@}c?6!Tfw`z7~<^`GARImk`uYRX#nv!5b!BzN5FHBE=Xkg$c#CQ{4O<`x%s zz*r2cZ~n@SHiJjqslphm{oRmpi10>UVQ4d?<)(A_hrl8%i3sm77b@U@Bx+UE1}>du zSY3`q7u5VXRz}8eOOUNRQfuJc$tyg$sJ_T4snWFHSX0e?MmF?2f% zj>C#2BYkTD$0+CpRfm$H33)ogW3iVph5vRGT|10f>jySe14;X1mbAF&&YPyucF@KJ zW=opgsM?>0WhE-++u|HBchgnB4{D*ILrtCcI!ZCh4wUH>!5ODOJ!_BXs_EbE!r$C+ zcpZQn%}q9*O-wIyH4Xg^{!&8iydiT5_>r$0dH3->*2=rh{N{ES<#WmEv}@xN)!Ff( zzr81{r7GgzZaO52|2nzu5!vXwBA|-bw$T7YSZdiRC@k_tVuzeMcbeoSN zhU^~aEY40WEAn}iJL_!@2jWgo^~OFf?NOFySG6x@f2TP@RJ#vF5$p`|O8#m$Zf`zv zfV4Nr3Hyh)26}m>FvGz4T~7Qajh3J7+ij<hfZMxpA>fay$cL77j{}xsZG1qsTp?6lhqAwep`|Fnd>+PF=UJRmm z>G;dOC1w`DbyVoA*KK~24h$crc=-MTm#1pUYSG?1^$7+lD}-YbCA4e_^8=o%5>RAJ z^F>qJMSZ6g(84$9<)eBpSL0d!$dS?@i(?GbKg;<6&N$hU{=10T7ezG|LHlGAN4jnP^y=X3;mp$PoHoVYw)4ZSi=GAqcTP8H~`lL!{JZG7KCX%v+Q^ zSW3&#hr*(uqK3!iFrG^k#~EKsgCjCk=GgD5>vYqu0h;r5vP3^Or}$)g4-5^09Ha^s zG)I?u1gH9=9UA=xWAzr}D78AZls`#ZJEvhL!e@9(&Jd13l4FV!%s37Z1%(5-3sxR1 zm<84brsXa0swAxGKLwy$;;HNe1!p6hV{Z0Zc5=u7l4NXy`*KD-|ywB9s}W zY3$*o?kp%sg>M>-SXZ^I>07djQK~UNwQ#(ehv>m)1e~Vgn1RuiqK3&fVJPZ3d@46gsx`%=Oo#^bSE7o%wnZ!PVs(1S(UtMm)Qe@l^B#Bi5!wPlc8nv=8jRw z@9B&5!42Id-g()}p_Mhzn97T>@+20ad)}^0!D}CDxP&*vll2>qCRY-Ri7u!3090`H z+>`pDjX)ujGW@V~4zUVAy>~^=l&YCBe@I72dW)j@?|r>kI+9;WT~n9BI9OgWEsM<+ z2Pnj_fvruWtgi>8)_y;&9v^KP2AAZS&P(O#?_rj$rIZ%tJ5InMv(Lw@Ep)SB=|NBp zpVqP*Ff{FKqc`Yqs-tSzh~9g_LcCAw&vn@RZ=F8EID3{y6_u!mL{{d3UvE~>JMFeY znWmu(@3d=|-JLCq*~fIfKx!YvJSgG+>_vrPMulOJ{@0bB)5k97*N*?a9#j8I^T3*+ z-}VIb7X_uOqJ;gv9Mi|?G!yabXZZF5om7H919l6sKK0C>L`e&hIqt)_{fw*$$_Xs| zThc50L`SbV9rv(s*$qWZNH@W6 zjGgwuU9U5@o$q_FftC%GPgAnYu2JRo!9vP*Q8aAU3(LS1QO?>+EgrHFZKu@0l4t~n zr23IDMl4_2t)LFtg{*FxB9v1ST_z77lp?kiyuAS+dszvuz}-HS$2bYI#IR)I2y5YY zd5KRmN)%c+l-&><<)8Z*?8n|#kfXOO{%C;=>5!648=$avgktU^nV1s6u^^$& zagD6!mlvDmSc-@)Tv&M-%S;awM>ymxq`cssdb_gSq_)*zH*ZZa8Ex8`Vlz5UOU>amd#n>5pZD$qM zY~i_yt^|2g*mvHWS|ZnEiu(^$H7k^%Evbaw6nFVm+dKFM?WPn(6|T5s#S*fc9AUPP z!-(9;$gFVip?=z|Uu3oc;a9)VhrSohw6(v&4+X7glj37JvjqdrEe{sB@rRk|r$ekX zELo)F41Y2D&)^RobZQpteq4DS|8%1caS}`n6n4<#1etTRz>|~)^E&TU;Ga1D?3YVz z{4Zp%|n%nP3Sc6>g;QINcR_P@@4;TpW;nk}Vtd_Mp3 zyl~gB$O7TO+pf*2IsH{F02ZJVO$8aP|lk`+{NycFU4|yogx81QyI!fZX?rbidvQ z18aoIh}A@F6|Grwk_erV9v{QpyCH^W>8fNXl9e$=Fnj$|;}lg&77~rMlcvjJ1E0*RF7a#xuP1r115X`#3y+CAAXM)ai1@t` z_&d)?mpGv9EdxGek_-c2<9isbpb^=`v?DBP_c}(!a{E2n!X!S7@k1h z;f$!co!MVKL^xHuAe~y>J~|GAm&8PorJb@6+njJ}$|@K(HC%Xil(K*tp}#P3WB%RM z^Zp%uX)NYPDnj%n`byS9-G^l{v47ZA!;_TdnSLQG3w*$Sn@2eDR6R>I@{NHW6h|jd z(O+8FmzJLGwuzn81dCS8@oa~F6XVl3kXX-yj;n2nc-?>^bjB@>bro!;U5_@#dyWm{ zXNy)Q%b6tOf8C^PoW*FOwTdY$`0eWQ4CgbbKKg~1uUMHf9!Ki`q67Q5N@<#y$8x_h z;Jf}#ZW)&)6LGG93^8?aQxuBLHS>{R3dG#psQy61hRxBQstx&1$^TdN8G9r}FCx)1 z?F+qnEfT1daq4mKzl~aIRG#-!?C)pxU!g#}^%T5>4t(@T-Mjqtx)~w%c|iEOFx?(# zAo^tbBJA+KqpA3I9Ny5iv!{mN;4{}Hlv(e1bG@_s^^iW`|2A(C}E5B@@@Er80qI8M)fl=z@HCQfSm)i-|yk)0o?rw^)= zL0s^1(y-j%p%`Z&!R&p|`b5Htg3y5pzENSmL}F54E>yQRJOD+F(5P-)0$Y}FPvRgO zSn102!ja{7O>lAHO!r!5s1Ab>B&eyLMKm;+MywfRLvZyFd#FY^c4JhEfw@BG+K6tS zB<%$VQQ3MVp=mlWtUOp;t{F(2!!c~5ILjmw;zhs(VNAI+c!|O?BT0s1F3aEYV{>RM zE3-bRtE|KF1T1YWZxlbL<9${_1(8W`!6OCoKmE*B1qCv6e6LSLB}R^G~PS{#Cd z33)so7Oimp>AM(Wcqf)W2UtCjps>8`AlD0=F!RMgkh-i2xj=VRZqGP46AV_lJ-5*lX7TaErHR6B zm1S`Lc>aIpXW9sqooMkRo6u2FB+-(Cc(^gr?(j2q?#ax)xhDyj`~lirIlVstcjkfD z`2`%!&yi~5?iUa8z@?h%BoXDJIEB2#R3L~7&k^BdoOi~14hd)rrOat$eE&$P8U>n( z%G$)Df(+J~H9Z_|wJX%|GQjzrwaAbi0aG&SurtrbOEj7zo;@C47%C(=)r(l4Z;V}c z4wzfLJ^CN@+yao)y_Urf=BRy!mbD#LvDCiY7JQNyjsaoZ~if zqHR2R=Q1K?;zWz$G%S_&iO}PnRF)pb-{ASi8RnvxVlq4wN~SekS@b2Lql$?*9Zis;GVsW{zd_5_`k<#XJ?sX z6rWmNos3FlRRCjocr&=GgER~@SYEnc^ z@>>IQ)V53SZ>dmd1vH#Q5?Zf$C*nes&daUGE11F$aH#3RcW}qDKRYsZy*4-0;p(MY z3BEO(cWl^&N8m{<^Q54nVj`w48E{nVkCub2eF5qkRWP%zUiL5`k73I~A!)4W<38Z@O?3Q`_2=k_RTgE*14 zvF4Lt_93gm%|e~%n}x&Q{ka$YWujpG^A4AITZ0 z_i|-8BEcWUH88PBI%BrLjNy#b4Z=OEaD<75Znf;(56QwumTE#6z8VDUk9>v4iM>w& zLi09F@5pm7 zyKX;p7cqyP&23}itk~pCPd5#9VlBrM(A8~?My}(Ul9>CyAnZjFWF&|CBd4DXl;<|In-)g=A~8 zjSHpRGA_;IDw;S8p+#!8`-xao@8<*mg7$^hv;{2-1N_P7Bqr>sgs2@U`>K3v?-0?f zzP9O}y*A9Mi4{u^0>m6WIyY%A>49P!oB)N*1qX#{$k78P_X;EXDT!@fZOf@VrPZM$ zH@h}gvA-RRw;w&CWe~QFEEE>-yBM7Rm_Zhz@gWG)^|RQjCj251NX7OZgYVdB;;!vt zux48q=O`K{<;C2`il&wHQIZd{-+4>V@++*%s^?(fv&m?Hrb?pLd5TLEF$HwOH@U8G z!YhV~xMQs2Dh(oL;E_a~*O@n7$fwZG3PS};ainRJ1(6YTL|kqWd35`q6x}wXe$lW!Z-|1* zMs`j>5BvWhH|(uTInZ8~>k&?XP_juE#po#(`~8yQ>qhD8;85{n+xN}s+l`peK$VnZ|6|7k2zf10==S0AN8sCP zZx_-UOm7#qHfXi1HK#Y7j+7Z*+~EjO(WR7+jL_W;**Rzf6tS+(5|GMi>XeMyY*Fg{aED=ljkQa16%&}mDU8Q~ zECy+f$RZ4z7PIt%xn>J*z=J~u2wEW!72T;AA*Wu8VXP z8ZSH$L1dW1F3Lnsi{%+Z2QuwU*PMHRK`01wIJyD-QAI%68m(ot*5%mf99WCD-nDw@ zIK=l9GQ!^^M9S=e2l5gNg5v3Dkk2DRJdEUVRY<1|*7U30dY7Dr9T z!7%qNlrZoq$Ye&I_|68pa7hf!U{o54P0kGYirK&-6D&0KILF|wXH-B_NcP-VMlu{9 z8b^8}rEm+6W7b7EfGD|Go!k+EK@D6?x&k2)9(FW6>KppjvX8D;c{E=$t0cS~Z!52lz%J?(Am*aRsuQw1pqCqcWo|z8NRNMj@ z=@7ET>bymfqs?y!xUx>Ox4JwqoFd!&>v#`^v7CC~kBwf&T(90;y>=gR|8uaS*D# z8^aJ2kUKLMBOTi3{+JMCz<$?>v7dQ8nh~Di_2}eij%FC^f1S(|FK)i4fKUj6`jzDF zcmf-$Zb+&snkz4HBul`ZQAodYl#7l$$A2&1lo3YD1+72)5{-|VA_XXblxOyhaZwZ{ z84`DqYiidaA1h%7rb120pWx8`xlxs&6o38xGI^vYp`ShlYygSy%*4zj@z9Q|ewQIp zly89IWdV!MwneM=`GnJnDL^xr9VvXrTwicus0J~6`$|WOC}EJKR_$xX<4m6O5^Q@j zZZsz40gBnRB=-$*YTikgboupLaYF@z=l>omrWDt~1AfP4Ocu|asWe^w{1)Nr2zwhT zET>3uxhV!gE?;{?qf?Le>vW)vRuDd)B3oyNp<`zbAtIZ|t;Z%gc6M8lmdS#cX&_Qx zy|a>2ZDK`40%CQQz9{LfsJ2Jf6(JQ8_#4iht-NE$x5kCwr;8EtbRJab_|=U#S^rCj zu3zWLaS)1_QyaReH4DDroEz#8?J}9Knqt_uyZOPW^>8NAU--HyQz2cqsH@^2Ltq|T ztL0MXlPybM_s~ad<1EmXfG;dom!yl?rE;+_T$!UE$oZ=d+p#z-40OTxsY^#UPghj_ zB*L8{?ttc4jKcu}J$o)jIPQ;ruRpFjp_7>51aRrvBLjqY{9U=P4;qerBEmRF4tnd# zks3X0&^-S^#}Q2x$_B_^%k_xCl!lCYW!T z7|y$0EcMNz5d|m26QrY>Q%rSh<1l1UO@*`^$<* z#@_~4FZ{56iRedy!X69BR**`1#Pqm=!DLLQtXVgQY<)^BAAjJJx=Bm7z8E0lEx;6} zWW$TW@|!G&HB~_x7tfhKmr)fNb<&_&Cg4j+XNAK8AZ^fk?d9nKrk>(<@UOUn6a?7g zC!#ARDWZNi1T>);K_kK8rzV~GWZAzkhPYtbk2-t=`o5u+KnrK045)!tEj#NTU# z_Sh{Ell*X2&{p`$F2jY3i%oP>Le!2(d9lHxbwkoRSc{=5*hFbJ;+AqhBj#-zYx8~6 z2bKQ(yY2NQZGjUiDwlJrV#ck*jbK&rUGmTzmKcuF7^;VQCJ`2Z=pS}4{j|p#{Gt6X z_EZUtL6)x)VZ;WBOW!`?8v`DUq?91qE1JV{zv&z@L@S7;1<;LjJ5em&!xc!e&LwH% zJoH;a18m6Llu4N;TZUw0DjgA*tEWy)sSmah$zxP==m~A#I-qeMs60vk%-B&SRSN={ z#}vfthROHFEsQACe5Z))t^NyAQcQFEo}*2A0veg9jGby`Wx3c~LF{<*l#wZ{N2ZjF zZlpv==1^*xvdc8(>@2}woz^F5*|5|LQ>SgT?XqEgO=PA9-Jml_9H$;t=2nE|oQP`G zh@`3}(ZE8YX%Oo&Z8*q4_=7=o`2cVnDsq4$5H>9l2W0txjcj8Aoh zg4Pj27lJqcyC*gj&=duHr%k-t=NSjX@jK(o>b{=7a~7x1GeUp5FnSD)&E9ogfJ&Kp z1lX8mff2tc>yR;Hz3;XLRVqsF=)o-B`dYUuOr088+oXO_Vz(~gwoIVtU^14TCfr!Z zkR;iw$CC*u7$Y5r#A2Bu_dykGp4+5GC<0O=BG+<+;e0`5%U$&EY=hA6ov&r!LUPb) zumB~*^TwcBgzXe@m9^?WNt`et=!E(U7Eo}B-$sZjK7K`3z z6%1JP$4Y3`i4xnBKE?-GwHu8u0fcG}E#az80#dCp%&aWaaW?r&ZvTNT(vU3^r64nk zV#Mz2gfnPJ*@T~_Be$9CZp1@yEL4TYAz6VKWV)`-YC#c_xT9&ywIfE4~3;zj$p!EX* z+}(2!rPN$HE$$|T(gS5_IQIz zbs|K%wqNsxchDG%^-cFBs_)i-ev&++PKC7;Q=z4ZRfT; zAp(^zGcXMpv+gdG?&g+7x6^XVh7a#>pwfcHGoLX0Di?Q(`P10Rs{20*=szL|fo7)h ziBI65M@-ajag$iH=+~^LLJ6d^lS{I&iVDq4JKwqo{@VF>%Kc7dG;6;Vt0u#d+(D(J ztd=8TQ#%1Ut&+H90@{bQcPQl!&%}Ccb#fLBpAcD+FFZyi&unwbM-4*+N(3ORu z5l1jc-HB0#98DFS$Jv@Wif%AYX=MRPlpbvq%yy(olwF<@{0uB&q6TsS-Zp4nL?PI% zjL@|rDK4!X<<`Y0SD*dHVgY8Of&&^wmjP`q;VwKHOoz<4BAh0)2mTr}TTcpfggs&e z(>kjGm^>g5*`pFilGRGQcL0KrP`#k2bVX4HT`Qwr^_@#X(5K3@AJNt6yEnsMMXdFF zMVh-xTppv5f(V$F$E9}}&!{fHZ-}%Hl~DGP*sEY?+3X23j5sZ#%)m5q7DDh8?Htve z&B<0LTkBcVa<81ZPY$YQhcvE2&uq(X1!eoRg-u*6h}%X zRp<_8db0y}@u#xFI?m)hvXbLCdE5k74;Ng_499JkTfNQoqhAY9`YOE_{gMe!$ z5l8u8C@bJY*OBMrX{W=$;~NW~aqez+?eQKrQ4sj|JnxcT?3Cz^95aDVq$reUlq-D>uM7y##FAt|50?H4&i?57bI?^DkbOKQZUJr65 zKoBM-pBI(*byAoruX6Ck`(RX5n{CWEC71;Prb#ZJssh}ohZ@$*|0IdT9k1jf_QTwO z&&>#@B)IolM~%V<@1>35Qc&Tiz{S7FI#~9#3dF~A?kjNP>8_#n&Qu~tDJG~9Au*3? zr33na-dhjhmJ_T<{}15FeD16W$8td-&HRs*Brz8ZRfC2}?V=;WoRT}YN@XU@X(l+G z1=kODwBP-2S8lW4w$kpFTMG+@f4Gq$C)eOZKpjE21TG#JJ3>}Bu+f5Q*aF97Sn)u~ ziG|))FUA%+jIN#hs@bRq?F@d5X#R9z%Zc)Qf$b&UP~qrO$P84vbq!SC-BX19Mx6ya z7BMaye&9QrvCM?az)e#S2#Nb(UHwbXv%IbAwwZ5opJbVZO~Qe%-6gq7j1fChp-nKuVz+l}b&xKy~`s0fdzkH7tp|mL$MD|05m_8bT{uwH# zD|Gz5En(`0$;#xzVWUEeh$-k?Cn^Boj zMV_0ZrpP0KhZvh(iXn3qd$ZbDOL12&2QrndHddZNd8U9!rN`6LHlruPG8j;k&EY50 zS)ZaRu>YZ#UgvP4%(~(|UD>hO#I2JZ$J$hNDxT*#GoaphNX5*|KJSQDOoUicQw7-_ zKTtN8+#-*KL^Ir{g@uu48_)i5p$&c1KCBd&llN2Sq|U*&e)4$ub)D%)5twVs3y-EF z>n@I^&f}uyHz1`Ez6u1Xgi}i2bWh~#dhLFUQN9yW@sCepLH{hATg>{$MGnzwGh4k2 zII-p4H1O$AqqDW@hH{fTqg&$@1v4uG7TmJ2S$_@z9Cg0*r|h?OT;*<55n7-4?bK)v z*~KiF9BD@uc=UM&~=7fD7rNy?_usmtK#0rJP7nQCmstmw%a5VLHR+uF9 z_Sp<&OmM2FH8x*T0Mc!4?@^`o6FYz__$|+Ej%RhClzy{L^hp zQrXm$+6CPCp2A>q`lL5_Kx1?Y=4@f%x-gi|3 z>kg^;+gCSfe=r!5d+L!qkw1h;hEi@GI+22pVObo5 z6nL{pRafY=MoCu>40NM}u!*yOzsGQ~`gT`&9Mv_S`m(}Bm**ZgIIp{yt+iLFfFV3u zzunW?4f7YNWRfYh-*)EjHR_p0!UKbGr9u24$eM6YL}Wbh(BuqM^ZSvR$-?QNIbl)_ zExBhlPxZo*^=H&Q#k3sxh-NM48=_4+F}_kqUGw4 zQ7PF=9+7xCm7&^;ITF5Rqwlor9b~yJ{VmNlGI~UG8XhKmmnvDIG|50XX38;V+WAla z#%!hKP>WTt0npUD%+;K&3i54{K3B5ciQ}UK;A#^6V-+Lb49W_M6)MJ&eh2ZOILUi~L^hPYAIeB1=MSAd5|pctmyEcmEF4?i(UWd2;kRNWXu$dz$D7)deD`(&_&k^PGSV zjY}0B4jPu)kQcmGRkMpDLJ@j*RyD94{pBN63x)04eBw0Bk~FN22o7EaFY+P6 z{9aUNjH6TZV;o<>1K){^Cg^D9U};E z$k}Wx&q?3Po)aXCgJT#S4(+x9ovNZx|j2d?)+4c zH$8rw@KP=YM&?HYP9pw-l z=e?~p!D7tM8IEL;2TEzVuA5v;}tC zbsMZ_k)S+(`hBfe*?b&TImWz;|Ay?CDptbZGb!UFg8vPB{Ij+9SurR-DfiK^Wl0;$ zWjbr2S0TY5IM@N1FC?;z%vSk6lvF4G)7@#{wvU#HBcRRypRDeGiRb?_P=M|yF{)05 WT8wU--@m^K!DJ*9#p^|lL;gRfvkSEV diff --git a/tests/ref/outline-entry-inner.png b/tests/ref/outline-entry-inner.png new file mode 100644 index 0000000000000000000000000000000000000000..5376c9961453ad7c1f3acf1fb40e93a606fae0a8 GIT binary patch literal 462 zcmV;<0WtoGP)vq&_AXr!W1 zt5&!$0+d=mA#Afy!76i$X#A!_>QY)O6~GW<`%YaJ`|#}K%9qIwvONVGxRT+L2L zqRHNxfxwOV9KsE2!#x%NT~*a=mgsn*VVu)Kngn3bm`zm6BsU1sq(xLaM(%M*GX-*v zFnxwbb2X1d6EL_5*e)&~C0?lXsYG0*XL1HFjpm8FY)~B@YG%kDuIM#(X~rZT=+yXr zu2#5G4-9QX+y?fmhc~*H)(;?Vde5{x5;82qGW@5*U*6A7eiyuS@c;k-07*qoM6N<$ EfkPy|dRtBHO-RJ4Ol$onC8g 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 Kt{P|=_WuAvVO=`_ literal 10099 zcmY*s8e~FG?9IgN{m!3IhX!E+;Fg`hHA%-)E8G-tFm~zYPowoq(LAxVqQ! zX_lpxh8*rtKD&uyBuI*#6<-~fgt1y&Duh(9h`uNQt1j?qKz4+-L=z;7jDk`k2ATm@ zlq5(aqf}&v-5AJT2%pE3wuU|^cyH7@PDG5hS?-VhbbNK481sKCequ1sjQIo&3RRB& z4)~Zl^z}*llh{eZiflk+1Z2fABc#K zPEKYQf0mX?^)+)LCnqI+uGV{e{A)Rol$nWAlqJk2AmGyqa3WRwM^Q3j;=YILy;1j*dHpt*fx*FWydLED8yAz4ljBJR0inCQ3pIwOM1+K3 zF0SXxt^Vm^nT;-gAlTa4`rrM<(7^2M-@CIF`lqKSeSLkmfWQ0bT|PcO)PElyyaYB{ zokN_xu-M2c)Y;C@&lR>qkTB<(@d*f$O%)XsreO@2AULtGuxPA|4DxVtO5heHZ@9U;FY(pcEjOY7x8S1b-+wsu zM^plcj5FQG!GVFZ^`HM%S{$h*YL_~_IX$)}@`i4Eg5dRjr$w`|jcd3qcKQE%|Jus$ z-Dql$C*N{W=CTP%pW_&*e$FLF}akVL;ipNc9+f;mZh`?F%1`2EJ~{M&a*Jxmhw zguG>aac5&@7V>)le=)Aar}*-wW@&b_kPiq)-cZlUjqYs@BU8_dRcbcA0uiF#-W3j55#MSFJX-( zCJaoCUV?ykC3#d_T~rN!y?{=TNXoLq}2f`iYC=@0|gs0Si69YHChtGiNAKz^wfzHO;o3L8h$DsfzP zdUp0sYHcDkWHsbyA|fI-gU-N+qWJf%t&Lxj_M(Rce*K-j%*?SmSeTgcdx%?4w+d4~0XX%wtN!qsc>%2IYx9 znW@vI-ma1zd? zd8M;c2>IpLFG&Cr9#~@W?5Jn>*J>JzE+K+vN-~`i<3$ME?lyj5&+Y5Wzg=Th#NR|4 z9iA7U8uh+J-$(uuhwobaBn_mO{cU?lGm=QnFan&kL9c8b7WK(s+_0^d%{?#lDMaag zxhZ<6GX-iHEsu?vO4Ss-QK)*d30#cF7JQ@vhWzY9n6#;zb>DFg*SPQjhSSB{{rc>U zChmaqhj5b?WI%|O%tOm5%`-wasteXE*GqsXt4r9^9Q`K9j8!4jA}<^hM70J)fa^|s zv9@``Gl(5@w8XP#|8O86Qn5H;N|{M>sK4x;>cy92)pWtRWIjUBURT#9VccCZMV@Xr z&RXT>>p`7Gy=c|k`L$!f#fn08Rgmzjfpa#mJ)kg+oC>SZmpQL`zB+1wq-8Qh&e-4A zLBwobd(P#X9_O*lF++dZOk3||>EEfCLDehC!DBb;l;$C0~^m=>1XzH-7Scv+Vj}#{GPY`4c4e`j^J#r(lMHn>Y=s8RR zDdcb5W{|!p`98E;`;~BnRxtcvj~#Vu>xX9{vfaB9I77x`Oh?4dCiARo8@8G|6S%?* zF?w1o)X@L;Yzxy>Uvu8y!|-e<+4KA*VB_HLOVuCuwR(}2j(g?NN=A%L-tXeQMYQ8H zU^Uci+7gKE95oW@AlWNb6jsL37@u4rJ_DC9HN{j=(qNWfplZQSN%_j4CkYl12O86r zij4(-SACyqUy;aV(|FAW|NR92>X6rje)j0z{o=v>qHRsn{cFI2PWQ?c3X^6vF0(C`)$`xUeC^#}BM2`X*@pkKGiS0o0_WSmif++a2xnMZ_p`b0xXK%% zE7IQj^{io_`4vpLPTUL*b()wLEyQ7GFpdu?6Bzn?^2Lv_m^2i`>LFnS5z4j} zk?DlGrhSJEp0E#4dc!6t?{D#1S|9hb*#YM&^6_-hU_9rO{TN$b_9}3HgApb5u;ay@EX^D{-GuQ^YoLPsV*+- z{s8MIMCXtT;rmrrHPKb(*Vyg$`q!W`I_OtbJ^us|7kzS{T4zl||Jwlq!!eW)nE^aa zJ?CMH2tA*p(q9cew>D71x~=yFP3Oy%@b5av)%o)>mG|{;Jc=s5%|Ljroj&C*dM>nY zwDM2Uy?x1JOUY1ys9AVOsth7(I%L)RAa~;0O#B!$ld5U3^t=ugdE8Zl@|$1$y#M+% zqX|hZ7(VYs`e+vtABM_-=DfVr-9T$m%U@S`fxXAhj2-pVSHB{=ngg!iiWD&`COVt5){h8O#7ffLYF=?@upJ(& zerv`XYN#wj4Z!oQ&S#feWjrfdJ1cr<@I9$&;hz|D9UjAU`nY zqw#XXQ3(6_Je!cVs zKh)&_PzZQgeZ1dPc6qpn@Lzv4Oqe*1BgW2-x%;LoX7jWqn)IKCqmyLVCLfPy^RDcb z;cdT;aZWa#puD6-P~9VUD8Rv=t9WaftM2>9by?>7H|ra?8D!oKHAQ?(x8&rqVprcLp3Y=;K&s2`HZ|L$H zc5VJ$bc@an`b4fY^|koqP0O(bOat$iZ6`CW8LU^D1&POm1C}0#lQj+hDpr8DXoXhb z6D*8v*DMkb)Gz1)XC+$}_t)(7t**=}FtG6s=BrSEh=09fc;~Mn`tPKL^`sO;S9e` zvhwM?6sCyv=SawJ+?GG(lTo#iYgD611agkKJ`jiz1OWccPJ6XoQ8Tfnrdl)U@BQZjU0Zl!b65Nu03} zn(E==^WwmV>ze_<<1!AV-yl*>4z>e0@uS- zx!h>2^&tnjo_U*g4;(>TshfN#AT_#j-jn5AkSWg%JV{a$2`#%J0NR3O8m<5)^7tg1 ze^mDy&TFz5QjL>*DyjoOy;$nZI^a?bo68++Z87TxlVubl87V0@HH9rwP-TPMY>S4^ocoxOOj z>PvU48yFv!x)iVXe#oo}_@pB}Zf&r9QpoqBv#eB$%mQX~RED*q>0^9H$H#p#Pf`=r zJT!^Nnc3$U{&t#(#};lmUT+d5eAWt=_*fV07)l`CD|~~7*KY&gC-rgMzSV^<_kf4A zwEAZotAwbcKfIx?Qzv=pDXUD#r~&PtO@)FVA^{9%_$Q;xwbBe}o_?ER{Lb>bL}lr+ zNc}6Z_lbBb^aQXw#`3L>=uZj8>R$J!_uo@}PwPw=jL&_)_~v(-I?;R0Q}(Uqpz~kA znI$7CMtv-5sznFhi3g@!kaa7MYT+Sg|J4@Zgx?7Alcumr-NCnvj=2F{X|up0$kH=l z-{P6<2(CC_&k2g`W4X%ceXB~hJ~NsSFf?*Cj88e~sAwTqXB+-D-haxPe=dcz=%)1M zjlBKr63Tkarz5`Oa(wnD!XJj`N?Ch&2vbXUbV#CFNFHUe;=_GD8T_Aez9Sy;U_%a; z^whW#c#PKICw(mp0~|tp36v?=TW1a-??VvR>DfY&b=qap6+Hg5 zJlPh~M!Rp32vZF#V-`gihbEP)EI-kFUy**8fRl+EbjLy-zDF?ke`*E`tsOp+=M99l zako5@?Ph6m5WFXV<;hK0s=-|q<`*3SWe?_CYs@SWk?A6~5A1M{D9u3(&>0T&5fF&X zoW{;0wU^6`M^6`3>Ub2)XvYzVTb-+0Qb1V9qfm4(9r2uy&;FpL3o|mVvo$)->}$vQ z13y8^pGX(8SVU3~pgk!cJd0~bV?TkQHF?R<O#_4Eg!Y@om%ezgS^IY;K+2m{$9 z13Ky9(xP?x*mFy+0AJiWQ~8HlgQlF@Zht|&{L$V!QI<`qIcuaBkLH-0JQl;J%2NMA zPK{5Y|HXeJ@jn)1oC?}{BVexM;`;L>e{&^x#7ExEM_#gQ_b0I1NKRpW%xVuw&;c&- z<4g6et7eGD(`&TTHwUtkN327f_sdC8u9Mts4Som*i0cUO zVGo0VV)YNW}iWilH?pdM;22C;2Q5)J%gZ?<+eK{|UBcxMK97<_u zi!3bK(j{C|G7U$s>&f`*g0_FvhK_tMAxdvMj*Q(d z`RUz%%RTwJHY)FzEHBc9AO3CRy57l*#jfA4-*Ucr-(3xn1#DmCW>50HH0Qs?8m(X5 zwUB!M+WHo73GeKse>=GF(aHUX_SIaP#m8*Jv zxnDDtetK3cmMLTi+6`&y%vN^`SHzjOJy%Pj53G{ZD*X@4fA0Vhm4&YlcOVL5g}xcz zj-uoq3zRzc(h+ma6WTi5%`Sf68NiM0(_5^(T9o~?TCogV!xXiPGNEbE>b0I<>rvRE zvWjNyvA+WHG|Zj-Q6eU02jNdkE(#(hVqd8%%kwV6cfufO{mMW+QQJg(7|d#vr;Ut) zh0{XD9A^Sl0s0S4pvsuFjEit1rtY$WSD&0vKL&extTfw$`C*vV2gH?S;qcSt>gM5y zZ3m@rjrp+Eaota)oF_a?y_03Jxf8|k#VLx0kwG8|2sSM%A`Tyyp&i!mDon^Ah2MnE zE6|$hOwR~A2}xN+S1n!TxZ$)%x`%cddG*tb6YcQ9yKK|p{sGe^EzU?KWnC79lR1+m z19?1*d>W;=NUX>9(=07*f&&R-V=G6aVFv9r7(Q4|0FI<25&$(SfK8R+Mj27stSjVB zvW}@vc93s7^yoQ3h--fP%EzjQEVzH=k`M)rC5Ad5cPdBvQ@#THKl=x=fep$q}_`1AM z)3!B8%5!~R7oYDrJ!=X4hjsb!Cr=TLA4s^Vn(@&I_nBXp6_K#yIU z_i2t3X z91&(_`eB39bZyXu|KvvIW4?$inbFh|MIj7<)>u&aAVB* zhJGh7@!G0&ig(XH%%X#P(2|Fk3T z9f>L>Gxo%ju)#x9pOg>mi@bSPidMX4;F>eA#>UAb>aq>);Xv+O@U~#35KpZC5b+n; zu$NU>C>dPLkPnBU-odF*JF@p^3#N-a51I)pwut8CDZTI<>1Xq10VGnlgy)pA9s1we z3nDMUt`ulNY`>9ZW{%xN0m-M{RV5f4{FLf#{yMaw>Xm6`K5gkE*Npr46yIEIz!IcT zDpDqrWCM?Jf+GEQF zV{KHS-KAtkaoVjqVMsppNkwFnUZ zjS|yKVVV+Zf)ccO%+7gaj>~WB3NRo-z z@9`-y{$SfybhR~+AzZhR4g1z;>oQgslyBN@e~1*ktz5$@@8u2n8JZjsr&0O&YM?&o zSl&g2{kzTew2LyG1QE0YXZs zscSKqp}JEnEmo78_!!sl@@2-j{C}v2SC>gwaG>23dqX|Kzei(YcBRQ zoL|Ba){-+R-s!P$C|d;i{J2TDk~F`-fNCFBT(6sv!m+8R9_tPDwphy3NXA&!Hf8yH zQo0&hBh7MFnS%Td`TtW8Y0xT#u5MOQGB2k+bfqN~IWbG8CnB?~^$g+r%`ZzZ$pYOr zgOGF(qbZZQWvX*k9L9vn_CcOcrOatgr@%orlb>=o2|Y`>HjPzKiytAUNcD4+f+&lBQa!ejF>Lj9u;TpkoRz7~_u zWwZm(5Z?A+W=W7)vZsY;1Iy;_zlcg4*s(j|6$8dm?HCDb^0kY<#{_aL6($9bpdw1Z?ql=W{mg{ zT>KQ!T6vb3?*n&r4|a;$EQI^01%^cixu2&ewn_q?A415=D)K%VdMRu&?e{84-uJJ7 zEU5<9vbSJfp}_P7H$XMZBMD8mL@1gyA>|5k z!Bp+9W{dHxTF3{6KH+HqIx%)IJ&jzfB;LpYG7ANs)i)}HqhO1VnXuE2n8};$)o>}Q z<>1*|)*@O!|6LSaUU~6i;FN$YH*5)yWEP8{#P?d?P3u`A`sR5@@Y~e3?pGbuh=QGl zpL`LaY5b{HpRT2^xULT``!N^hPg(xR(aO)1W*rLmcy`#CJgT6xPJCXW75%*6V3nY+ zljYZtz~CU07DK^(u^>IeZZfk+K><^x;OOHjq{v`M->j4bfTs&{0_R~7!Y_yG_SCnh ze3-VQTH`QT;(qzPkbp7&5dQlXp2wPARXCmvK~`aR2MbB!#;nO zt>z!F;uEn?2`7uVIz!EBQ#51`pkgZ_29*6ki0+Nzw;BmAi(crY9(ngV+U*UeFaP^oU}Y^_aydk zg&8Bd!R60Dnf$fu0*kH<;k1LPjp#nKoXs^Y7{nCUg3|NHQ1y-C zc6fy3rJ7Vj+ge2qop?Btkc5$(^ z>%{eY3i%gX4NJp#8{eX^=jfE3nC?Sk(PzY>?qSJ#JVRga|U-2LO$%Gs2uab&AwtLo+9)6}BiQ!sun z(SXs7svW5Z8Bet$vdyP|ehG;p=@Br}g5uHiQ6Of~kcQzw*f@HV#B41FH<(~Z<>m6A zGsAQ{15=F>pRr8`c$~nyw`O&dYmKGZb$7MKO*Y(eJ;i!-BBjn2ADGEf)5mWjl`&{u z^L3&ZG;!cHySzflT*ugj5`9F22CWsz8RNOIDyjT36YwNx?DP1a8lDIP9hq9zypnY! zrPCZf5y-S*c+Dq=L|e%B^nG#gkt`P-^v7>aMGuSD`Jjs&7>x0X3I_byEw;!`bF!%~ z`Zg{GCA+4!37Kw~LBe=Q5IzN5z+z}r@u%(W%pTY1ccdxdhE9IU$A`lq4)2-FNXH#& zic~hos%(;>6t+V~f?2q=Xb`c1T}ghU1}t@W=^*tsf9^%AO>nnt+)RS$&m^& zm})AlWqk9VqW=cz5E2y!I}42w43q*<=?3Dt*0*P-%717lg4&Qnhzjt2d{@NI+(TrRy)d+BWT~v3SA56LrUdku!|IQ@nwJxIuJw!Uo7QWkM>Ic38fsC zn86_K*8;O}XI#PaSX|1-NKDuv>&H;hb3u7`qRS+D#3wyC=lt#7lTZ#Sk(= zE=h(PZDom^NbLB4g%Rjo7>35CPO)I$gb)!U)cK?eCW?=aIVGCQ|AofVvdzZC>5k*2gU8Z?Es#xnP8peXJxOV-bv@w z3bs2cD)b7c0jH)h_#-Pe8p<_Q3g)_vqA)oi-fEn~PxCP-`HwkTEZ^V{^1 z#S5PEk$44qU@?Ol5x;{(C27E@@lyuv_CRlPu`YGxoEM#dCgRDZP#L{I;3uZu0ICxTHbjC&vJTMIo zB>@9F@d8v~p0*s(95+9VRbvF@$y)R7u8^MLkyiju>ys6l<(Q~(4Ag6R$V&>V^-*(LVmFK2TWf79k{N93{9Ww4F(dB(<)WX%XMNnl>!&o$%)&ws3w(_LK#u8Hk%lVp1-BJ`MQ*R9HAn zZYjjM7AfL>5Q=+}<2qq1%58OA+!^ObPnOeDH$-NO{A;H+6Jv&Ab>hzvS2vZ9txoo*;cfzYV5g?$3%8xAO>l2(`Yl|(08j&Z7V&}7Kr zQP}GBJ|McL|L(W#ReqgPlj6j7oqa%a;v3C1#mye8se&6&M!@k@Ei0j$aMnd{g`+ zxIt<(q3JPX#Adi~{0%A3I`!c2NUUa*ZV!C9xqn;@ki|80Im^fdJ==_Ttux&PChKuB z8G(7_=9gyX^&VdJZ33=owl_Modq7=ToUb(`$+coEfaALt4J>=Lgb?+yfb^#i)H0~K z@P@`zqLy}XUBU6?UQw2?Wt<39%Hmuo1D|XJ)g}6AqEGY{Jl)e+aG6hH#7i(p@SY;2F{yYz!7+^_Pr_~7Ui^)(95THqgTl^or j@?Ew3Z&@O*`3jV-e$JAZQF>oX2LmG~1(mFZ7zO_?=V|&$ diff --git a/tests/ref/outline-first-line-indent.png b/tests/ref/outline-first-line-indent.png index e40b440949a8bc1ab590393a19c186bc0d793152..e3341295cdf508f9425b4553211321fcd4aa2125 100644 GIT binary patch literal 5539 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 literal 10837 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 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 0000000000000000000000000000000000000000..e6dbbb5f1ddaf02145241e91eb2e01dab11af21c GIT binary patch literal 6935 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 P00000NkvXXu0mjf-RwE) literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..097e0bf88f1858e0d927b48233d7f39ede2ce5a8 GIT binary patch literal 5712 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e746b35b62c7603710b7ee3025693e3c7eef0bd9 GIT binary patch literal 3101 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 literal 0 HcmV?d00001 diff --git a/tests/ref/outline-indent-auto.png b/tests/ref/outline-indent-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..53517abd8352ac231a80fb3885def697d86ae548 GIT binary patch literal 5176 zcmV-86vyj{P)c_OWIg%tJQ9{X=^u^s#Ubs7PN{2A|iqUB8n_!5d{HJWM4&gWM5?m z+4p@%L|eDC{ZW(lBqzC<5P@f=nL+!#`G%bN&b@c$y~E7A_cQmt7ygm*jF%VW6o`dV z#6l@zp;#zIER-S^N)ZdiLMhL&g+U<^uf4JCVp#0x_-!p=w%hVrZYhL>{!w1hP=BKX zLN5!^{?*r)YJAD2mY#hUwg@@<`bT_X`rFG^rDqoW2Nu4yWQE38_3{qr8<=>1)n`K^ z)8c{tVQq6XBdgGG77h;I+_7^{N?Pvv@3+OorHBE~o`s>8V!|U6i%V;wW3QTQHxmOE zi-q_~eDS5%&`V=;_w~vfbG_Ke*bD|T>fw7UK56e9R1Y(=iw1_KsPQqS=;<5F%`c~} zt#7NTYo*RBxZd46sw%6GI66^RTwLd=@gLLHF+h#W#_;GZ>eTc+iq7ug!s06GimJvN zOnapEWj2NXpRH!swU;ODmiBgyfRaio~Rp2M_;#V#LHI z+8%aj@92TP(bx*@>E#Q}@bL5*8J&c<|KKls2PY;wyZX+a3!=`xmYcNvZ=W}^kO)5 z^40XhqB3e9-?IaQqtr+29VzFouZa4Le_&7VAT|5M zX+#e=9GMr&)Lx2MC`BxkA{L5;Ql4Q8HBLekuVjAo$r?9zKV8^{nV*)nn7aMC*Lv$V z)APaMF0NjfP|Bnp!MQjB!VnfB-g)mM&a%}sS$o^d5eZh-_7M0dIDW!Y%p>MrvUDYQ zh2bn*vGP+zRX`j}cUcgHXbCVmSlH1uWWN6pV2b$1IB?Jb0LTayGS+P{0W;HX;dkq{ z`1l2F+`I!{r(&LBq{g&x^Ol_;pHKBEgaG8kFU+8q;-3*L#P5^9yEyxm)lK{q0*k{` zEk9o5~eM76MnfbYZ3;6|Q0fC|Ow;ddsush({RhV< zW*~a|Mh@FKGMSl`fALZjwbyU{6_xeWd-hsQ-TWP*vunWK;kY`w%i*!q?jF9?HI3A} zf3>(Za}T1uqwkobE0b4}GoqpssZXBr!l|G(+qI9Pxuxs4%W3L_#MHR>tJE$h+#8!Z zAQ*W6I49^9Z(inwGPRc?7D^EdrHF-Mp%mj515GN{Z`k7Pb53P2zQ?$q<9m%gpeBaw zd(97UJ|HnPJlWSj3SpqB{nB?nz^s;<(_vjPe_#S@Yv-({!$%xW2%6z)?zD`2qhW5q zAwY_&sX6vXT^DS-`m=94CD!GN*8SV@+)tOVT%`Yuln%w z1=}uve>J{8#WN#Yxa{2z@db#F9adZ8A$C@P2s2Nf3%om5TR;DhAGZEX4dTp&+YBOO zh62;T!RopTfrqMsBHC<7R*q1;;i;$UjiK8cR(8+R8x9i(tF8!7>`g&)uz1jU)f@S~ z3jBJSoN~=@nEUH*HvHy&wz8@LWEO09)8_4dXU-=kr6FHXSboCQqqwAUo*qDZ9i2{s zjE7y0Av;z?8xR>{V)8afI+MA1B_5uB)NZG}LBpv{w*5><-O}29z}k+<@Q64BHIjG= z0@YAi!E|&&jc(C7HC_%cN{WrQ!DMty(xtFyY7lmU>FHZ{w`~21Nw9d=lb+N#Az9f4 z)IaVp8ymkxfDZD`WNchA$Ue30;bUat3UhBY*@>iJZkgIk5eub=g;K;qu~5qY1#|ym zx)-wum5Gc>;24^N=C=0U^o-oO*3Mv+3P$Z%9pmF0fML1-ZpP%)o#Bxw!(nbLJ?3I8 zhVIHUPMSSS`gm*vr;M_Q^qe*9REKDD+oCg#>ZIH|zc8l;;; zYG$y76QZQFiaSQJ21j?M!EKeZLhbj&223aCQ`im4Zjp>nNYSl>JXW$m%oAYrUwyrv zJ43ZZYTC6WZ!f3T9+p-~h zfxp2QB5}(Nh+|G}tk0X{;9d;e+l?C-&((wLDet0^Y6xK7u<-b~s-Y`%NX@_Ot9MF4(Nk>V`%RXJp=|_hWMAgdwYp0 zioyW?&y?0n7l@*;F>AaOT_C7T3(M>wN+e26Lb1z~l#@PK7ExqTL_JiG{dfN0gTq1Q zVIBk-IIth~%$$Aw)>?a5YwhohUcY_m%9t>SOZ0)z5S;H~0_8;VV?Ff;)AMBL@qAE&A^h?4ubD^q-mJbSD?WzqRwu+Ucj`%4q{QcU3hf0D-8wdX+l9SU-xU{GnUtiM=V zRVg5ZfuKs`<;v6N%Xv6RXMaI>b@UbuuALZ?Uy3l8QX=%#%CBHH&+?$Vy7n21?k+4Y zaTj;*JxFwb2gTRTw@~5wCp2!1aH{px1_}sKEofz##7kR4aE*30g!IPVsYDM{j`t3h zNRQ(>Z@{Y(tJU#a_Dd^sl6hg z&l*jmQ?FOh$QB`(#?=K{y3?VY^^629e_o#*Dl!-m3K!K2;Y=JN~b8>1np__a^ zcJoe5aSZI`;Tu^IoScPsC#N0QQ}^e>OAc2vCn%i+p%VgN_Juu>mT!>{XBLq1`3obf zpE_EYYg%^Dza9Z(2a>m+uucfu98@jY=OAQ{!eZ{ULq|l7l#}(dVkR?>7LOe5!%gD^ z8ai{4A&uIlfLU{GjgAdR_=VcpVq!6JZGr+32!|Y0L}S7bpQ2A~v2Kwz^zE#@IY(mX zC4m6Bxt;1i=PdM%GeY!+7Q(VVa2}AFGo;3%%G(BE6|~!_QbAZjSV34pSd|LGs#FkG z5LOUwws&tKT2)a1$Dc!b$xQ9$?IrUuh@zLm$3hHCf=GJoF?txf5Q!mSMK76@Bq83? zfr5GnGH<0xqow8jk~)))*PQ!|2}(a$u=p6qnUDE~&lzTKHpBYn?6c24d!2pG-v7P! z+LhcDaSbK_hSl@Z;6PDftuY|G;c4)30Hr<`HT->M?z8p}8;>e9ls4oKrkc=6lS8yBey)U1T^ES4&Enz|Z`NmGAHvEgGbQ-L8_W3}ov!7lCgz5aNk?HI*b_iOuK5I)4D?cE_(&j4+9LN; zA@qgLKyDU(Aah8{s;@TKn?VAUocgH^!bX=6G7dbjq|ssuJm#Xg1qwsP(-1kq8=4g) z7O^9Uk3aFWIRrZ3C?|X`^F#Igf|ud9L9ja_DEX#GzfCoIT}s63NgtS^0XPVRdMJ*`_oVH_G6G9j_Z@kTWj!unPA#YA^;QOXb_bD}3OrU#B5DNEd)o3(Cn4YrJdrst;GcD+O zK2@4~uHw_sJxP^c1do(^@f9Cm=1*x*l$J|3Zrr$e^X8DIM}2*LDPOyGt@_L5RfLeQ zU%#Gm9<4?$7Hq``v4o<+5{e2-C@L%}yfv)f(D>={&o^%RX3g5K3HD(ha9{iHzW>3| z<)3{aMw${wFxI3(qnU}59x^mx1vF{$wA=X<#XF|Yd=yoZj7d)+~9LUi%=GWKC$fNT6@fGJh6hhs*s($2L}@=3K&6;7xiw1kg>U#Qv0Jzg}H!%Fr*4& zqKnaOQNW0(a0IqvHn(}#go%(YE!~@Ei2E6X=?G3r7r^nCgGwJH;N(>ETXUReE!B0$CO6e1Y z%cnBuDl<`LB@fxXBx=uF$SP8$D$_;F(7w=_5{e2-C@L(WsBk>1uu2a5)0Q3U)^FO> z`0bCIw-G8y_u(Ta20ypCrFHGPjd;4~+%wJR3fJxz>tX(61orUsM~saKQy-Yc=r#{o z%#M-|e!6kef)`(52*eo4$gd3Q!`{wl7$ms^UV-@_$O(-!${TWNym5E#OIGwJLd`EM zTm-P)VHIjvupHzuqzZuyr~;uV${T`_*1r|F8kSdQVA{8Ec2U^2@dCvU5!#GM;wU;CpyOPv>pWd@h}F z<4NspKb^ffd_4LAz&Cd`1d-1#YcoP-Y@V2#x0=`uH~0GBMo4aueJv^kTmP#jo<7}t z=+KeFhmW2*b%t={$g$4O?lWgvE?l^D@ZjO&$4_3p+J5$I>wxEWbaWj!a7Z$V{mfW- zN=wVxwziItPn`HW)5tl5o}NB$fRL|Txq9y0`H-8N|B?S7Cw6!DDs#w}E?qf){$j`q z?-nW(_mjK2dZYugy-K?J!vK0Jcv7|z1lD&4hMA{(xf z!-<7)XBa`{KZoH*JKfaQ$H%8Zq2cuy^9sy7E}W~ToqxFMNySh`H5qboA*&osdnT;0 zj>m=fZGwm-7^*X0ldY@76GKiH?+M)FkSUaUst}QQZr(z4W^AjF4-iJ_`I8-A933)R z>K=|L0*<5#vHv(EAI*^;ThND_5k6`W6KmwvT96mWhO1eL1GunuwCr6W>kQb&ybAf; zWgtpOBT}#2YG@y0PR5<`Pfh%gvGZ5sX!u+2ywA%A^=KS}xGq!U5|d?U$s7}G6=BN(-6h#- zQm*L9@K3!PA3xtbs|gsY`_N9gf2Jixw95=!R!}>@?I>_he*~IN2RH1pLuPD{Dwj<8 z4$RcFT=$^5iLp;^^YVOIEL5W9NZ+^X9vp zqXr==K8oSoJ$rtaQW*k&Z{EDcU--U~?1S$igV8xwabl*)aw6vPPbPls*oh?X?k?p0 z`~M0epNAAi8Jt^&_WeUsQ&U4jL-?Ipx^!uwGh;HWT)A@9s#T4RjkH))SVB=@2}OlP mg(VafmQYk!LQ&zk4*vtyJal;x*#R2>0000@FlWRl4wlT4bK%#7_!r&U|6wbrV2sdcaWZms*iEA9ejg0jumbu zggpm3cJ2xGC>t_t?B*>A?}+GaJCa9^o)oib`@jBOZ%o?R3JKldECE^4!@ME`d0QCRq@frEx^h>iu% zofmjE`y9a7umAbYcj|N>-)VP#&F5`&%w`I2ij4136qc<-n2E5s|_% z8{>8*Bnbxvho+@x2~YK#apmd_;n8Ep3;c59&J=Gy;moY;g@HlBNy#beBBO;Pqhb=1 z_6t)WBlE1Vw~s%EJ5+e}+D%R*%&BRBaKi3nu0=RvP1OFB!+^pJ96XddVx$)}aHV!0 zZ5!tXQ@FMB3TV@U>I@=M+P9i7ci|VrZeA_X&d>B&rLT;d!*dpXt4zIJ3-61<}Sz>!j#KCki(K*kRv5|xp47+VU~3~;DWST~F^rV>5!P$m5Kmaq~& zHI;CbsRU5C5@Wsk4B-ist3=v5e6)Q>;^HNt!plNeZI0U^?Cmo>_1G!l ze*K4Dxq1t5?fRWQeFtmj$+XjxCi@9*-L`v4P?#{Mh}*JLm{!p|hf1zqyG=W_lTWz> z;rOiyv`rYWC4QH%t`Q(hNuv9m1L`3u+77`Hks≪BIk|3#CngLkaB*0~+Kpu=QfbrX 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 literal 0 HcmV?d00001 diff --git a/tests/ref/outline-spacing.png b/tests/ref/outline-spacing.png new file mode 100644 index 0000000000000000000000000000000000000000..897a5f74609d16efe885a14d491024be77afc562 GIT binary patch literal 2553 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 DgvX)g delta 1463 zcmV;o1xWgc3&{(RB!7@eL_t(|+U?t2Op{j_2XF@q;lj(g#h92dbz8D5yKu%V(~Frb zBUy;aGUJ6a8eo#mp-d1cIDvpbEngMIj4`qa#R9dZ&U85ZK(Lh377LX^X-m;UTj&>1 zzU-I#o7dC^g_&t)g3ptalXIT)yf1lwd2@Q2lr;}2{+C!o1b+lg&=dqs&;(6E&;(8K zDxl?Z`JFp=JRXlkBAJ?+BGdl@&6OB7u@qIUMx)6pmW^)9S0&XB3>&SOGR-t<&0ysn z{fX75(Qp6s^J_P{kPL6D*v?{g;sLMUly!BhF?bmah5mNiAyzh$q9tPpi7wQEm z$FeZqv-iM__J8iz3L4M;;YZs9Ldi>je*c6*Gi+fRpj#F9k*seYIV&;b3UnAh4P8)J z#^zM!<#Y7L@m2pu|L|klCDC6m0Xin5r~HbZY1ldd4e`$kdoSKH#g?jTuZ=8$uKk^# zb)o=MK(n&*3Ho`VKRBU8&$Qj#en9g(OkY>_%QR+bzkgY0v@C&6OFzjeJ%=g&JTe6R zT+sO3Tc0KmL4Q%A7D`9^b>rAi`i8~_^%j+Oth%QD`@^YNjoZCf(%N3%AjANhMiHBf z4J34{?(S}{*E={kXt&!ZCntx7h8{e4fD;RrIyySgV~h;)SVav6sK-@H3UoT%#KZ&& zaH}N+J%2qti;IiH!^1Y4&EarpwOXIgCzHuS1qy{?VPQe9*ITVtr_(twFyQz5pDr*M z3@C89TzBu@ML}0rSExX#RL;-OhePi_kW|OJ^rFzUoz^7Y$OWm*<@Vedupho?%KP)a z-RWtP+m1Avs&83bE>BKwL26p&&Gy?zj;3CRG7@4M~CamIlPzg;Ahl8bzj12UprKKPi2m}D_?d|3B z`M4pQ%?=fmmzM)bC=^0ptyVWQGyto(xR@yj27?t96&8y{Boe`{udk2CguMar%#D=W=rGo}cSXTGCThNp&r z)#jitEcDHgpeYENpeYENpb46Spb46Spb46wDF~XNDF}L1L;HNbnVFf;)ZuUt^s>=z zoIlg)J|}gf7im1m+;{u9o$j-@-Icf96@MMBVDK4N84QM9yLLrKM`veeV_H>JwPC}C zh=>TM(@D_J4$ornn?J_iNYi~%G3uD}BjXP|Jl{2iOyqH$dVF!Y3wb;qWOH(InCaH7 zTQS88PSDE+!CT-yG4*&n1ifthB6|d#dVl!| zv}M|l%zMQn;q=&B^4lE!mc{3R)@rqEHajaTOCphAD<|kD(A&@4%aYicmMoeGhc-IA z>+=kPeox_bXHF^wlR1p+kp|7mLLN{V!VwP91NZG`y2*z?p<;x=HT} z(6h6%7;N0Qk;#{pm0^m*2|+)GKDPWG{dTV5&2*i8*3V4my+QOMktqDsv17-M(5Yj~ zmMu|HQTRa+#vBd@d7S?U`XROguzp!>4rTt<%^?$=IgG;}ra!kjB97}mE`J@(x$5|~ z!OH8M2?_4ow-4iBFvy&vRVo#xH8nMnk&!7WDS<#BHa0dUCI+wh>kfT+U>3V`Vv`Mh zb@vo9BTnyk%{F8bnr!$~#MTiAK6>l-To^xa(b_sR$NYjVK0ZD%F%i8`C@d%_NJvP) zG$|>G`ImsUwzlNt^;cBgyQf1+x>1pmj-k7ePU)78p&JGekWK;Vkw&DuyQD_x2I=mudwk#Tz3Z;~ z7u@s9JZsiIXYI3}_{51&RhGqiLG}U;4i4+RoRm87>jeh~pMZt{L~jIEec|B9sNPG7 zYkJKeW}&)iX-yBvTDe(Hk`C`+a=m?9Y+{}tPJp_L#wdl>7NIFF5dnHbVDk+bLEMMi z7LD@FwGpQW(qOGrv;fTgOcsv1@T?j!K& z)xlEIQAB0BHI^RmSknuREi6Jp6bm-bPmh+&dbPz?CJ~B)Mdo&Uw)y;Y-SLpr?tg!@)WXXyE-NF`y#s7j`BE)M#GlIl zLXE;xPHs^2`BvqGQpE2jL6&MLjf-Bn&|)|x`cHm7jSw91C!Do(!zP!_onLwUE*rBo zRw_%)?m6CDq#~$a9#W1+zr53M*&Z|ep~KN4gd_UEt{r~vtq-g(H*7hxF>H22SA_L^ z#`)a+mBU=!|Ds6ru|zpbfTYFq*dwpq@AmBWe0u0*0X;zRjPg5yk8E5zP3l}u5wtY&xYQ{x;p*ojhK}T!|W3AJfiUEizGbmczyzR zJQ6Kzpf`J;Z^8A6L7wkd$U)X7SKB#|<^1>f=b}OAWOa5k*e{dll)7amid1;F^;R`mjTOd~(!@$hls zYl`QW6<9mdW%{mkh*uLTqMM>YqsXfhPJ+^r1iOhF zFnG-Qt)I$5{OkkxEkBU__b#l+c^!JMj)H|ypNo3H8{ z;Bm+rf4v&>*;*MN&!ik*$#@4tXW|9f5dBdldiP;IS~4fw=jfKWO$>*EqRwK$XlzIX zddsEXetC7!6vc_*GYC`r6c+8q5cOP?S(C%2XDj{32lMk&S$u4urRo5S;vlfRv<428 zYIkt60}|IRzM1deFg!ZkD5)|PzKbK{LZ11RYvCKz&H-z?$KIVRNX@%Fevr;G%-v8J z+nV|ztAAieo)Kq#N&96XB zH86dpVyr9QXRFrX;^F!|( zA-bJPc5|Sxf4@*bv-ZJ7lh7d+6=$$)r~CU1mhMEQV0n|ZH+&eu^6vY*o7;CWf?v-@ zAuu_eU8aU>o9x7 zoy%g+$M;A0Zz7g_!}MQ6?#B@QsAUungix6KAx1w|cLlQjn&T;PUh!qp2HS56G@4rw zfucY}n(6Eu&1MDA^g1$4uM$o$zfq}&*V&K&rl@f{+nG?icg9ic@W6i-aA z-Zm9kyZ{)CftmajpSY~XXap5h26Tl?=PFG*C2`XQqI}%C6dzM;y!P;>+eyRifUC*ZsY)=G__PbgS4tIV2M@Il5?IsCU6S)O++KJ0F>wqA zQWuACn845iVoCVFnQv#WqEscY7+xj&qs9utDzr*lymoDC?n80jUcZeO>)`d_gb!+B;?U~W6$15M~v`|dO%6f3{-->H*p)c6HAqJwts|2 z=9~^HE^XB8;hZnh4hn{-!UozxP<_6}IaP&bdDo^p^mG{eBBM+q;yp5+wa}X{LxMEr zU*5qoc#bVGyRE$@Z-^DNei+rf5f?ls#|A04Y9EU)A{yRIsy%siRX-p{;q%q`(uwku zutU*loCku@dHquKIFZS!ikA0xZ+;gj9H94}5JqsjZjViAoR0=Rt$lt4Ou4ty)e`Vx0AZq(2V`GGB)&=@7B#a$m30vKD{LSc={{ z3!y384Y{sN%j1%Dfkw>MVH`$^J4M=2c|qr@Q@F)xw{+q) z31*||+$5xt7X<)IQ&i+eCbkYrK@wOEmbV%UuoS0VnJL_GXh%)r1&^PMtkWx37}Skj z*8-gpm&0x$HMx_2In>%l8!X>iO;f8}m&!OMRd@c$!Fr7)$ne3i41f}9|Ap`}u8tViMMqHID z3LFHLhe#$@AdnA+bMT(KvbdVmZ-=-@B$6UPHCFAO^>fh!8RoT-e)+oLsF6&F2cFW477GW#ACka01Fi z?_VdCPeF?oKfk=$@-D;Hhdivm+7(cOCl4Bf19|6rpKYY^g0tr?4hxyyDVaBb zR~+eKgOkn4cahVp50L@YZY@MRm}2;6sHH`0{e(^-M^&@omdn?Rt~fDY{AWJwO_j>7 zUx+@e;ea*?frS(Dk3pzZosC308yg?{nV0tbGa_M}S@oB+3MsuoVg}HOi3M<%@S_4P|m$djM) z$zHf*#yt^>UL);*?{Qm$+wO0KEOD^1-Z*%^7M+|@*yjuOO5hfiwS%Bd@zcZkxcO^O z>^SO09tYE#i`@x;W^S4F_V%teA`7&MFExOTYfhU=wJWx;n{1~T=fAsd^bs4^THzHT zqGCIB|4e2g9d0dPV548ag<-h)Oi1)sfFoqg$tWm(&`17&cZgtXA1AaP`O0BCm(vOqP-MX_353bXDXU%!*pE+S^`Ce=M)J2s)B4)dTV!r_Qq zKSO9Li~ff)J#KyE&-N8YZSr3jaLLK-Vw{45gIl|AnFlJ#hW~U9CcIr=Uq5a=9|IUT znp;0KiiqWBwwS^z2IF=i885FJKP|f5E;<`tKXU&%20561+=%D~la8R@~f` z!t&Z2d0HLk1U*H)htI;eFf{-9s-c4;(qFvh43C&twi$i}@o#0kH_msc9Q;$)O-V+! zmv&R-wySOo19Z>sQj2G~K~r~k_pBEaY_|lEd4Dy6Q}-J6b;^cpH98(1Uiqc~kfIP_ z$3?y%Wfftci%x~cjV2z4`O1LsuAeD&5}Qp3Fd;nyj0-?bRcd*$Bf>Cs#FALec_qIB zt7;8fJRpzz#zSvcO{AodB$t^;qx0of6Ky4Nw0`3Qstn69XiN-q4fm~#$;l6%BUz<% z&`{^=WVE`Qr5SZ&}daVx_>8dC-gzDe=hC862F-g14UYx3* zp1rkBcgDN1O20m+8L#G+8+<@ccY~Nc)K5x$ksN$LNFT(l!X{?%c{m+lGj3!lsH-XUWU z5wM8{Tk^;?!|NZDF8$dwhG}lU_7B9w8tr&QyBng<0evzMnEG%|Y@OKkbe7r?zab|T zfoWK68ZyELMSqik8@ck@H1#* zKo_^LWB`hikh3E&HahwvaVYyOC1{~?c8WzY098pHRUa#K=omWp*#J<)3moA^*}}d& z@N{a!KCtj!!IWwStxh7R?;dDr*?IrVeE1(1;(r3D2ACU=-4^TpF$jmoYC<7fBW~S< z)*Vr6icQ8rS1s0oOfUpWG3YzptE~+1i&}_uB06_A!@Uwd-haQt-

dMPzvt`W>FK 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

=bDhhvkJjjiic|Uacbz10ty+0J? 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 literal 9302 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 (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,54 @@ 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") + +--- 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 From dda486a412b31acbf767087c748a62ccc6b510b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20F=C3=A4rber?= <01mf02@gmail.com> Date: Thu, 23 Jan 2025 13:08:48 +0100 Subject: [PATCH 09/34] HTML tables (#5666) --- crates/typst-html/src/encode.rs | 5 +- .../typst-library/src/layout/grid/resolve.rs | 2 +- crates/typst-library/src/model/table.rs | 66 +++++++++++++++++-- tests/ref/html/basic-table.html | 35 ++++++++++ tests/suite/layout/grid/html.typ | 32 +++++++++ 5 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 tests/ref/html/basic-table.html create mode 100644 tests/suite/layout/grid/html.typ diff --git a/crates/typst-html/src/encode.rs b/crates/typst-html/src/encode.rs index 62146f867..71422a0fc 100644 --- a/crates/typst-html/src/encode.rs +++ b/crates/typst-html/src/encode.rs @@ -120,7 +120,10 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> { /// 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) + matches!( + element.tag, + tag::meta | tag::table | tag::thead | tag::tbody | tag::tfoot | tag::tr + ) || tag::is_block_by_default(element.tag) } /// Escape a character. diff --git a/crates/typst-library/src/layout/grid/resolve.rs b/crates/typst-library/src/layout/grid/resolve.rs index 504159e83..f6df57a37 100644 --- a/crates/typst-library/src/layout/grid/resolve.rs +++ b/crates/typst-library/src/layout/grid/resolve.rs @@ -602,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/model/table.rs b/crates/typst-library/src/model/table.rs index fa44cb58a..ba7924422 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::{tag, HtmlAttr, 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(HtmlAttr::constant("colspan"), colspan); + } + if let Some(rowspan) = span(cell.rowspan(styles)) { + attrs.push(HtmlAttr::constant("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())) } } diff --git a/tests/ref/html/basic-table.html b/tests/ref/html/basic-table.html new file mode 100644 index 000000000..6ba1864ef --- /dev/null +++ b/tests/ref/html/basic-table.html @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Thefirstand
thesecondrow
FooBazBar
12
34
Thelastrow
+ + 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], + ), +) From e61cd6fb9e9a90de8d78f05a43246f08feddcf8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20F=C3=A4rber?= <01mf02@gmail.com> Date: Thu, 23 Jan 2025 13:18:46 +0100 Subject: [PATCH 10/34] Support `start` attribute for `enum` in HTML export (#5676) --- crates/typst-library/src/model/enum.rs | 24 ++++++++++++------------ tests/ref/html/enum-start.html | 12 ++++++++++++ tests/suite/model/enum.typ | 7 +++++++ 3 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 tests/ref/html/enum-start.html diff --git a/crates/typst-library/src/model/enum.rs b/crates/typst-library/src/model/enum.rs index eb3c2ea45..2d774cbbb 100644 --- a/crates/typst-library/src/model/enum.rs +++ b/crates/typst-library/src/model/enum.rs @@ -229,19 +229,19 @@ impl Show for Packed { 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(HtmlAttr::constant("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(HtmlAttr::constant("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}")); + } + li.with_body(Some(item.body.clone())).pack().spanned(item.span()) + })); + return Ok(elem.with_body(Some(body)).pack().spanned(self.span())); } let mut realized = diff --git a/tests/ref/html/enum-start.html b/tests/ref/html/enum-start.html new file mode 100644 index 000000000..8a4ff37f9 --- /dev/null +++ b/tests/ref/html/enum-start.html @@ -0,0 +1,12 @@ + + + + + + + +

    +
  1. Skipping
  2. Ahead
  3. +
+ + diff --git a/tests/suite/model/enum.typ b/tests/suite/model/enum.typ index 258c6f6bc..e957ae9e8 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]) From b3fb6c2326ac6d585cc17d1f643bc06e076be042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20F=C3=A4rber?= <01mf02@gmail.com> Date: Thu, 23 Jan 2025 13:21:34 +0100 Subject: [PATCH 11/34] Support quotes in HTML output (#5673) --- crates/typst-library/src/model/quote.rs | 81 +++++++++++++--------- tests/ref/html/quote-attribution-link.html | 15 ++++ tests/ref/html/quote-nesting-html.html | 12 ++++ tests/ref/html/quote-plato.html | 21 ++++++ tests/suite/model/quote.typ | 23 ++++++ 5 files changed, 120 insertions(+), 32 deletions(-) create mode 100644 tests/ref/html/quote-attribution-link.html create mode 100644 tests/ref/html/quote-nesting-html.html create mode 100644 tests/ref/html/quote-plato.html diff --git a/crates/typst-library/src/model/quote.rs b/crates/typst-library/src/model/quote.rs index 2eaa32d4c..774384acb 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::{tag, HtmlAttr, 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. @@ -158,6 +159,7 @@ impl Show for Packed { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { 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,65 @@ 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( + HtmlAttr::constant("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 = + [TextElem::packed('—'), SpaceElem::shared().clone(), attribution]; + + if !html { + // Use v(0.9em, weak: true) to 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; + } + realized += Content::sequence(attribution).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/tests/ref/html/quote-attribution-link.html b/tests/ref/html/quote-attribution-link.html new file mode 100644 index 000000000..4da8b47f5 --- /dev/null +++ b/tests/ref/html/quote-attribution-link.html @@ -0,0 +1,15 @@ + + + + + + + +
+ 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..c652bd97b --- /dev/null +++ b/tests/ref/html/quote-nesting-html.html @@ -0,0 +1,12 @@ + + + + + + + +

+ 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..fc052d10c --- /dev/null +++ b/tests/ref/html/quote-plato.html @@ -0,0 +1,21 @@ + + + + + + + +
+ … ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι. +
+

+ — 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/suite/model/quote.typ b/tests/suite/model/quote.typ index 2c93f92cd..d0dcc55dd 100644 --- a/tests/suite/model/quote.typ +++ b/tests/suite/model/quote.typ @@ -84,3 +84,26 @@ 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 +] From f7bd03dd76533cda2d2626d6470d3bb55e03b012 Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski <133046678+wrzian@users.noreply.github.com> Date: Thu, 23 Jan 2025 07:27:38 -0500 Subject: [PATCH 12/34] Fix delimiter unparen syntax (#5739) --- crates/typst-syntax/src/parser.rs | 4 ++-- tests/ref/math-lr-unparen.png | Bin 0 -> 493 bytes tests/suite/math/delimited.typ | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 tests/ref/math-lr-unparen.png diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index a65e5ff6b..f9fb8b616 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -442,10 +442,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 diff --git a/tests/ref/math-lr-unparen.png b/tests/ref/math-lr-unparen.png new file mode 100644 index 0000000000000000000000000000000000000000..d418b14eaa978f7975cc8185ce6e25b52c7a5056 GIT binary patch literal 493 zcmV+IEiC>vD{QupfuT z9}}B_3aZl+)==5U9`ix0&-3GO`8c%!#QM3`o5~iyRK5vj@A!tp;%~Z#!0a=RsT{{U zjeg>fkFQODRL{R~S^T^n z(x3QBU5nqBL)c}1a9I4S2vS2ezon|h|01^>Ja}OJWP>bRL7lby;K75NX4q3XAAeW` z1^01S{A&qR!Ij}uN!)xzz~a|8XkziNg9Lp1Zy#-Zd@}nVev404zoCi6pGqDR@Nvdt z>RKGM{TPUPI~N?s`?g1;D~R3>4&*=6z!o3d9z-RJw=LR?#9O)OAi9Esi&i4>HZR&X j+G!iLc+}!ii)je}qm_{qY}NN400000NkvXXu0mjf^L^=Q literal 0 HcmV?d00001 diff --git a/tests/suite/math/delimited.typ b/tests/suite/math/delimited.typ index 226740501..ca82427dd 100644 --- a/tests/suite/math/delimited.typ +++ b/tests/suite/math/delimited.typ @@ -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] +$ From 58dbbd48fe415c5a345fb1665aab478a03b5df82 Mon Sep 17 00:00:00 2001 From: SekoiaTree <51149447+SekoiaTree@users.noreply.github.com> Date: Thu, 23 Jan 2025 13:35:29 +0100 Subject: [PATCH 13/34] Add lcm as an operator in math mode (#5718) Co-authored-by: Laurenz --- crates/typst-library/src/math/op.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/typst-library/src/math/op.rs b/crates/typst-library/src/math/op.rs index ef24705a7..5b3f58beb 100644 --- a/crates/typst-library/src/math/op.rs +++ b/crates/typst-library/src/math/op.rs @@ -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. @@ -75,6 +75,7 @@ ops! { dim, exp, gcd (limits), + lcm (limits), hom, id, im, From ce299d5832095013bbcf2baef38552df6d2fc21b Mon Sep 17 00:00:00 2001 From: wznmickey Date: Thu, 23 Jan 2025 07:52:20 -0500 Subject: [PATCH 14/34] Support syntactically directly nested list, enum, and term list (#5728) Co-authored-by: Laurenz --- crates/typst-syntax/src/parser.rs | 6 +++--- tests/ref/issue-5719-enum-nested.png | Bin 0 -> 800 bytes tests/ref/issue-5719-heading-nested.png | Bin 0 -> 217 bytes tests/ref/issue-5719-list-nested.png | Bin 0 -> 506 bytes tests/ref/issue-5719-terms-nested.png | Bin 0 -> 921 bytes tests/suite/model/enum.typ | 10 ++++++++++ tests/suite/model/heading.typ | 4 ++++ tests/suite/model/list.typ | 8 ++++++++ tests/suite/model/terms.typ | 6 ++++++ 9 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 tests/ref/issue-5719-enum-nested.png create mode 100644 tests/ref/issue-5719-heading-nested.png create mode 100644 tests/ref/issue-5719-list-nested.png create mode 100644 tests/ref/issue-5719-terms-nested.png diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index f9fb8b616..5b9e66e28 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -160,7 +160,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 +170,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 +184,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); }); } diff --git a/tests/ref/issue-5719-enum-nested.png b/tests/ref/issue-5719-enum-nested.png new file mode 100644 index 0000000000000000000000000000000000000000..7670454498688ea2701f53fc5a56f137f42db0f9 GIT binary patch literal 800 zcmV+*1K<3KP)pUv$QdP>})$aKd}za*#x~%JkPm#o~!rw>c#e)^W}s=rW3|s1~Zt! zM*@EdX+FkJ3-;^75j-E#duYKxOi1g;(`121wfzxpIbf}AGQWiuyb}S8a{tq=17WwBu+;dykUx%`im@Ko7mG-=o4OC({rkH$hN zVb=;yZ;`>BND71Mje-#=VW7Q1OfCdYsJ~^D6do{PPgo5^x+H`bwP@L#=7nJ;7MKvS zHx2w*w}kNL+e#Hm=Y^pn&5B%4pgct?lWC4E5C${&(D1UwC$`m4`Cx<++}|{sO8VPF zVCe}Zcxr9y6zSvz08UCUU~DH9?xT6fU2S5N zWG+yiyrd>m3C|V?gBg5S*uO-ID=(;hHc1Pfs5L(&-FXL&ceLPPhd+vRzXmfD@A#uZ zgyU-N(u2ng2>HeH)Zkw_Sg<3sMFW^=!DUIxn@HdS$!TI6bm?q?Fqpvygf~ePsx_I} znd!eN!Hwl&Mkrb_bKyH3_(xua*jE%ofNhQnyshuv4KFxB1+MBgA>)2TwhoH4F1&{UW)IrAHqpg^vSY79LE%SoE&g{RdwY6+tmza@Dahh zJn*32oLeXZJd*;TSO!?vBGVmT9Rzwy$LPaaJ%Hoe^x>s51K5uA(TCxD`{wm5+guE0 eFoPK!dj0`Pr%E}6wyz8T0000EZxfpUZ5a)k*$4xaxv z>s)8{(w=u8|Nno^ZG3ai;`K-E>uP>~1**-r-}g^yt%{qj+}4UKj~vqt8UFlC6*5~Z QR|s;mr>mdKI;Vst06Z99ivR!s literal 0 HcmV?d00001 diff --git a/tests/ref/issue-5719-list-nested.png b/tests/ref/issue-5719-list-nested.png new file mode 100644 index 0000000000000000000000000000000000000000..9c9a7cc62f9fabfc6e15ce6b01c1c108c0bd069a GIT binary patch literal 506 zcmV0005ONkl9oL%^2oD;09igAHG z#u*DD{MxvYO4BxVHzIt>s>KkN2}0#N9t_qeajKjEOI!FmzLA+Ax;){b0wI{-?7Z#y)1gCfcOl$3*sBvmntIDD?q%G&NZ6$rru{~rDts;O=6Ul!0|2opF07*qoM6N<$f|hsQS^xk5 literal 0 HcmV?d00001 diff --git a/tests/ref/issue-5719-terms-nested.png b/tests/ref/issue-5719-terms-nested.png new file mode 100644 index 0000000000000000000000000000000000000000..8428ae4eee55e9d02e2d5cbf4db5837a565307a6 GIT binary patch literal 921 zcmV;K17`e*P)N5jb^JU=$x0tnPP79 zek*fMx14xE#k`doSfDepyybGJ+0>mUr$@{fe(4;7c+aQzJ^w%aIETaYa@N2_11gBU81EW6}yM&SDk|<0iK41n#HFS z?q%oWJzZ5?--TrSiQoR^-WfLF^y6)uY8@)kQR`FsiSia=*pf(P5~7)1*!5gPgQ*S~ z0XF7O5;Z+UHvmxF`UYD+_TcAe5QJNb3-www1m<6b@I|5X_6L^~R!Rn^ZI5q9 zvj$gz5{>_8w$^%qOu1;7C72$WR7i;5Y{bE22i0NmE&F^Io+VHqje|R|3(JnwajL6` z%0^h9@dfGHiNOW9QM9j>UD)M9XXjiUv3N+JnjX}>0Gq(pn>|>D20*-7o}t$egD|&F zK7>^7Q4;{fqp8*nNvy&BqF^b^Ci-eYD#!)3Kvp40D-3uyIJF36mk-P2;b*Q4|=^<`ZQJYS1U>LrseHvr=w8_ z-0`+iubW>`Hd)^Y%?G00@{H&m+}%%qQsb227{iCeeWyq;-m^=D2q!s|I*LmiW99R= vz!c=<9}_!8*XknUd+-3kForSwzr()(Ihd{iz4HV~00000NkvXXu0mjf1~sp1 literal 0 HcmV?d00001 diff --git a/tests/suite/model/enum.typ b/tests/suite/model/enum.typ index e957ae9e8..288392d45 100644 --- a/tests/suite/model/enum.typ +++ b/tests/suite/model/enum.typ @@ -199,3 +199,13 @@ a + 0. + 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/heading.typ b/tests/suite/model/heading.typ index 45e5b50ae..4e529fdf6 100644 --- a/tests/suite/model/heading.typ +++ b/tests/suite/model/heading.typ @@ -148,3 +148,7 @@ Cannot be used as @intro // 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/list.typ b/tests/suite/model/list.typ index b3d9a830b..96ddf3c18 100644 --- a/tests/suite/model/list.typ +++ b/tests/suite/model/list.typ @@ -276,3 +276,11 @@ World - 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/terms.typ b/tests/suite/model/terms.typ index 61fe20b0d..23ac6e513 100644 --- a/tests/suite/model/terms.typ +++ b/tests/suite/model/terms.typ @@ -90,3 +90,9 @@ Not in list / 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 From b7546bace7fb8640d1e7121b8bd7baf3cdb576e1 Mon Sep 17 00:00:00 2001 From: T0mstone <39707032+T0mstone@users.noreply.github.com> Date: Thu, 23 Jan 2025 14:05:12 +0100 Subject: [PATCH 15/34] Ignore shebang at start of file (#5702) --- crates/typst-syntax/src/highlight.rs | 1 + crates/typst-syntax/src/kind.rs | 9 ++++++++- crates/typst-syntax/src/lexer.rs | 6 ++++++ crates/typst-syntax/src/parser.rs | 2 ++ tests/suite/syntax/shebang.typ | 7 +++++++ 5 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 tests/suite/syntax/shebang.typ diff --git a/crates/typst-syntax/src/highlight.rs b/crates/typst-syntax/src/highlight.rs index de8ed65c9..c59a03384 100644 --- a/crates/typst-syntax/src/highlight.rs +++ b/crates/typst-syntax/src/highlight.rs @@ -287,6 +287,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..b4a97a3e0 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: `/* ... */`. @@ -357,7 +359,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 +377,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", diff --git a/crates/typst-syntax/src/lexer.rs b/crates/typst-syntax/src/lexer.rs index 6b5d28162..17401044f 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 diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index 5b9e66e28..5de71cafc 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 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 From 2d33393df967bbe55646b839e188c04380d823fe Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Thu, 23 Jan 2025 19:24:35 +0100 Subject: [PATCH 16/34] Add support for `c2sc` OpenType feature in `smallcaps` (#5655) --- .../typst-library/src/model/bibliography.rs | 6 ++- crates/typst-library/src/text/mod.rs | 10 +++-- crates/typst-library/src/text/smallcaps.rs | 35 ++++++++++++++---- tests/ref/smallcaps-all.png | Bin 0 -> 512 bytes tests/suite/text/smallcaps.typ | 4 ++ 5 files changed, 42 insertions(+), 13 deletions(-) create mode 100644 tests/ref/smallcaps-all.png diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index 95db8a222..762a97fd9 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -38,7 +38,8 @@ use crate::model::{ }; 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; @@ -1046,7 +1047,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))); } } diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index 6cca24587..edbd24139 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -755,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 { @@ -1249,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) { diff --git a/crates/typst-library/src/text/smallcaps.rs b/crates/typst-library/src/text/smallcaps.rs index 1e88974f5..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/tests/ref/smallcaps-all.png b/tests/ref/smallcaps-all.png new file mode 100644 index 0000000000000000000000000000000000000000..f3be53f8290996cd1ba6dccc177412d3631f12d5 GIT binary patch literal 512 zcmV+b0{{JqP)mHk zcidp#+I|6{I1D$=34uHCR1nB!vl-BImi4Tj2EP7l_VPP_lwP-XQ3wo8cIZ0cyO;7c zDq)M~q(=b?R~N5=b&OS=GXi1GZT4s0SFUB2C}0W5XUmj>!SzQZZ1Oh4mn#|+0B{!V z4mtRzA3?DD8DL_u`xA~%I0b+j419x4V zz*DF3|G`6521LRVJ_tPjeFHWoVXb9M9K5~fA^9Y3W!n4}qs+J&5duGVovtxQ&pWEU zX|={(_wMS;+e!G?z$J|~JIrF~5(394T#um)O(#M{%gGpz`_ekt+PO7DyB&yF5dyOd zgVm|R-^+vS!T=Smm@_#zQE7;RFQlM!5P$2+hz*n=K=*5-6k1+DnGx}efk*E*5mo9w z{OGB2O|W2b0`R$Zie$MhoG30S4$FW@Si%xM&hQR$(n#()v83?;0000 Date: Sat, 20 Jul 2024 21:21:53 -0500 Subject: [PATCH 17/34] Just add MathText SyntaxKind --- crates/typst-eval/src/code.rs | 1 + crates/typst-eval/src/math.rs | 14 +++++++++++- crates/typst-syntax/src/ast.rs | 32 ++++++++++++++++++++++++++++ crates/typst-syntax/src/highlight.rs | 1 + crates/typst-syntax/src/kind.rs | 3 +++ crates/typst-syntax/src/lexer.rs | 9 +++++++- crates/typst-syntax/src/parser.rs | 14 ++++++------ crates/typst-syntax/src/set.rs | 1 + 8 files changed, 67 insertions(+), 8 deletions(-) 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/math.rs b/crates/typst-eval/src/math.rs index 51dc0a3d5..f93f147eb 100644 --- a/crates/typst-eval/src/math.rs +++ b/crates/typst-eval/src/math.rs @@ -5,7 +5,7 @@ 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,18 @@ impl Eval for ast::Math<'_> { } } +impl Eval for ast::MathText<'_> { + type Output = Content; + + fn eval(self, _: &mut Vm) -> SourceResult { + match self.get() { + // TODO: change to `SymbolElem` when added + MathTextKind::Character(c) => Ok(Value::Symbol(Symbol::single(c)).display()), + MathTextKind::Number(text) => Ok(TextElem::packed(text.clone())), + } + } +} + impl Eval for ast::MathIdent<'_> { type Output = Value; 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 c59a03384..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), diff --git a/crates/typst-syntax/src/kind.rs b/crates/typst-syntax/src/kind.rs index b4a97a3e0..c24b47fe7 100644 --- a/crates/typst-syntax/src/kind.rs +++ b/crates/typst-syntax/src/kind.rs @@ -75,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`. @@ -408,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 17401044f..b8f2bf25f 100644 --- a/crates/typst-syntax/src/lexer.rs +++ b/crates/typst-syntax/src/lexer.rs @@ -685,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 @@ -693,8 +694,14 @@ 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. diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index 5de71cafc..55d5550b6 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -252,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); @@ -264,10 +266,10 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) { | SyntaxKind::Comma | SyntaxKind::Semicolon | SyntaxKind::RightParen => { - p.convert_and_eat(SyntaxKind::Text); + p.convert_and_eat(SyntaxKind::MathText); } - SyntaxKind::Text | SyntaxKind::MathShorthand => { + SyntaxKind::Text | SyntaxKind::MathText | SyntaxKind::MathShorthand => { continuable = matches!( math_class(p.current_text()), None | Some(MathClass::Alphabetic) @@ -316,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; @@ -414,7 +416,7 @@ fn math_delimited(p: &mut Parser) { // 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::Text); + p.convert_and_eat(SyntaxKind::MathText); } else { p.eat(); } @@ -535,7 +537,7 @@ fn math_arg<'s>(p: &mut Parser<'s>, seen: &mut HashSet<&'s str>) -> bool { } let mut positional = true; - if p.at_set(syntax_set!(Text, MathIdent, Underscore)) { + 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; diff --git a/crates/typst-syntax/src/set.rs b/crates/typst-syntax/src/set.rs index 9eb457b84..a7b9a594a 100644 --- a/crates/typst-syntax/src/set.rs +++ b/crates/typst-syntax/src/set.rs @@ -64,6 +64,7 @@ pub const MATH_EXPR: SyntaxSet = syntax_set!( Semicolon, RightParen, Text, + MathText, MathShorthand, Linebreak, MathAlignPoint, From c47b71b4350434a73734789ebde1374b791dc88e Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski Date: Mon, 29 Jul 2024 00:25:03 -0500 Subject: [PATCH 18/34] Basic SymbolElem addition --- crates/typst-layout/src/math/mod.rs | 10 +++++- crates/typst-library/src/foundations/ops.rs | 12 ++++--- .../typst-library/src/foundations/symbol.rs | 33 ++++++++++++++++++- crates/typst-library/src/foundations/value.rs | 6 ++-- crates/typst-library/src/math/accent.rs | 9 +++-- crates/typst-realize/src/lib.rs | 10 ++++-- tests/suite/foundations/content.typ | 12 ++++--- tests/suite/math/symbols.typ | 4 +-- 8 files changed, 73 insertions(+), 23 deletions(-) diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs index 06dc6653b..905e159ab 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, @@ -535,6 +537,12 @@ 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::() { + // This is a hack to avoid affecting layout that will be replaced in a + // later commit. + let text_elem = TextElem::new(elem.text.to_string().into()); + let packed = Packed::new(text_elem); + self::text::layout_text(&packed, ctx, styles)?; } else if let Some(elem) = elem.to_packed::() { layout_box(elem, ctx, styles)?; } else if elem.is::() { diff --git a/crates/typst-library/src/foundations/ops.rs b/crates/typst-library/src/foundations/ops.rs index 85a041b6c..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,10 +32,10 @@ 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), @@ -130,10 +132,10 @@ 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), diff --git a/crates/typst-library/src/foundations/symbol.rs b/crates/typst-library/src/foundations/symbol.rs index 3045970de..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. /// @@ -425,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/value.rs b/crates/typst-library/src/foundations/value.rs index efc480d3f..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())) @@ -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/math/accent.rs b/crates/typst-library/src/math/accent.rs index b87e527f2..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-realize/src/lib.rs b/crates/typst-realize/src/lib.rs index 6ab6d81c5..99db2ef1b 100644 --- a/crates/typst-realize/src/lib.rs +++ b/crates/typst-realize/src/lib.rs @@ -16,7 +16,7 @@ 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, - Synthesize, Transformation, + SymbolElem, Synthesize, Transformation, }; use typst_library::html::{tag, HtmlElem}; use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem}; @@ -221,7 +221,7 @@ impl<'a, 'x, 'y, 'z, 's> Grouped<'a, 'x, 'y, 'z, 's> { /// Handles an arbitrary piece of content during realization. fn visit<'a>( s: &mut State<'a, '_, '_, '_>, - content: &'a Content, + mut content: &'a Content, styles: StyleChain<'a>, ) -> SourceResult<()> { // Tags can always simply be pushed. @@ -230,6 +230,12 @@ fn visit<'a>( return Ok(()); } + if let Some(elem) = content.to_packed::() { + // This is a hack to avoid affecting layout that will be replaced in a + // later commit. + content = Box::leak(Box::new(TextElem::packed(elem.text.to_string()))); + } + // Transformations for math content based on the realization kind. Needs // to happen before show rules. if visit_math_rules(s, content, styles)? { diff --git a/tests/suite/foundations/content.typ b/tests/suite/foundations/content.typ index 31ef1c54c..9ddee5975 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)) => $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/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")) From fecdc39846959e0dae12e51282bb35d3d417547e Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski Date: Wed, 22 Jan 2025 11:04:01 -0500 Subject: [PATCH 19/34] Use SymbolElem in more places and add `char` cast for content --- crates/typst-eval/src/call.rs | 9 ++++----- crates/typst-eval/src/math.rs | 6 +++--- crates/typst-layout/src/math/attach.rs | 10 +++++----- crates/typst-layout/src/math/frac.rs | 7 +++++-- crates/typst-library/src/loading/csv.rs | 16 ++++------------ crates/typst-library/src/math/lr.rs | 9 ++++----- crates/typst-library/src/math/op.rs | 5 +++-- tests/suite/loading/csv.typ | 4 ++++ 8 files changed, 32 insertions(+), 34 deletions(-) diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs index 69b274bbc..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}; @@ -402,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), )) diff --git a/crates/typst-eval/src/math.rs b/crates/typst-eval/src/math.rs index f93f147eb..bfb54aa87 100644 --- a/crates/typst-eval/src/math.rs +++ b/crates/typst-eval/src/math.rs @@ -1,6 +1,6 @@ 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, }; @@ -25,8 +25,7 @@ impl Eval for ast::MathText<'_> { fn eval(self, _: &mut Vm) -> SourceResult { match self.get() { - // TODO: change to `SymbolElem` when added - MathTextKind::Character(c) => Ok(Value::Symbol(Symbol::single(c)).display()), + MathTextKind::Character(c) => Ok(SymbolElem::packed(c)), MathTextKind::Number(text) => Ok(TextElem::packed(text.clone())), } } @@ -114,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-layout/src/math/attach.rs b/crates/typst-layout/src/math/attach.rs index 8a67d53b3..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::{ @@ -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()); diff --git a/crates/typst-layout/src/math/frac.rs b/crates/typst-layout/src/math/frac.rs index 63463d761..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; @@ -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-library/src/loading/csv.rs b/crates/typst-library/src/loading/csv.rs index e5dabfaa6..1cf656ae2 100644 --- a/crates/typst-library/src/loading/csv.rs +++ b/crates/typst-library/src/loading/csv.rs @@ -136,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/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/op.rs b/crates/typst-library/src/math/op.rs index 5b3f58beb..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; @@ -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')); 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}") From 7838da02ec8a9ffbdfa61ed3dfedb24557a0e49c Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski Date: Mon, 29 Jul 2024 00:25:03 -0500 Subject: [PATCH 20/34] Add SymbolElem to realization --- crates/typst-realize/src/lib.rs | 53 ++++++++++++++++++++++------- tests/ref/cases-content-symbol.png | Bin 0 -> 191 bytes tests/ref/cases-content-text.png | Bin 0 -> 184 bytes tests/suite/text/case.typ | 8 +++++ 4 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 tests/ref/cases-content-symbol.png create mode 100644 tests/ref/cases-content-text.png diff --git a/crates/typst-realize/src/lib.rs b/crates/typst-realize/src/lib.rs index 99db2ef1b..ff42c3e95 100644 --- a/crates/typst-realize/src/lib.rs +++ b/crates/typst-realize/src/lib.rs @@ -221,7 +221,7 @@ impl<'a, 'x, 'y, 'z, 's> Grouped<'a, 'x, 'y, 'z, 's> { /// Handles an arbitrary piece of content during realization. fn visit<'a>( s: &mut State<'a, '_, '_, '_>, - mut content: &'a Content, + content: &'a Content, styles: StyleChain<'a>, ) -> SourceResult<()> { // Tags can always simply be pushed. @@ -230,12 +230,6 @@ fn visit<'a>( return Ok(()); } - if let Some(elem) = content.to_packed::() { - // This is a hack to avoid affecting layout that will be replaced in a - // later commit. - content = Box::leak(Box::new(TextElem::packed(elem.text.to_string()))); - } - // Transformations for math content based on the realization kind. Needs // to happen before show rules. if visit_math_rules(s, content, styles)? { @@ -247,7 +241,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 { @@ -301,7 +295,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); @@ -314,6 +315,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) @@ -792,7 +801,7 @@ static HTML_DOCUMENT_RULES: &[&GroupingRule] = /// Grouping rules used in HTML fragment realization. static HTML_FRAGMENT_RULES: &[&GroupingRule] = &[&TEXTUAL, &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. @@ -801,6 +810,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() @@ -1124,7 +1136,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)?; @@ -1147,10 +1168,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 diff --git a/tests/ref/cases-content-symbol.png b/tests/ref/cases-content-symbol.png new file mode 100644 index 0000000000000000000000000000000000000000..b0b8a65e322ce257658328c02455efa3f21dcdc7 GIT binary patch literal 191 zcmeAS@N?(olHy`uVBq!ia0vp^6+kS_0VEhE<%|3RQf;0tjv*Ddl7HAcG$dYm6xi*q zE9WN`Z)@Wqme138AzrP)-EMf%64NzaW>$N&Gof5Qv4 m3E>~}w>**BS^=@pjg{dlLr2oy8~h-LF?hQAxvX;IkOyX9M(PwfByU;F>^%|{*g7KX+Po-~LBj{NKa Date: Mon, 20 Jan 2025 14:39:26 -0500 Subject: [PATCH 21/34] Update math TextElem layout to separate out SymbolElem --- crates/typst-layout/src/math/mod.rs | 6 +- crates/typst-layout/src/math/text.rs | 238 ++++++++++-------- tests/ref/math-equation-auto-wrapping.png | Bin 160 -> 159 bytes .../math-mat-align-explicit-alternating.png | Bin 1009 -> 1035 bytes tests/ref/math-mat-align-explicit-left.png | Bin 992 -> 989 bytes tests/ref/math-mat-align-explicit-right.png | Bin 1028 -> 976 bytes tests/ref/math-mat-align-implicit.png | Bin 1027 -> 1046 bytes .../math-vec-align-explicit-alternating.png | Bin 1009 -> 1035 bytes tests/suite/foundations/content.typ | 2 +- tests/suite/math/alignment.typ | 8 +- tests/suite/math/delimited.typ | 4 +- tests/suite/math/stretch.typ | 6 +- 12 files changed, 151 insertions(+), 113 deletions(-) diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs index 905e159ab..702816ee6 100644 --- a/crates/typst-layout/src/math/mod.rs +++ b/crates/typst-layout/src/math/mod.rs @@ -538,11 +538,7 @@ fn layout_realized( } else if let Some(elem) = elem.to_packed::() { self::text::layout_text(elem, ctx, styles)?; } else if let Some(elem) = elem.to_packed::() { - // This is a hack to avoid affecting layout that will be replaced in a - // later commit. - let text_elem = TextElem::new(elem.text.to_string().into()); - let packed = Packed::new(text_elem); - self::text::layout_text(&packed, ctx, styles)?; + self::text::layout_symbol(elem, ctx, styles)?; } else if let Some(elem) = elem.to_packed::() { layout_box(elem, ctx, styles)?; } else if elem.is::() { diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs index 7e849c46c..6b9703aa2 100644 --- a/crates/typst-layout/src/math/text.rs +++ b/crates/typst-layout/src/math/text.rs @@ -2,7 +2,7 @@ use std::f64::consts::SQRT_2; use ecow::{eco_vec, EcoString}; use typst_library::diag::SourceResult; -use typst_library::foundations::{Packed, StyleChain, StyleVec}; +use typst_library::foundations::{Packed, StyleChain, StyleVec, SymbolElem}; use typst_library::layout::{Abs, Size}; use typst_library::math::{EquationElem, MathSize, MathVariant}; use typst_library::text::{ @@ -22,54 +22,66 @@ pub fn layout_text( ) -> SourceResult<()> { 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 = (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(); + + 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/tests/ref/math-equation-auto-wrapping.png b/tests/ref/math-equation-auto-wrapping.png index 9c600172e63bac08577921144c30027a5772d275..2476d668caa919892baeac9574ea57d771691ac2 100644 GIT binary patch delta 130 zcmZ3$IG=HXN_Cp2i(^Q|t>ho}4h@M{9tC#0>n@vmG(4PdO8;xNQ;%*m?`A*t=+yt6 zCEI`e{1n^&Z~5LAi?$z~|F`~X&X4~e&F(&~xU}Sd;(InF>#H(kb`^EhjfWXt$&t;ucLK6V=KSB5a delta 131 zcmV-}0DS+S0iXepBz$K{L_t(|+GF@XK!9P?;!%r7EvA{p-LD78;;erIWU lMdql*qZW@^JZdpc008krkH9a7&glRE002ovPDHLkV1nN-L!JNt diff --git a/tests/ref/math-mat-align-explicit-alternating.png b/tests/ref/math-mat-align-explicit-alternating.png index 37e8dc06a7e06a903d87ef61d608dc8b92589097..1ebcc7b6847d96c69e3e5787510299955d3c7003 100644 GIT binary patch delta 1013 zcmV5X$Yw>YHx!K*VnU3dFnK@$4M zIUcAvM|UOfHq zy%c(#61ny`VXU1??GtPaa+Z`WQ)M?C*v(y+(8 zOAGv9HGixIChSd%$t8E0%bHV)eK#+tNjNy4O1#LBG+#?!H`Nc}P>W0l>gtIO00a^xXSwF z_gHWx#z=Sk6sF8s_)7 zawnCzW-n{;mQz&XyM+Wkdzf&l%|nPBk0rjn7eTW1x-Ci=w)+YB4hJFn`!~Zeg`~r-*^d#evVLy(K0wqY|}3f!RWbZ zSAPR=tX29}^LG7`xSV*-rvHV6?xs=;aH!?tMxZwf(TPvj;f*t2Q1&Je5|#`a>wWMK zsEsOs9fiUwmz?;%O3dRZhenKiN#+GXVlhEiX-saweN7`RR{=8EBcRag8$;} zcl3YWz_gWj=2Xgw&4)U&sxd?PeY;BsaBfg_XccgG3nnSWsJsVZBZs17*4p$|eFCh+ jfy_ErMw}66#H{NRi#W{02W|T?00000NkvXXu0mjftI+K7 delta 987 zcmV<110?*52=NDyB!9R`L_t(|+U?l?Pt;`qz;XWte{E~J^+P|prB>jY|WW#}Y6%D>SAh@jS2piTp5Qc^!)LPd92)FG3>YjjPxFHV> zBPaS^0$4CQ4%V^_4bre0Vr!eR|oL9Su&i`gNC?q@0$SYt(SNB0Diyz zK^=gj<>CQCb$?t-ZvwEloHqj4UY852&jr!2jhh8iz+Z2866}Dwd$`gx3FMZ{rc8#O z^vjOo%hSLoRpW=mMBo`!D*Q(jS~8V)i+=;mXNGs>14}W=sjNSN)c*R|pZVWRta3c7 zAJ~7VI^F^h+a(qL!GIRkrj8&?53bDsto5!t5pK^-`hN-G>)jYf7?@X7fUFX!aCSC2 z%l}j=Y$!!%6=_o8eP(oaWuH_y#)8f)F;d~J1?a3`>r=2}`LM6!J_!B2ASmr#gw=M1 z@aAp7#|R&NkU0p@V6uD*;4;o6Bc#n`*a^p)Yl{N;@a7ys^_Wjq0z7CkB>?zaYj=17 zy3Cn%gnzTstsFtf*(?>_ZX!f**WKFk2_WzL)i^PBCvC(J1O6AC6{d+W-IZu+E-3(} z?T>KO-o5tR^jkp2&HBA&py{)|=&yj~SM8_Y?|-kVQCNcJAR2T**Y<^sa=ZmBT4#j;>q^&S^MU;W7nfZ0Y<0>D4XoNGqIMoaZJ zgn#bZ3=6Fl3{lY(EgEie|7+TaRXl*=>k5yR{hEq!1?vB(U;I*KHC{p0Jsuu zA7=~#hR({kt3c9{WSG-EfKS1LFcx$pOjNCU5ZudN4_Z9a0WUys3^#(ZwgPmXMK-)- zL`MhbrNcDWpqp=6kPlPUhEAGF0r@a9=wv4H&lL$r!jZ7v{sWMZ&8QxdpnU)U002ov JPDHLkV1j|p*Ny-H diff --git a/tests/ref/math-mat-align-explicit-left.png b/tests/ref/math-mat-align-explicit-left.png index 09ce93982ad85083cf89b7a3b488ebed9fb05fc4..cb9819248275a76b4ae40dff2472c993c64ee49d 100644 GIT binary patch delta 967 zcmV;&133KP2i*sdB!8qyL_t(|+U=N0OcPNYhdt}X#DfPtY2wY8s1Px3(FVmO6-(R@ z+yKS6KqN|B)5MslxS%mXt0uG*1j~{rg-AdPr3944(AZEZYg=ebSxP%lrtRPNriWp4 z%uHY=a_Dz>-;Xepuh4W3T?GG$MUikM90^Clf`&aALME1u3C<=X^o zdJ)>7|zu+XrD5Rf#;p z%U$U3ZL!@*uVW2svQ07fkjJ-MuU^)paj30OQie<~rQEpTG0b9o1$TI8F{PQ7={{g* zxO3WpJ>B9DU-H9cCxNl-KM5z}o|RV4aPt}vfelTZ;eQXi#0bZgaE4zbiV;qH!5My* zBt|&t8D}^xT8waX+5-KH>F`wD08q5Gz#mv^3Cg}&>t7F-)_I3d+b(os7b(jh0fUib z$3bcwbETMp!CQ^`<($K^)s&hw847G$g)$BpklKQc1Rjl=3u<66Q2s{79$qcu49lhX z48?R6k#tg{XKXY848|?w7&y=v1219DSL;51a_zwJV=?}c|e6B-3EeUJq7&2?sP%B{=;sbVd#_#8P4he pzF}|(8Foaz=Zb_Q;Ye6megldE%v2mRp*jEn002ovPDHLkV1k1o(oO&X delta 970 zcmV;*12z2J2jB;gB!8z#L_t(|+U?j|OjCCN$8n#xmnD1H!}c_jy)7mxM7F485(Kve zw8UK)Ac|y|&A=p%Wo}C1mTY4RBTH1A3*|Ok9V`xTGrFObMpR&F1EEqVwiMcO?d4$U z{xAP^JSFQ=;Z!&kPK6U1_J389Ku7&r!_mFdB+x>6 zRAczteiF*~y|%DRM}qB}))YQ61R#xqviX4!!rFir1j`u2FOMmV%*`q-;l&I9%V&4q zMA2fgy#S%uvEK&J+;P4MbSJ--xy zd`Z2X8<6|zf`5kaNBux#BbV!EQF7yXMC5=Xu7;45cP6Er@oktEEfjvX*S$}|R>1!>Uene*ZW%cmva|q$#$T-U8%n#iF z4+lebg#W#+FmcN@DtVB5#i<&;{TfNI$)px)Q&qI%VE zc{)juUjCHdSUtRY{Q(F=gAmVoO+pA>b3(lEp!B5S(wReMfDV1v55$=-cIP8};4QNf z$6q?R%lx=u-Fk%EhRP;@nQnb9al*Z=nSYfVX7n9Y8p7{p=SgI@^8eG=Q^wArB#6DxCxv z$w56_R|jyfz49D@r&FH`5b14BlMqJpY5WsKuHVk<0M>e%Hz1f=%enySPHPC;a)4;s zbbrg4Dd6*_K(=_^*c07pL1;aN?tTCzpSF2+PXYgY7~5(C0vqQ~bv^*H?rR8BV1w1{ zk*`Z`0iRrMe(Ms@Q*F#Tj$r@h?`^-LX#dpF@~0=ZN7T&Zv>oC+t_BX|4FHkap2r2qf`07*qoM6N<$g6J&O!2kdN diff --git a/tests/ref/math-mat-align-explicit-right.png b/tests/ref/math-mat-align-explicit-right.png index 3592c0cf53ae7b3568390f256dcbe34fc510b306..b537e6571847fc1fae250cc43fedc960ce557e18 100644 GIT binary patch delta 954 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 delta 987 zcmV<110?*52=NDyB!9R`L_t(|+U?l?Pt;`qz;XWte{E~J^+P|prB>jY|WW#}Y6%D>SAh@jS2piTp5Qc^!)LPd92)FG3>YjjPxFHV> zBPaS^0$4CQ4%V^_4bre0Vr!eR|oL9Su&i`gNC?q@0$SYt(SNB0Diyz zK^=gj<>CQCb$?t-ZvwEloHqj4UY852&jr!2jhh8iz+Z2866}Dwd$`gx3FMZ{rc8#O z^vjOo%hSLoRpW=mMBo`!D*Q(jS~8V)i+=;mXNGs>14}W=sjNSN)c*R|pZVWRta3c7 zAJ~7VI^F^h+a(qL!GIRkrj8&?53bDsto5!t5pK^-`hN-G>)jYf7?@X7fUFX!aCSC2 z%l}j=Y$!!%6=_o8eP(oaWuH_y#)8f)F;d~J1?a3`>r=2}`LM6!J_!B2ASmr#gw=M1 z@aAp7#|R&NkU0p@V6uD*;4;o6Bc#n`*a^p)Yl{N;@a7ys^_Wjq0z7CkB>?zaYj=17 zy3Cn%gnzTstsFtf*(?>_ZX!f**WKFk2_WzL)i^PBCvC(J1O6AC6{d+W-IZu+E-3(} z?T>KO-o5tR^jkp2&HBA&py{)|=&yj~SM8_Y?|-kVQCNcJAR2T**Y<^sa=ZmBT4#j;>q^&S^MU;W7nfZ0Y<0>D4XoNGqIMoaZJ zgn#bZ3=6Fl3{lY(EgEie|7+TaRXl*=>k5yR{hEq!1?vB(U;I*KHC{p0Jsuu zA7=~#hR({kt3c9{WSG-EfKS1LFcx$pOjNCU5ZudN4_Z9a0WUys3^#(ZwgPmXMK-)- zL`MhbrNcDWpqp=6kPlPUhEAGF0r@a9=wv4H&lL$r!jZ7v{sWMZ&8QxdpnU)U002ov JPDHLkV1j|p*Ny-H diff --git a/tests/suite/foundations/content.typ b/tests/suite/foundations/content.typ index 9ddee5975..c3c119e33 100644 --- a/tests/suite/foundations/content.typ +++ b/tests/suite/foundations/content.typ @@ -85,7 +85,7 @@ [With ] vars .pairs() - .map(((name, value)) => $name = value$) + .map(((name, value)) => $#symbol(name) = value$) .join(", ", last: " and ") [ we have:] $ equation = result $ 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/delimited.typ b/tests/suite/math/delimited.typ index ca82427dd..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 --- 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"$ } From cd044825fcb1651781f1dbcafac4dec8b216e370 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 23 Jan 2025 23:18:02 +0100 Subject: [PATCH 22/34] Handle boxes and blocks a bit better in HTML export (#5744) Co-authored-by: Martin Haug <3874949+reknih@users.noreply.github.com> --- crates/typst-html/src/lib.rs | 29 +++++++++++++++++++++---- crates/typst-library/src/html/dom.rs | 15 ++++++++++--- crates/typst-library/src/model/enum.rs | 6 ++--- crates/typst-library/src/model/quote.rs | 7 ++---- crates/typst-library/src/model/table.rs | 6 ++--- tests/ref/html/block-html.html | 15 +++++++++++++ tests/ref/html/box-html.html | 12 ++++++++++ tests/suite/layout/container.typ | 7 ++++++ 8 files changed, 79 insertions(+), 18 deletions(-) create mode 100644 tests/ref/html/block-html.html create mode 100644 tests/ref/html/box-html.html diff --git a/crates/typst-html/src/lib.rs b/crates/typst-html/src/lib.rs index ffd8e2505..1fa6aa216 100644 --- a/crates/typst-html/src/lib.rs +++ b/crates/typst-html/src/lib.rs @@ -14,7 +14,7 @@ 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::text::{LinebreakElem, SmartQuoteElem, SpaceElem, TextElem}; @@ -197,13 +197,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-library/src/html/dom.rs b/crates/typst-library/src/html/dom.rs index 5b6eab4d6..2acd839dd 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() { @@ -605,6 +608,7 @@ pub mod tag { /// Predefined constants for HTML attributes. /// /// Note: These are very incomplete. +#[allow(non_upper_case_globals)] pub mod attr { use super::HtmlAttr; @@ -619,13 +623,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/model/enum.rs b/crates/typst-library/src/model/enum.rs index 2d774cbbb..4dc834ab7 100644 --- a/crates/typst-library/src/model/enum.rs +++ b/crates/typst-library/src/model/enum.rs @@ -9,7 +9,7 @@ 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}; @@ -229,10 +229,10 @@ impl Show for Packed { if TargetElem::target_in(styles).is_html() { let mut elem = HtmlElem::new(tag::ol); if self.reversed(styles) { - elem = elem.with_attr(HtmlAttr::constant("reversed"), "reversed"); + elem = elem.with_attr(attr::reversed, "reversed"); } if let Some(n) = self.start(styles).custom() { - elem = elem.with_attr(HtmlAttr::constant("start"), eco_format!("{n}")); + 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); diff --git a/crates/typst-library/src/model/quote.rs b/crates/typst-library/src/model/quote.rs index 774384acb..79e9b4e36 100644 --- a/crates/typst-library/src/model/quote.rs +++ b/crates/typst-library/src/model/quote.rs @@ -4,7 +4,7 @@ use crate::foundations::{ cast, elem, Content, Depth, Label, NativeElement, Packed, Show, ShowSet, Smart, StyleChain, Styles, TargetElem, }; -use crate::html::{tag, HtmlAttr, HtmlElem}; +use crate::html::{attr, tag, HtmlElem}; use crate::introspection::Locatable; use crate::layout::{ Alignment, BlockBody, BlockElem, Em, HElem, PadElem, Spacing, VElem, @@ -194,10 +194,7 @@ impl Show for Packed { 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( - HtmlAttr::constant("cite"), - url.clone().into_inner(), - ); + elem = elem.with_attr(attr::cite, url.clone().into_inner()); } } } diff --git a/crates/typst-library/src/model/table.rs b/crates/typst-library/src/model/table.rs index ba7924422..82c1cc08b 100644 --- a/crates/typst-library/src/model/table.rs +++ b/crates/typst-library/src/model/table.rs @@ -9,7 +9,7 @@ use crate::foundations::{ cast, elem, scope, Content, NativeElement, Packed, Show, Smart, StyleChain, TargetElem, }; -use crate::html::{tag, HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag}; +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::{ @@ -268,10 +268,10 @@ fn show_cell_html(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content { 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(HtmlAttr::constant("colspan"), colspan); + attrs.push(attr::colspan, colspan); } if let Some(rowspan) = span(cell.rowspan(styles)) { - attrs.push(HtmlAttr::constant("rowspan"), rowspan); + attrs.push(attr::rowspan, rowspan); } HtmlElem::new(tag) .with_body(Some(cell.body.clone())) diff --git a/tests/ref/html/block-html.html b/tests/ref/html/block-html.html new file mode 100644 index 000000000..98d971b88 --- /dev/null +++ b/tests/ref/html/block-html.html @@ -0,0 +1,15 @@ + + + + + + + +

+ Paragraph +

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

+ Text Span. +

+ + 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. // From 467968af0788a3059e1bed47f9daee846f5b3904 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 24 Jan 2025 12:15:09 +0100 Subject: [PATCH 23/34] Tweak HTML pretty printing (#5745) --- crates/typst-html/src/encode.rs | 52 ++++++++---- crates/typst-library/src/html/dom.rs | 94 ++++++++++++++-------- tests/ref/html/basic-table.html | 22 +++-- tests/ref/html/block-html.html | 8 +- tests/ref/html/box-html.html | 4 +- tests/ref/html/enum-start.html | 3 +- tests/ref/html/heading-html-basic.html | 28 ++----- tests/ref/html/link-basic.html | 16 +--- tests/ref/html/quote-attribution-link.html | 8 +- tests/ref/html/quote-nesting-html.html | 4 +- tests/ref/html/quote-plato.html | 16 +--- 11 files changed, 135 insertions(+), 120 deletions(-) diff --git a/crates/typst-html/src/encode.rs b/crates/typst-html/src/encode.rs index 71422a0fc..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; @@ -20,10 +20,11 @@ pub fn html(document: &HtmlDocument) -> SourceResult { #[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, } @@ -88,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; @@ -118,12 +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 { - matches!( - element.tag, - tag::meta | tag::table | tag::thead | tag::tbody | tag::tfoot | tag::tr - ) || tag::is_block_by_default(element.tag) +/// 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-library/src/html/dom.rs b/crates/typst-library/src/html/dom.rs index 2acd839dd..1b725d543 100644 --- a/crates/typst-library/src/html/dom.rs +++ b/crates/typst-library/src/html/dom.rs @@ -475,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, @@ -572,37 +610,23 @@ 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. diff --git a/tests/ref/html/basic-table.html b/tests/ref/html/basic-table.html index 6ba1864ef..189a5b314 100644 --- a/tests/ref/html/basic-table.html +++ b/tests/ref/html/basic-table.html @@ -8,26 +8,36 @@ - + + + - + + + - + + + - + + - + + - + + +
ThefirstandThefirstand
thesecondrowthesecondrow
FooBazBarFooBazBar
1212
3434
ThelastrowThelastrow
diff --git a/tests/ref/html/block-html.html b/tests/ref/html/block-html.html index 98d971b88..d1716c6d7 100644 --- a/tests/ref/html/block-html.html +++ b/tests/ref/html/block-html.html @@ -5,11 +5,7 @@ -

- Paragraph -

-
- Div -
+

Paragraph

+
Div
diff --git a/tests/ref/html/box-html.html b/tests/ref/html/box-html.html index 5c970a6bc..b2a26533b 100644 --- a/tests/ref/html/box-html.html +++ b/tests/ref/html/box-html.html @@ -5,8 +5,6 @@ -

- Text Span. -

+

Text Span.

diff --git a/tests/ref/html/enum-start.html b/tests/ref/html/enum-start.html index 8a4ff37f9..fc9b3c061 100644 --- a/tests/ref/html/enum-start.html +++ b/tests/ref/html/enum-start.html @@ -6,7 +6,8 @@
    -
  1. Skipping
  2. Ahead
  3. +
  4. Skipping
  5. +
  6. Ahead
diff --git a/tests/ref/html/heading-html-basic.html b/tests/ref/html/heading-html-basic.html index 56b1e32b7..54a22faf4 100644 --- a/tests/ref/html/heading-html-basic.html +++ b/tests/ref/html/heading-html-basic.html @@ -5,26 +5,12 @@ -

- Level 1 -

-

- Level 2 -

-

- Level 3 -

-
- Level 4 -
-
- Level 5 -
-
- Level 6 -
-
- Level 7 -
+

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 5d998667e..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.

diff --git a/tests/ref/html/quote-attribution-link.html b/tests/ref/html/quote-attribution-link.html index 4da8b47f5..753807db2 100644 --- a/tests/ref/html/quote-attribution-link.html +++ b/tests/ref/html/quote-attribution-link.html @@ -5,11 +5,7 @@ -
- Compose papers faster -
-

- — typst.com -

+
Compose papers faster
+

typst.com

diff --git a/tests/ref/html/quote-nesting-html.html b/tests/ref/html/quote-nesting-html.html index c652bd97b..6b05a94a0 100644 --- a/tests/ref/html/quote-nesting-html.html +++ b/tests/ref/html/quote-nesting-html.html @@ -5,8 +5,6 @@ -

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

+

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 index fc052d10c..f516adc29 100644 --- a/tests/ref/html/quote-plato.html +++ b/tests/ref/html/quote-plato.html @@ -5,17 +5,9 @@ -
- … ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι. -
-

- — 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 -

+
… ἔοικα γοῦν τούτου γε σμικρῷ τινι αὐτῷ τούτῳ σοφώτερος εἶναι, ὅτι ἃ μὴ οἶδα οὐδὲ οἴομαι εἰδέναι.
+

— 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

From 26e65bfef5b1da7f6c72e1409237cf03fb5d6069 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 24 Jan 2025 13:11:26 +0100 Subject: [PATCH 24/34] Semantic paragraphs (#5746) --- crates/typst-html/src/lib.rs | 9 +- crates/typst-layout/src/flow/collect.rs | 85 ++++++++-- crates/typst-layout/src/flow/compose.rs | 6 +- crates/typst-layout/src/flow/mod.rs | 45 +++-- crates/typst-layout/src/inline/box.rs | 2 +- crates/typst-layout/src/inline/collect.rs | 57 ++++--- crates/typst-layout/src/inline/finalize.rs | 2 +- crates/typst-layout/src/inline/line.rs | 14 +- crates/typst-layout/src/inline/linebreak.rs | 27 ++- crates/typst-layout/src/inline/mod.rs | 77 ++++++--- crates/typst-layout/src/inline/prepare.rs | 48 ++++-- crates/typst-layout/src/inline/shaping.rs | 10 +- crates/typst-layout/src/lib.rs | 1 - crates/typst-layout/src/lists.rs | 24 ++- crates/typst-layout/src/math/lr.rs | 11 +- crates/typst-layout/src/math/mod.rs | 7 +- crates/typst-layout/src/math/text.rs | 13 +- crates/typst-layout/src/pages/collect.rs | 2 +- crates/typst-layout/src/pages/mod.rs | 4 +- crates/typst-layout/src/pages/run.rs | 4 +- .../typst-library/src/foundations/styles.rs | 101 ------------ crates/typst-library/src/layout/container.rs | 10 +- crates/typst-library/src/math/equation.rs | 4 +- .../typst-library/src/model/bibliography.rs | 44 ++--- crates/typst-library/src/model/enum.rs | 15 +- crates/typst-library/src/model/figure.rs | 33 ++-- crates/typst-library/src/model/footnote.rs | 6 +- crates/typst-library/src/model/list.rs | 13 +- crates/typst-library/src/model/outline.rs | 1 - crates/typst-library/src/model/par.rs | 110 ++++++++----- crates/typst-library/src/model/quote.rs | 19 ++- crates/typst-library/src/model/terms.rs | 22 ++- crates/typst-library/src/routines.rs | 76 ++++++--- crates/typst-realize/src/lib.rs | 155 +++++++++++++----- crates/typst-utils/src/lib.rs | 27 +++ crates/typst/src/lib.rs | 2 - tests/ref/bibliography-grid-par.png | Bin 0 -> 8757 bytes tests/ref/bibliography-indent-par.png | Bin 0 -> 9087 bytes tests/ref/enum-par.png | Bin 0 -> 3521 bytes tests/ref/figure-par.png | Bin 0 -> 1701 bytes tests/ref/heading-par.png | Bin 0 -> 555 bytes tests/ref/html/enum-par.html | 36 ++++ tests/ref/html/list-par.html | 36 ++++ tests/ref/html/par-semantic-html.html | 16 ++ tests/ref/html/quote-attribution-link.html | 2 +- tests/ref/html/quote-plato.html | 4 +- tests/ref/html/terms-par.html | 42 +++++ tests/ref/issue-5503-enum-in-align.png | Bin 0 -> 421 bytes ...sue-5503-enum-interrupted-by-par-align.png | Bin 1004 -> 0 bytes ...align.png => issue-5503-list-in-align.png} | Bin ...lign.png => issue-5503-terms-in-align.png} | Bin tests/ref/list-par.png | Bin 0 -> 3319 bytes tests/ref/math-par.png | Bin 0 -> 387 bytes tests/ref/outline-par.png | Bin 0 -> 2911 bytes tests/ref/par-contains-block.png | Bin 0 -> 426 bytes tests/ref/par-contains-parbreak.png | Bin 0 -> 426 bytes tests/ref/par-hanging-indent-semantic.png | Bin 0 -> 1594 bytes tests/ref/par-semantic-align.png | Bin 0 -> 3082 bytes tests/ref/par-semantic-tag.png | Bin 0 -> 278 bytes tests/ref/par-semantic.png | Bin 0 -> 3485 bytes tests/ref/par-show.png | Bin 0 -> 932 bytes tests/ref/quote-par.png | Bin 0 -> 2792 bytes tests/ref/table-cell-par.png | Bin 0 -> 645 bytes tests/ref/terms-par.png | Bin 0 -> 3892 bytes tests/suite/layout/table.typ | 11 ++ tests/suite/math/text.typ | 5 + tests/suite/model/bibliography.typ | 18 ++ tests/suite/model/enum.typ | 38 ++++- tests/suite/model/figure.typ | 11 ++ tests/suite/model/heading.typ | 5 + tests/suite/model/list.typ | 38 ++++- tests/suite/model/outline.typ | 9 + tests/suite/model/par.typ | 141 ++++++++++++++++ tests/suite/model/quote.typ | 11 ++ tests/suite/model/terms.typ | 40 +++-- 75 files changed, 1098 insertions(+), 451 deletions(-) create mode 100644 tests/ref/bibliography-grid-par.png create mode 100644 tests/ref/bibliography-indent-par.png create mode 100644 tests/ref/enum-par.png create mode 100644 tests/ref/figure-par.png create mode 100644 tests/ref/heading-par.png create mode 100644 tests/ref/html/enum-par.html create mode 100644 tests/ref/html/list-par.html create mode 100644 tests/ref/html/par-semantic-html.html create mode 100644 tests/ref/html/terms-par.html create mode 100644 tests/ref/issue-5503-enum-in-align.png delete mode 100644 tests/ref/issue-5503-enum-interrupted-by-par-align.png rename tests/ref/{issue-5503-list-interrupted-by-par-align.png => issue-5503-list-in-align.png} (100%) rename tests/ref/{issue-5503-terms-interrupted-by-par-align.png => issue-5503-terms-in-align.png} (100%) create mode 100644 tests/ref/list-par.png create mode 100644 tests/ref/math-par.png create mode 100644 tests/ref/outline-par.png create mode 100644 tests/ref/par-contains-block.png create mode 100644 tests/ref/par-contains-parbreak.png create mode 100644 tests/ref/par-hanging-indent-semantic.png create mode 100644 tests/ref/par-semantic-align.png create mode 100644 tests/ref/par-semantic-tag.png create mode 100644 tests/ref/par-semantic.png create mode 100644 tests/ref/par-show.png create mode 100644 tests/ref/quote-par.png create mode 100644 tests/ref/table-cell-par.png create mode 100644 tests/ref/terms-par.png diff --git a/crates/typst-html/src/lib.rs b/crates/typst-html/src/lib.rs index 1fa6aa216..25d0cd5d8 100644 --- a/crates/typst-html/src/lib.rs +++ b/crates/typst-html/src/lib.rs @@ -16,7 +16,7 @@ use typst_library::introspection::{ }; 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) diff --git a/crates/typst-layout/src/flow/collect.rs b/crates/typst-layout/src/flow/collect.rs index 76d7b7433..f2c7ebd1e 100644 --- a/crates/typst-layout/src/flow/collect.rs +++ b/crates/typst-layout/src/flow/collect.rs @@ -20,13 +20,15 @@ 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::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, @@ -34,6 +36,7 @@ pub fn collect<'a>( locator: Locator<'a>, base: Size, expand: bool, + mode: FlowMode, ) -> SourceResult>> { Collector { engine, @@ -45,7 +48,7 @@ pub fn collect<'a>( output: Vec::with_capacity(children.len()), last_was_par: false, } - .run() + .run(mode) } /// State for collection. @@ -62,7 +65,15 @@ struct Collector<'a, 'x, 'y> { 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)); @@ -95,6 +106,43 @@ 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, + false, + false, + )? + .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 { @@ -110,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.last_was_par, )? .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.last_was_par = true; + + 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 = @@ -166,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 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/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 6023f5c63..cbc490ba1 100644 --- a/crates/typst-layout/src/inline/collect.rs +++ b/crates/typst-layout/src/inline/collect.rs @@ -1,10 +1,11 @@ -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::routines::Pair; use typst_library::text::{ is_default_ignorable, LinebreakElem, SmartQuoteElem, SmartQuoter, SmartQuotes, SpaceElem, TextElem, @@ -16,7 +17,7 @@ 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 @@ -27,7 +28,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. @@ -113,38 +114,44 @@ 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, + paragraph: bool, ) -> 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 paragraph && consecutive { + let first_line_indent = ParElem::first_line_indent_in(styles); + if !first_line_indent.is_zero() + && 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 hang = ParElem::hanging_indent_in(*styles); - if !hang.is_zero() { - collector.push_item(Item::Absolute(-hang, false)); - collector.spans.push(1, Span::detached()); + if paragraph { + 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::() { @@ -234,7 +241,13 @@ pub fn collect<'a>( } 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; 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 fba4bef80..9f6973807 100644 --- a/crates/typst-layout/src/inline/line.rs +++ b/crates/typst-layout/src/inline/line.rs @@ -18,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. @@ -430,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; @@ -631,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 bedc54d63..83ca82bf2 100644 --- a/crates/typst-layout/src/inline/mod.rs +++ b/crates/typst-layout/src/inline/mod.rs @@ -13,11 +13,11 @@ 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}; @@ -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, + consecutive: bool, ) -> 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, + consecutive, ) } -/// 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, + consecutive: bool, ) -> 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,51 @@ 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, + true, + consecutive, + ) +} + +/// 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, + paragraph: bool, + consecutive: bool, +) -> 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, consecutive, paragraph)?; - // 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, paragraph)?; - // 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) } diff --git a/crates/typst-layout/src/inline/prepare.rs b/crates/typst-layout/src/inline/prepare.rs index 2dd79aecf..e26c9b147 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>, + paragraph: bool, ) -> SourceResult> { let dir = TextElem::dir_in(styles); let default_level = match dir { @@ -125,19 +129,22 @@ pub fn prepare<'a>( add_cjk_latin_spacing(&mut items); } + // Only apply hanging indent to real paragraphs. + let hang = if paragraph { 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 +152,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 2ed95f14f..b688981ae 100644 --- a/crates/typst-layout/src/inline/shaping.rs +++ b/crates/typst-layout/src/inline/shaping.rs @@ -29,7 +29,7 @@ use crate::modifiers::{FrameModifiers, FrameModify}; /// 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, @@ -66,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 @@ -405,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 56d7afe11..443e90d61 100644 --- a/crates/typst-layout/src/lib.rs +++ b/crates/typst-layout/src/lib.rs @@ -17,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 63127474b..f8d910abf 100644 --- a/crates/typst-layout/src/lists.rs +++ b/crates/typst-layout/src/lists.rs @@ -6,7 +6,7 @@ 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::GridLayouter; @@ -22,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() @@ -41,11 +42,17 @@ pub fn layout_list( let mut locator = locator.split(); 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()), )); } @@ -78,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() @@ -124,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/lr.rs b/crates/typst-layout/src/math/lr.rs index 19176ee88..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}; @@ -29,15 +30,7 @@ pub fn layout_lr( 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); diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs index 702816ee6..e5a3d94c9 100644 --- a/crates/typst-layout/src/math/mod.rs +++ b/crates/typst-layout/src/math/mod.rs @@ -202,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); @@ -619,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()), @@ -692,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/text.rs b/crates/typst-layout/src/math/text.rs index 6b9703aa2..5897c3c0c 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, SymbolElem}; +use typst_library::foundations::{Packed, StyleChain, SymbolElem}; use typst_library::layout::{Abs, Size}; use typst_library::math::{EquationElem, MathSize, MathVariant}; use typst_library::text::{ @@ -100,14 +100,15 @@ fn layout_inline_text( // 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 = (ctx.engine.routines.layout_inline)( + let frame = crate::inline::layout_inline( ctx.engine, - &StyleVec::wrap(eco_vec![elem]), - ctx.locator.next(&span), + &[(&elem, styles)], + &mut ctx.locator.next(&span).split(), styles, - false, Size::splat(Abs::inf()), false, + false, + false, )? .into_frame(); 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-library/src/foundations/styles.rs b/crates/typst-library/src/foundations/styles.rs index 37094dcd8..983803300 100644 --- a/crates/typst-library/src/foundations/styles.rs +++ b/crates/typst-library/src/foundations/styles.rs @@ -776,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. diff --git a/crates/typst-library/src/layout/container.rs b/crates/typst-library/src/layout/container.rs index c8c74269b..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 @@ -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/math/equation.rs b/crates/typst-library/src/math/equation.rs index 1e346280a..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 diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index 762a97fd9..a391e5804 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -17,7 +17,7 @@ use hayagriva::{ use indexmap::IndexMap; use smallvec::{smallvec, SmallVec}; use typst_syntax::{Span, Spanned}; -use typst_utils::{ManuallyHash, NonZeroExt, PicoStr}; +use typst_utils::{Get, ManuallyHash, NonZeroExt, PicoStr}; use crate::diag::{bail, error, At, FileError, HintedStrResult, SourceResult, StrResult}; use crate::engine::Engine; @@ -29,7 +29,7 @@ use crate::foundations::{ 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::{ @@ -206,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 @@ -226,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( @@ -246,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)) } } diff --git a/crates/typst-library/src/model/enum.rs b/crates/typst-library/src/model/enum.rs index 4dc834ab7..a4126e72c 100644 --- a/crates/typst-library/src/model/enum.rs +++ b/crates/typst-library/src/model/enum.rs @@ -11,7 +11,9 @@ use crate::foundations::{ }; 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,6 +228,8 @@ 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) { @@ -239,7 +243,12 @@ impl Show for Packed { 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()) + // 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())); } @@ -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 ce7460c9b..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; @@ -328,6 +330,7 @@ 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(); @@ -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, ..)`" ); @@ -604,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 f3b2a19eb..dfa3933bb 100644 --- a/crates/typst-library/src/model/footnote.rs +++ b/crates/typst-library/src/model/footnote.rs @@ -310,11 +310,9 @@ impl Show for Packed { 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/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/outline.rs b/crates/typst-library/src/model/outline.rs index 0db056e40..1214f2b0e 100644 --- a/crates/typst-library/src/model/outline.rs +++ b/crates/typst-library/src/model/outline.rs @@ -297,7 +297,6 @@ impl ShowSet for Packed { 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 diff --git a/crates/typst-library/src/model/par.rs b/crates/typst-library/src/model/par.rs index 8b82abdf7..0bdbe4ea6 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, + elem, scope, Args, Cast, Construct, Content, NativeElement, Packed, Smart, + Unlabellable, }; 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,7 +159,6 @@ 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. @@ -118,23 +170,15 @@ pub struct ParElem { /// 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, - /// 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. #[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 +187,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 { diff --git a/crates/typst-library/src/model/quote.rs b/crates/typst-library/src/model/quote.rs index 79e9b4e36..919ab12c7 100644 --- a/crates/typst-library/src/model/quote.rs +++ b/crates/typst-library/src/model/quote.rs @@ -212,17 +212,24 @@ impl Show for Packed { .pack() .spanned(self.span()), }; - let attribution = - [TextElem::packed('—'), SpaceElem::shared().clone(), attribution]; + let attribution = Content::sequence([ + TextElem::packed('—'), + SpaceElem::shared().clone(), + attribution, + ]); - if !html { - // Use v(0.9em, weak: true) to bring the attribution closer - // to the quote. + 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 += Content::sequence(attribution).aligned(Alignment::END); } if !html { diff --git a/crates/typst-library/src/model/terms.rs b/crates/typst-library/src/model/terms.rs index c91eeb17a..9a2ed6aad 100644 --- a/crates/typst-library/src/model/terms.rs +++ b/crates/typst-library/src/model/terms.rs @@ -8,7 +8,7 @@ use crate::foundations::{ }; use crate::html::{tag, HtmlElem}; use crate::layout::{Em, HElem, Length, Sides, StackChild, StackElem, VElem}; -use crate::model::{ListItemLike, ListLike, ParElem}; +use crate::model::{ListItemLike, ListLike, ParElem, ParbreakElem}; use crate::text::TextElem; /// A list of terms and their descriptions. @@ -116,17 +116,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 +147,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() @@ -157,6 +165,12 @@ impl Show for Packed { seq.push(child.term.clone().strong()); seq.push((*separator).clone()); seq.push(child.description.clone()); + + // Text in wide term lists shall always turn into paragraphs. + if !tight { + seq.push(ParbreakElem::shared().clone()); + } + children.push(StackChild::Block(Content::sequence(seq))); } @@ -168,7 +182,7 @@ impl Show for Packed { .spanned(span) .padded(padding); - 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/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-realize/src/lib.rs b/crates/typst-realize/src/lib.rs index ff42c3e95..754e89aac 100644 --- a/crates/typst-realize/src/lib.rs +++ b/crates/typst-realize/src/lib.rs @@ -15,8 +15,8 @@ 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, - SymbolElem, Synthesize, Transformation, + SequenceElem, Show, ShowSet, Style, StyleChain, StyledElem, Styles, SymbolElem, + Synthesize, Transformation, }; use typst_library::html::{tag, HtmlElem}; use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem}; @@ -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, } @@ -575,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; @@ -650,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); } @@ -661,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); } @@ -676,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); } @@ -703,7 +716,18 @@ 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 { @@ -722,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); } @@ -729,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 @@ -750,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)); @@ -794,12 +824,16 @@ 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 realization. static MATH_RULES: &[&GroupingRule] = &[&CITES, &LIST, &ENUM, &TERMS]; @@ -836,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(), @@ -914,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. @@ -936,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) } @@ -1277,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-utils/src/lib.rs b/crates/typst-utils/src/lib.rs index f3fe79d2c..b59fe2f73 100644 --- a/crates/typst-utils/src/lib.rs +++ b/crates/typst-utils/src/lib.rs @@ -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`]. 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/tests/ref/bibliography-grid-par.png b/tests/ref/bibliography-grid-par.png new file mode 100644 index 0000000000000000000000000000000000000000..5befbcc54160ec62b898eeaafc5649464f416d46 GIT binary patch literal 8757 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 literal 0 HcmV?d00001 diff --git a/tests/ref/bibliography-indent-par.png b/tests/ref/bibliography-indent-par.png new file mode 100644 index 0000000000000000000000000000000000000000..98a3c4d049b1759d06ca051b14ec394f6a6e7692 GIT binary patch literal 9087 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 literal 0 HcmV?d00001 diff --git a/tests/ref/enum-par.png b/tests/ref/enum-par.png new file mode 100644 index 0000000000000000000000000000000000000000..ca923a52623a0bbc725b1b61526478894637f8a7 GIT binary patch literal 3521 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 literal 0 HcmV?d00001 diff --git a/tests/ref/figure-par.png b/tests/ref/figure-par.png new file mode 100644 index 0000000000000000000000000000000000000000..d70bbcb12970dcf6e0b3f228544b2bbdd99cb3fb GIT binary patch literal 1701 zcmV;W23q-vP)66u|orXqvvLCDznXL?}q0$XXEOfdY!qS}Y2ddQnIa2_!^R1j?3}Qe;z>7a&lV zL>3DqMG&woLRl4|3KYs#R@uP?u)jFT4Z+k0q(`JeZ6itGC9S+IhdJkMv@@`h!7 zK#R~ZHtM0H^AU>j$5Od>uNv+wLBEj*FAfeiz>&EpBg($tJ$fX% zK;u;%G@_}g$#Z#md2nzLWqf>GF4KH zSXe0D`}px=kxBEOK7Fcm^TowQ+DRi-0`!g@J2EpfeSCZ#K72^$&CSipEGQ^QPfw>a zv9YmzeSHoN4p*;U9T*tczJ2@9&=9(TfdLS4TWBUEB%C>O27Q&~OP4NTg!tg-=(wur z!i5VwO-xL{x-y@Zq^72NdwVkyd3kwPu3Yi-^lWHoV2W(rx-}*y=G?h+j3N*Z95}#? zX=`hv8UFtM{5L*6J|!h3GBT1Uqhe@iNae7wFbfL{Rq<%NucD#?j(G8&J$uM>adBZv zR##V_J$qJg$;rto zO2QidU0GymYAPHV$L#O#=a0d`!4VM=WC9KkMbXpK!-Y?uK25|UdFRfZz`#Hzfti_^ zFtTmiHhRPWl$Mr~w6U?FR8>NUa;gGOcovv0U%uSi+bcldxN$=XT2xc^>eabP6cXIU z-&aX^csPzMh+}Z&+tt+6l*z2DthBT=;?Yc_7&$lMf36b%gxngt8onXL%UOFm)2!^4AkLT6Yl$yZlb7tzz# z*OxxCbzyZE0v|LuCC|LpC@h*$DEv; zm}iVPnT)Q!zW)9D_w)1fiE`9r4iP$9TU+r4UdIe@b8|~hPNvx>Po5-pGieB1j40e| zYHHpwXu6F@)YjIDWOH*fN!*a|nvs#glim^sDQaqJVnaqipi7O7jZ_ocK@oO9ztSuQ ze}5^MQm$h{s7ZYhOo&d{SqYj^ffPW|8OZYTa%Mes2pWVATo{`aiB*v#ZzqQeyh1dR zzl*h+Ooo4CWCVq7s^|v`ag9Epu&}WDy-)$s($d0D-?uMAmXWx)I94iRiz-lN9-WPSYF^~>!jiypWGttI)4^&N|>kWVLo8IavTVVgGJga5vIDL zd4X?BClC=QKr#lv=U-HI+^r%o69;{YY#4y?$=>ZRje*<4;jn)m)=#zW*fovcb=}en zoIm8XOJr(5UR{!5jf(?in$c6&+d59~z6I$gH8R24N*3S&<%8ABVUPMkT_gLgX@gbp3 z>jzsU6j#d@f!|sWZhV9xuuo0^qp|{a%C)Z}1Df?H`(%t-@u#<_0C&|Tu-jm(3Uat7 z2Zq2eoZ{#w)Fg6_V4YZ+6Eu`{9FzJ9|Ei^J+mQO tiEHTonGp&O6MH=LK{x-g37hcmegYM2&@e%Fp(OwS002ovPDHLkV1m2N1W*6~ literal 0 HcmV?d00001 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/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 index 753807db2..c12d2ae2d 100644 --- a/tests/ref/html/quote-attribution-link.html +++ b/tests/ref/html/quote-attribution-link.html @@ -5,7 +5,7 @@ -
Compose papers faster
+
Compose papers faster

typst.com

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

— 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.
+
… 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-5503-enum-in-align.png b/tests/ref/issue-5503-enum-in-align.png new file mode 100644 index 0000000000000000000000000000000000000000..4857e731bbcc4cc780e07103c6bb34d9dc7ef13a GIT binary patch literal 421 zcmV;W0b2fvP)Qf%f!~xC`@+x=CpK*OY*d;ifjzHKAc5T~=EoJQ z%E5>(GUz`#!GjOu3Ailv)iEy+h8bqKOkvH$gY%BQugGA*rc~p+)0V}XxWUeyd3ToL zN4dNLjU=%54!nqPJ;3{Sbw!~yjRr5)Za`mBb|=u zb^-ueRHpRzG#~^MOfbPy85Ry-X9)A!D|ykbC}9Yj>+o+d(z=p&>|kJI38L_N!=edh za4>xc;*F77#|%F0>K~tk_jIy?O^dVRLnr$H0F_(s9Rw@bWwZCf!2hi$Z61QSd! z!BY`-H3wyZ>$Q)!$pX81ZGN;>4Jv&Iru;X;xrVe)62o}7q#~N?^??Hhb8AluiZ}E@ zY8d>VT%_~`xW6l?tBQsC5~PO5G5!w*g)p3pa&=+t*aDQ5NDjB1F0vrwWf+bxJrin6 zH6)q$?sl_$kM#xe3<#gRd_)!)9?t}AHNgZE{G;&eR1Z72t7@_l_mLUQZBUAh;PN46 zaFJ1j2To{yWd^sjH;C}=E+jC6Zz+@Gdm^1+0mDZP?J-;$000y#+W%HFgZ(#aKRp@? zchjRML$q836HG9{QxA4J?DD|Nj+Dv*4{9d6_IZ^RXs_@JHP1Ah#Bg7xLLF`8syAzz zp4M6p;#87}mly{4-=l`U0QGzE@M;{?7XgW30m5GhMc{l?=T&O_3vszja@evZrw+;X zFubUAkN-|e)g1G>)zjkR3y!A*WP$m%HXm&@!2}chgK)pj$`I~u6?3Q8zz(jyAf_`J zU0r1L#mpGo z)Q1?c1P-=ryM`F_Ct`U;&Z~jk^c)NgE8Xe;0veVfsgZ!`MiN_ zYwM04kC*rQ*bYcAc63-dA;)rcYnyo5YJv%#Hn82s6n-k!nM{VTko%k14^A+>@w`bm zO|`=<$B)ksR*dEuWr4F$S;h{Y1=s=r08={33z@;Xjdzgq766cah&!GUq~#)*V1fz$ a_Iv?W=#otA7_LeH0000dusHn%7(I3M7Ic3M634YCwP_q6h+!ESilrrcUS`HO;l$w@hn4H`6b=uqP`XT!q6tgrF$ z@ws*D)}cd(nm2DQ(8aW2!-fqUIPmH{%gD%xjErQE`8#UVs4-*4tX;d-8uWn!2m18s zGi1n+4I4HHbpANos8OR4BSzQ+osf_)>7MoN+t+~Z)vFgAXU&>byLN4B(7wLDglukx zqod=E8#e^{FI%>3*|X0+>*eKT6SU<9?Af!&fcE$ICsH3jempBH%Nlgms#VSP!NI}% z_wN_zJT`y+{A0(CS&tJ0`t|E){7suS1qKG8Ybjmo8v6PaXaH_ix|6J#+cerAz7Q=>rA~;OBAc)~)T@wIhs- z8#k^)hYkejbLY-+ZwTVMckfn=mI<^#%LH1W1-j&n?8oNKn=KzNT)41q-8zB3pYev` z=jS(b=FFQnZ>FWCHEr6I=Vjjdc*yJByLUm~;!%(HOv}e@+qT);+Y59d*0yciY15|R z9655Ne*OApkaux#fYVq;^cOqpWyEV-d;*RBm7 zJh*%J?vP!uU;!_LM~@zji;Lsz+_`g8Qc`f3?+X_$1g-^iOiT=+h8yGR>MGDV*y6>D zpL_1Pg8sofckVoS@??@5WRU^_0&q+q?%1&-&c%xt$#vjxid1Uu&|zn1r_hndm~@~d zD`fL5*^F}K%8~cL=eLoZ2F~8Sdri`A z0$sR~Au&YM+H@2aZ(;KHxCFDa{x9*#_ zvQu~OqV=x2Ko`@74fPo@yuLxUXLn3wWEJDzy_0tJN-N|0`1}LVhyIAQfWCe$`P$W1 z0&T<2oH;{Q+9v3@ID6(YN3Wh92K4@YEz;B7z8llvt6m<~pr`y0@Xgm=BS+Nl(cN93 z^VpUxTV8nKh4Aojo1ljb_9g^#Y}*=bKqn{HU>x-E@%GlBU0utVxm>$;nKN050{xd! z2Ps{;G^M{HhK`Vsid!}}#jzPW%9nqMz=9JPSpMjdmI9r}Y{m`RIZ{#_)6!}(odNg5 zWPcZz(&x`5O`7Nj@l6|}mMjkAfuvfsSD5T`e+}{SDo1Gk;tRKK-Rkn&7!y-np`%Df z)oR7^2##}SKLXkEWf3b^L>$=P0-qVZY-xD*&6FdD;}`@qPkuP3G84?)=j@%^X#%Ym z7J*h$NhZ(&EzmN77HFA3KlPyX5G~OEN1*9E@6f@eV@KCP1HBUy9WGz`SfER4M)&^8 zlT0bdND}SZ*<7GY3iRMX-lIpo2eRL{MQ5H&=-cOA5_Kj+O;&I0cMX4A6KN8A6jsRI zQFA$a<|Do`d6K_CKN#qQ1P997UAnjdE-b7PBN>~bR;_GI-kFg$ZEF7Vb8yz_#Hv+a zAvec%@A7}g&L|DgRUK4Eux~NYb zlmV%No9Rq#B`BzZ;d}d5YNt-FRJ$p4LwWkNAaCz-`}W3s{<#a+pEw>*U6HD;Kt+;aP#PRQe0V|M8asAuP*6~A zJ#~9H7#U4~2Z1y=iI5^M(Ibp&C zI{K$ipN<09k|j%sAxw6ZX^`C>lc)~qSeIYxN{Sz`|m z51VHZOIo^gDYKY6fw(-HKM2CknKK6^dd|X;FB%0JggbC7plj8tB?X#1cIC>IPMbkf=d)Y5DTy0xc70ftCrhKnt`?paohc z&;l*cT4(s=gJy~HsFCl{5=Nin&vSwWx|n-q(VF!B`xQ=}h)1xXefwH;GNSyGo8y(s ztsER)V^KH@@S8Q00!?v^wyxpB>KkNq-9!6xWWKXqQbD2xLgFaw&-=r4P?e>=JX z8Z*|QQSIO_8WJ^XypHaYK2N*V~}M zTigkOF49v+|9{(n}~QBW2j>rXL-cp}gb3%YXUQl$2c z&(GtI?agoy^(Shxnux_Lrc5r-4+WaqHnZ4N2oE2MW1Dc+1<_6L@y9hdMJ464&uUR8 zWX+a9>xD(21zIN10xi%&AkYFW6KHiR-R}@+fqvYe*=dEs75(INhpt}Lm|Q67I@(8Z z$ZQk@^v9_IMxVwU=;0+N*AVDJcC$d9YTEbm4MrBtlJdbj6Ae?Bxc)aH*N5rdKnD=L2dC4|&7s#FvKSmwqm3eJMqNQVUklxOV) zjsa~Ez~k`r)QrAoY$jH=Y}rpf`NSq@bOjR=9aw|KPa+|k3L%5@=7y5^$6;+Dr4|F4 zK#?0XbDV%9(0PpQuL^p+qlSXat68hU(jXRbn&8V!M--Dy!T!Sg$6Bua?L)H`k@cPUEY;%P zZ~;G(=n!f~>xD(2l~j@mv_K2AOrQl?CeV-9{tF^GX5k&=%+&w@002ovPDHLkV1h*F BaoPX? literal 0 HcmV?d00001 diff --git a/tests/ref/math-par.png b/tests/ref/math-par.png new file mode 100644 index 0000000000000000000000000000000000000000..30d64794cb9fbcf1d65e0b3f04b47c5e3ec2e776 GIT binary patch literal 387 zcmV-}0et?6P)Nkl%xiej^)6-?EkuAO!@pBuh#ebKqyGMe>-Z%fa1F27KZAH;-ZFhi7$AABemuYsH@T~t=K^C8CYjbaF>-qQpf9?MNAtYFA_5WDqWfUJf z{`nty$mQAp2gp8t>G&TcfbjA3j{lcXeQdgFXD103Putp4a16!blEsso|6k79x&tMS zr*&+-ifr-El1)tpw~#H4IrjfHS*iNv2^i}ign)pJK#a`4au>?@3l>7CcwW15(`d;x hYVoMWqZSVnive`)&pxRUt8f4S002ovPDHLkV1kYxxsw0@ literal 0 HcmV?d00001 diff --git a/tests/ref/outline-par.png b/tests/ref/outline-par.png new file mode 100644 index 0000000000000000000000000000000000000000..04c63f62c4827dd51999a285df1715a094520101 GIT binary patch literal 2911 zcmV-l3!wCgP)Gs000XnNkl^m+(@f)kYS(Vk{C-XO8`EAFyY;QT*0<+-_q*Si)jgOB{N=j;JY01ye4+{(H?Ci97$>-DY!m32nHhU~`-+MR8ChCd0(Z;S8y_Ecc6J^c8?&rDHa0dUCdLMVHW&>4{{BGo z^73LDZ)+5Tr*VosJiwhaAudlbWvzwThfY&le<|jp>MDmy4 zAbgh4YPAUo2_(zl+1c66&CRa|dTniuZ;OkI`&&YPe}7O=P)$uuczAeDPR`@wBf)}# z0w*V@#Kgqr=4SSZH#9Xh5ucr%jf{*0XjG`EsEEk@{e5zBa(Q`qL_|bMNeS>REGz^E z2UF7B-JPDEjuRLdcy)C}a&d7nwcg&|$k)`=6v?Tnsl11U_yNBg=&r6VJ~b;V%k1pt z<|e9AS67F>xVQ)kd3kyGmzS4R1q1}Zbf9-}aREC~LNaOd^Yh{{H#avlG!&<`wUzkj z=qP?^X(@hVVkDe4-XG_cXy%Qr&*ANQyhPM zsO|3V4!I-{W{qu-U>zJBKvJ}&y}ex~e6to~SGKmc3JVKq7#y9Up&@GqTKo01hld9@ zIUHmc5fH(RjSYCj2WMtxA`$SKAe2N&)6&vF9I6rlM3MjS@W36MR)+83;J`>GBiBSP zFRzuAm64GV`XN`5<>h7KvRu?MvX^3CdAX>?s;jHnegp(I&`UH{ZXe&Uo}M0#Fbo3j z?CflMm%M}w4-cac85tSqcuGpj`T02qTy!n^AzC~zFu-Q;D0FFu6@)7Q{6I|e6F~r2j2#Q&J(+e4baD04Bn!LkN$Rqho!ESs;;_^O6T=?)E{rSVb zxwEtt#$mimi_{J+2o_vQ{Qxd5;^5|{vv%mxFJLKHOBcmm1qBgw5k*k3f`wABR1r)h zc#XMuX>3gDC6yR!MOzU1g9l#1>4lPGw*McI$8(aClgH$}{NCr~l(7ygGun(cqpi$n zGurCErr@W!?-D}Nx2*nN5ANS9dbckg{|vX zoCgv+LKp;TES{jg)NF<~cK~{dXudh!B!Bw-fv(lut;Xl$! z9D?b2X#j{XsI9w^DiIbD;&dw+4-Q5I#dLE!BN@Ox8iNt5=?d{!cHs^V1 zZf0X+Q~L1mm|K#1OiJOvgsBQs-dC5 z%*`*th?SLf?3$rPXB~VgTw+Gl!YnI_(QF-L&t-Raw<0;@cigF!l@*6t z=n!B}4i8C?=z#=Dvvt|+a|YJ%Ezvej7>z?hY`2#l9UbN6Tr$XAZDL}=p(}WkbZ>7j zJVlEg0|Ns(2&-aWqV&kf2v-x;aaUszV{&rRU5%zz8WarG($&~qse_?V2(ILCmG(=k zcq8;-kTSm;I)zR^DpOO_(P$j)ev-f1J7X3#f-np>BKRk)#llj15wRC-w6L+XQ%fs< zfQ^4au$8;S>JUxf^h~fkQeBerkn|QI4;fhGERw)Z6P~wN#w`YUSccti-g&>T-;+Cs z!|{Cn5ylS>n=p8NJwH8tN(xL#S!%P{hWZi9(suhH+89!f>YY!sMoXFDE0hcT6=>)Dc`()Dc5&tJm$@$o=iEP+7b>Hhxx-Q7DXYksi! zWhiJRXhADM3tG@h(1KQi7PO$1parc2EoedCioL65v$@~zr4|=7z618v2_}_VM9^5| z2ZKQ_mm{(n1HIs4fhK2{onxYQ@!k=SBDg4^NxdMrm;v?yK$9;oxPR4OSwnF+@Hmt9 zgGYj&Jkgxe(G!z$P&X?wttQBQ`T1K!z4K7xDfJ;udFkK)+I6>^^?D8HYPBl6G?xwn zqDG?;FB>6uKMCo_Wy7&^hTacCYm9nIUmo zA%1c&(OQnwBsqm&@tyljCl;<60~hwF|`cb%YMHEYp&T z3%Xn`bEQKbt?YyqH;%2->99XH_f*WtV#8Hoi(K0ZWCKY+ehr^ zr_)JxQN9czOQ;~-H|ferTS~$C2`*?sD?tld30lyCR)YR-^b6Y|-l4spLNWjV002ov JPDHLkV1nq{iBA9k literal 0 HcmV?d00001 diff --git a/tests/ref/par-contains-block.png b/tests/ref/par-contains-block.png new file mode 100644 index 0000000000000000000000000000000000000000..f4bd071f62fe2e7ee6eee07dff9f8d71301dbbc1 GIT binary patch literal 426 zcmV;b0agBqP)8FMq|`en-UFzBm_1^p%g^~1`R<$MM~rgVVFjm*_(z*feB$5Mz|QX38iL& zc~A<4$S|@X(2`RguS7Y4ON$4BI{)>0;0Moe*Ws6{V__C%;lB%Kj;W{*KlX8U2f#0D#Aa!ofh*Q$0Tu;95WkLp0of0$|2iJB;NGAku?!ykcUoY9t#!p|T^p z20D!gz`ltA*ksD(f?|+ntG@!@>P$h<4SS*LhT|c-U~|H%9SxRXx4nxISYo&9#89uh zS}U-CBAZEiJsrs#IaxiV7Z9IcbKjxy#3!-K2)qRlVhAo%Pi45+1PGJ#T4>o$s|`+c zB%$4jOHhIxbIg3T#sK&`F}sR;aNeRK@WbF0KsP>yoBBLdD;SDh^#St3P&+j*s~t`$ zlxb>dD)1`T6X6NBO`3RKw8ZgUBnBJ-lx5Qk^EC0I&|**NGUV9H#lkGi!v6#Q0=gDM UEs$${_5c6?07*qoM6N<$f~DBB3jhEB literal 0 HcmV?d00001 diff --git a/tests/ref/par-contains-parbreak.png b/tests/ref/par-contains-parbreak.png new file mode 100644 index 0000000000000000000000000000000000000000..f4bd071f62fe2e7ee6eee07dff9f8d71301dbbc1 GIT binary patch literal 426 zcmV;b0agBqP)8FMq|`en-UFzBm_1^p%g^~1`R<$MM~rgVVFjm*_(z*feB$5Mz|QX38iL& zc~A<4$S|@X(2`RguS7Y4ON$4BI{)>0;0Moe*Ws6{V__C%;lB%Kj;W{*KlX8U2f#0D#Aa!ofh*Q$0Tu;95WkLp0of0$|2iJB;NGAku?!ykcUoY9t#!p|T^p z20D!gz`ltA*ksD(f?|+ntG@!@>P$h<4SS*LhT|c-U~|H%9SxRXx4nxISYo&9#89uh zS}U-CBAZEiJsrs#IaxiV7Z9IcbKjxy#3!-K2)qRlVhAo%Pi45+1PGJ#T4>o$s|`+c zB%$4jOHhIxbIg3T#sK&`F}sR;aNeRK@WbF0KsP>yoBBLdD;SDh^#St3P&+j*s~t`$ zlxb>dD)1`T6X6NBO`3RKw8ZgUBnBJ-lx5Qk^EC0I&|**NGUV9H#lkGi!v6#Q0=gDM UEs$${_5c6?07*qoM6N<$f~DBB3jhEB literal 0 HcmV?d00001 diff --git a/tests/ref/par-hanging-indent-semantic.png b/tests/ref/par-hanging-indent-semantic.png new file mode 100644 index 0000000000000000000000000000000000000000..e05795c7f2f128ec72ac6e728b58dd09c86120df GIT binary patch literal 1594 zcmV-A2F3Y_P)xYL>H!bzfj#ig33fPe*zX7Y=~(gWG`>&9E?Hm_#d5w+Cx;%B2i_&ZxLt3R4RE$% zZ325euA1TY+Efb=D>Oz~VM|cr6EiG?4;vFZT6!mXoa-S{eFmuf#Y_i=CC|JwYA4wf z#dFJ@a>7R*G4BwkaSr=ctI6A|)j8VB34p&i$!t-Rg}k-vcn zgCJ1q2H-EEmA{Kz#!7aS;!PsthO7J{4v56N!{bE$R=V+-C=S ztsTJB;rS#yum}FJ!WAE88Y&~>^>Ft}G9ugLhSMJ|n)Lf27k&Nsb~gH@9V4n`hFgZN zqH^RAnkwjD{nT*!rw}#r!8QQ3lmb5E0B{0;12`w*=zkO}ADp@Rg)x<6O_C-n6aybp zoTPjH;wgKeP7yMG-_04~@wv5Zekovu2~uBqbGU46-KL zCA{oQme>SoQsY@EmOTT%Vy+|?Oo#papg;D58>C5VgT@vYi49mg(TD^H@LC%>b^{Y1 zhG6;Ntusyxf1)3(5vu^w#cF_KVk3Bs05D13a>4~t2Ht;wjncod4}KJ?20l_l-i=ae zR0o&Qh<|{;bdbiA_)2&Ue?Ge)L4VmOJ`z2M~AIU0ClRUb`^H3k-1{WEaSvW|d8N)agUlpC z^uXV=%Wm9;=U#%cK=>d0;uX-&`UZ~5W{vX1J;T?%s-b(j_PKp!<9fHOv^<{)Z2`pr zhc5V;lb2c!&$)cgK7H?rmfc~+%9o1HTl}&jC)R`J)zTF
FfXSk?93zX}=((4Ku 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 LNkvXXu0mjfO9jX3 literal 0 HcmV?d00001 diff --git a/tests/ref/par-show.png b/tests/ref/par-show.png new file mode 100644 index 0000000000000000000000000000000000000000..1ceb26f71142ca0b0edd35a4da93a41fb252e271 GIT binary patch literal 932 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 uG6l2(S^=$00j+>mrhryJE1;GAFZ&xQWdS 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ð3J!Q`ss(-k-mX;;$ zv2{brkd`B)S(g=Qm`x_p^xJf~q8wBXjjg5<$!(lR7Z%vHbRa+jVZIhc^TogS_0cvs@#ZjVpN*Ig!k_e=?3}O2k$!sn!oR-U0 zVUBh4=6L2If~bcGT{g)*cN&xP*#Pj=7~xy!?&ug2jnNGK90;3x3*2`JBYWgdHy|Dx zCex#1TU#fPY5dhP&p1_PQ{>*g(DplMrcU1_1W+^c?^3OIXDZtH11}2v?Y02PY(}x% z!~;pHrCV4s!A?uCxOtQ}iA}Ptq<5wU*c!(ZX);wDG32T=Dl`PfsZE|OD$kg=LfRSK zt9%L}tDiD^>mgdTJn^MjCW$}Cc(gV8pd4gvfgXN^qZIzCsve*+C&O#w+JC*T`5UX)+3_AbwP0;;IyyR5x8333eSExO zwieEF2i$MX_UQ|cZmbU5j5vZqyI?4LxRf!zxyrulAX~(O+&}Yb40X)9-EGx;y;A{R zu5?A_$=(b~LBGVC)1%&Tl+J}fD*;iURg4a!DdWEok=R?~m={FvQZHSS7gOqIy}@X(Jth(T6jOJT-XK zg!Sbbc#7Y!vcquSIHt7BG`JVoDgv-AK!qc!GymAvD+Uk!bRpLYnUk{PAP&Expd&1k zV_%B?H2H+V7Y?$#c5R5|>6;y!W{D8U@>?+5tX z73-1&$P%6}9mRkQ=S#d-eea9A>`uM~h*Li5z({GfvX@zqH~u3}|E9se-c$f}*{OaS zZ?Qo7uk*{F{94{(YOl{JMsWqT!UVq+6?IH`F-ctF%f!u1{#N z)t^r;*tAVSGZJZSFZTBGw1TTXy~{MJDpZ*Ls;}*1lttt9E=tjhnEjW08*b88dS&EJ zBvE$xc__>e2#VKP#X9t2Kon^wtaPU#?T`+(TR5vc^%Weh=)tGO%wSIGYKh&Pk&Z02 z&6?*6O;tP-!N+U&;lZcuw>X|bAME;fl$lsC<`V$B!dS{G(n?;sh`arabrem&wH;S zOcg|i(^RGH);&Bt;wxMdfpDU7g13~=O-f30Du|hvr>B|A!&3@>n}Q-=5lW6qC1cl? zNPDKD1b-|EZN&MpDb7t&HOh9rRJ!e6lTzcLMTsV6sFP%l3_Xf++#Ug(U~W!>POF!E zS;4;d`EE@dJo&(TlLa6K1OHFc2_zc4kGvAH(6Jo`a$6#G!(`g(`f4?*k7ND=UUYA0 literal 0 HcmV?d00001 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/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 288392d45..7176b04e2 100644 --- a/tests/suite/model/enum.typ +++ b/tests/suite/model/enum.typ @@ -183,22 +183,44 @@ 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. diff --git a/tests/suite/model/figure.typ b/tests/suite/model/figure.typ index 58ba2b2a4..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 diff --git a/tests/suite/model/heading.typ b/tests/suite/model/heading.typ index 4e529fdf6..4e04e5c56 100644 --- a/tests/suite/model/heading.typ +++ b/tests/suite/model/heading.typ @@ -128,6 +128,11 @@ Not in heading // 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 // ... diff --git a/tests/suite/model/list.typ b/tests/suite/model/list.typ index 96ddf3c18..9bed930bb 100644 --- a/tests/suite/model/list.typ +++ b/tests/suite/model/list.typ @@ -238,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] @@ -262,18 +289,11 @@ 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 diff --git a/tests/suite/model/outline.typ b/tests/suite/model/outline.typ index a755151d6..49fd7d7cb 100644 --- a/tests/suite/model/outline.typ +++ b/tests/suite/model/outline.typ @@ -242,6 +242,15 @@ A #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, diff --git a/tests/suite/model/par.typ b/tests/suite/model/par.typ index 0c2b5cb54..84f2ec152 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) @@ -69,6 +168,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 +188,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 +215,26 @@ Welcome \ here. Does this work well? #set text(hyphenate: false) Lorem ipsum dolor #metadata(none) nonumy eirmod tempor. +--- par-show --- +// This is only slightly cursed. +#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 #parbreak() D + +#block[E] + +#block[F #parbreak() G] + --- 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 d0dcc55dd..51c4bba59 100644 --- a/tests/suite/model/quote.typ +++ b/tests/suite/model/quote.typ @@ -107,3 +107,14 @@ When you said that #quote[he surely meant that #quote[she intended to say #quote )[ 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 23ac6e513..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,18 +104,10 @@ 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 From 176b070c779ef8aa4515c8ff062b17ca9114fd3f Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 24 Jan 2025 13:31:03 +0100 Subject: [PATCH 25/34] Fix space collapsing for explicit paragraphs (#5749) --- crates/typst-realize/src/lib.rs | 4 +-- tests/ref/par-contains-block.png | Bin 426 -> 423 bytes tests/ref/par-contains-parbreak.png | Bin 426 -> 423 bytes tests/ref/par-explicit-trim-space.png | Bin 0 -> 215 bytes tests/ref/par-show-children.png | Bin 0 -> 920 bytes tests/ref/par-show-styles.png | Bin 0 -> 471 bytes tests/ref/par-show.png | Bin 932 -> 0 bytes tests/suite/model/par.typ | 37 +++++++++++++++++++++----- 8 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 tests/ref/par-explicit-trim-space.png create mode 100644 tests/ref/par-show-children.png create mode 100644 tests/ref/par-show-styles.png delete mode 100644 tests/ref/par-show.png diff --git a/crates/typst-realize/src/lib.rs b/crates/typst-realize/src/lib.rs index 754e89aac..50685a962 100644 --- a/crates/typst-realize/src/lib.rs +++ b/crates/typst-realize/src/lib.rs @@ -729,8 +729,8 @@ fn finish(s: &mut State) -> SourceResult<()> { } })?; - // 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); } diff --git a/tests/ref/par-contains-block.png b/tests/ref/par-contains-block.png index f4bd071f62fe2e7ee6eee07dff9f8d71301dbbc1..27ca0cf6b0cb79a4f2fc573d20054596bd009117 100644 GIT binary patch delta 397 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 N002ovPDHLkV1lSL(j@=@ literal 0 HcmV?d00001 diff --git a/tests/ref/par-show.png b/tests/ref/par-show.png deleted file mode 100644 index 1ceb26f71142ca0b0edd35a4da93a41fb252e271..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 932 zcmV;V16%xwP)Bb(Si*|f=W>9Q=hWGlQ@2_q?IkRcgr*pehq95_)yp&#O8 z!X-}Bew`Ny6;v84#T4~KoW!ODq z>=TEFrvsl_Y~rwO6L7+kCJmQOUf?EPVzIpKlZ8W(PyzxIkzhhU356BDV>se$Pes>v z_Lgxmc#U_9k2BVOAZcyz&h$vZ+rCIb_e%o+D;-#SrQogWW?zA~ZotJKWhbTJOpX?l zf*q8vmV!5C`Abvq$3Npvmx5`2!cF&-DqwK-@|IGE6#Vb--e#`$dw@W{=PF3SuC2QQ zTA2zftgynj1^?6dd4!1{E&{}>cCTCJ7lQK!&y2jwoQ(=V_R`}Mi zeOaZg-VE4y6n2^cGZk0d6!@{a7sDo<*q1N`eiHsXhdf-LR=s0xObbL|g%wu#Zij=3 zR5D`mcvK8-IK1Qv#A*T8^xVoB2`TuKjv%l-1E9c3&x91b>-jQ+U)wSOR@8Rbk%IH% zMRlV!V>jUHmc3$d&HSXr|WT$>*O8vx7- zLE@AUyt(EXH%p7Q0G@Qo9WUfWAu#s>3_Jyata(yb{j#e9c)c5Vt`!az3&H#~#S^gB ztpV`8W81gtCn30f(0-6)2mpwLK?okGwMVov6;@c`+nnp3yU8IFH8&&x0000 { - if bibliography.title == revoke { return it } - set bibliography(title: revoke) - let p = counter("p") - par[#p.step() §#context p.display() #it.body] + if it.body.at("children", default: ()).at(0, default: none) == step { + return it + } + par(step + [§#nr ] + it.body) } = A @@ -235,6 +237,27 @@ C #parbreak() D #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$ From 85d177897468165b93056947a80086b2f84d815d Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 27 Jan 2025 14:15:20 +0100 Subject: [PATCH 26/34] Support first-line-indent for every paragraph (#5768) --- crates/typst-layout/src/flow/collect.rs | 14 +-- crates/typst-layout/src/inline/collect.rs | 32 +++++-- crates/typst-layout/src/inline/mod.rs | 30 ++++-- crates/typst-layout/src/inline/prepare.rs | 8 +- crates/typst-layout/src/math/text.rs | 3 +- crates/typst-library/src/model/par.rs | 86 ++++++++++++++++-- crates/typst-library/src/model/terms.rs | 8 +- tests/ref/par-first-line-indent-all-enum.png | Bin 0 -> 425 bytes tests/ref/par-first-line-indent-all-list.png | Bin 0 -> 383 bytes tests/ref/par-first-line-indent-all-terms.png | Bin 0 -> 755 bytes tests/ref/par-first-line-indent-all.png | Bin 0 -> 1335 bytes tests/suite/model/par.typ | 51 +++++++++++ 12 files changed, 196 insertions(+), 36 deletions(-) create mode 100644 tests/ref/par-first-line-indent-all-enum.png create mode 100644 tests/ref/par-first-line-indent-all-list.png create mode 100644 tests/ref/par-first-line-indent-all-terms.png create mode 100644 tests/ref/par-first-line-indent-all.png diff --git a/crates/typst-layout/src/flow/collect.rs b/crates/typst-layout/src/flow/collect.rs index f2c7ebd1e..34362a6c5 100644 --- a/crates/typst-layout/src/flow/collect.rs +++ b/crates/typst-layout/src/flow/collect.rs @@ -23,6 +23,7 @@ use typst_library::World; use typst_utils::SliceExt; 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 @@ -46,7 +47,7 @@ pub fn collect<'a>( base, expand, output: Vec::with_capacity(children.len()), - last_was_par: false, + par_situation: ParSituation::First, } .run(mode) } @@ -60,7 +61,7 @@ struct Collector<'a, 'x, 'y> { expand: bool, locator: SplitLocator<'a>, output: Vec>, - last_was_par: bool, + par_situation: ParSituation, } impl<'a> Collector<'a, '_, '_> { @@ -123,8 +124,7 @@ impl<'a> Collector<'a, '_, '_> { styles, self.base, self.expand, - false, - false, + None, )? .into_frames(); @@ -165,7 +165,7 @@ impl<'a> Collector<'a, '_, '_> { styles, self.base, self.expand, - self.last_was_par, + self.par_situation, )? .into_frames(); @@ -175,7 +175,7 @@ impl<'a> Collector<'a, '_, '_> { self.lines(lines, styles); self.output.push(Child::Rel(spacing.into(), 4)); - self.last_was_par = true; + self.par_situation = ParSituation::Consecutive; Ok(()) } @@ -272,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`]. diff --git a/crates/typst-layout/src/inline/collect.rs b/crates/typst-layout/src/inline/collect.rs index cbc490ba1..14cf2e3b8 100644 --- a/crates/typst-layout/src/inline/collect.rs +++ b/crates/typst-layout/src/inline/collect.rs @@ -5,6 +5,7 @@ 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, @@ -124,26 +125,33 @@ pub fn collect<'a>( locator: &mut SplitLocator<'a>, styles: StyleChain<'a>, region: Size, - consecutive: bool, - paragraph: 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); - if paragraph && consecutive { + if let Some(situation) = situation { let first_line_indent = ParElem::first_line_indent_in(styles); - if !first_line_indent.is_zero() + 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.resolve(styles), false)); + collector.push_item(Item::Absolute( + first_line_indent.amount.resolve(styles), + false, + )); collector.spans.push(1, Span::detached()); } - } - if paragraph { let hang = ParElem::hanging_indent_in(styles); if !hang.is_zero() { collector.push_item(Item::Absolute(-hang, false)); @@ -257,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/mod.rs b/crates/typst-layout/src/inline/mod.rs index 83ca82bf2..f8a36368d 100644 --- a/crates/typst-layout/src/inline/mod.rs +++ b/crates/typst-layout/src/inline/mod.rs @@ -42,7 +42,7 @@ pub fn layout_par( styles: StyleChain, region: Size, expand: bool, - consecutive: bool, + situation: ParSituation, ) -> SourceResult { layout_par_impl( elem, @@ -56,7 +56,7 @@ pub fn layout_par( styles, region, expand, - consecutive, + situation, ) } @@ -75,7 +75,7 @@ fn layout_par_impl( styles: StyleChain, region: Size, expand: bool, - consecutive: bool, + situation: ParSituation, ) -> SourceResult { let link = LocatorLink::new(locator); let mut locator = Locator::link(&link).split(); @@ -105,8 +105,7 @@ fn layout_par_impl( styles, region, expand, - true, - consecutive, + Some(situation), ) } @@ -119,16 +118,15 @@ pub fn layout_inline<'a>( styles: StyleChain<'a>, region: Size, expand: bool, - paragraph: bool, - consecutive: bool, + par: Option, ) -> SourceResult { // Collect all text into one string for BiDi analysis. let (text, segments, spans) = - collect(children, engine, locator, styles, region, consecutive, paragraph)?; + collect(children, engine, locator, styles, region, par)?; // Perform BiDi analysis and performs some preparation steps before we // proceed to line breaking. - let p = prepare(engine, children, &text, segments, spans, styles, paragraph)?; + let p = prepare(engine, children, &text, segments, spans, styles, par)?; // Break the text into lines. let lines = linebreak(engine, &p, region.x - p.hang); @@ -136,3 +134,17 @@ pub fn layout_inline<'a>( // Turn the selected lines into frames. 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 e26c9b147..0344d4331 100644 --- a/crates/typst-layout/src/inline/prepare.rs +++ b/crates/typst-layout/src/inline/prepare.rs @@ -85,7 +85,7 @@ pub fn prepare<'a>( segments: Vec>, spans: SpanMapper, styles: StyleChain<'a>, - paragraph: bool, + situation: Option, ) -> SourceResult> { let dir = TextElem::dir_in(styles); let default_level = match dir { @@ -130,7 +130,11 @@ pub fn prepare<'a>( } // Only apply hanging indent to real paragraphs. - let hang = if paragraph { ParElem::hanging_indent_in(styles) } else { Abs::zero() }; + let hang = if situation.is_some() { + ParElem::hanging_indent_in(styles) + } else { + Abs::zero() + }; Ok(Preparation { text, diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs index 5897c3c0c..9a64992aa 100644 --- a/crates/typst-layout/src/math/text.rs +++ b/crates/typst-layout/src/math/text.rs @@ -107,8 +107,7 @@ fn layout_inline_text( styles, Size::splat(Abs::inf()), false, - false, - false, + None, )? .into_frame(); diff --git a/crates/typst-library/src/model/par.rs b/crates/typst-library/src/model/par.rs index 0bdbe4ea6..cf31b5195 100644 --- a/crates/typst-library/src/model/par.rs +++ b/crates/typst-library/src/model/par.rs @@ -3,8 +3,8 @@ use typst_utils::singleton; use crate::diag::{bail, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - elem, scope, Args, Cast, Construct, Content, NativeElement, Packed, Smart, - 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}; @@ -163,16 +163,56 @@ pub struct ParElem { /// 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)]`). - 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 that all but the first line of a paragraph should have. + /// + /// ```example + /// #set par(hanging-indent: 1em) + /// + /// #lorem(15) + /// ``` #[resolve] pub hanging_indent: Length, @@ -199,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/terms.rs b/crates/typst-library/src/model/terms.rs index 9a2ed6aad..e197ff318 100644 --- a/crates/typst-library/src/model/terms.rs +++ b/crates/typst-library/src/model/terms.rs @@ -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] @@ -180,7 +185,8 @@ impl Show for Packed { .with_spacing(Some(gutter.into())) .pack() .spanned(span) - .padded(padding); + .padded(padding) + .styled(TermsElem::set_within(true)); if tight { let leading = ParElem::leading_in(styles); 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 0000000000000000000000000000000000000000..38cdea7926d11bcd7f1071bb100779305541f432 GIT binary patch literal 425 zcmV;a0apHrP)YUQM5#m4D+#4`&(xX%!KXnuc;8zncdO-lY+aeyRNC3y4gQF~f~XIF_gV1n|qq556n}@XRTI4+41Tp6JF?+eC1f$d*ZqkT{Z%nAQ0}X>tO>yIl_=Qu&;Uzb zL9p{NC)dLRMKN&5yg&ZEmQi}RGyiVifEf7M=Q~8Gpmd*iFOcHpVwhoun-l&8!d`<7 TAPREd00000NkvXXu0mjfQTn#b literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..cf731e79fc8a549a581f4b3bc9d6675fa3f1837d GIT binary patch literal 383 zcmV-_0f7FAP)SsPZ8rVm_(t*i|35p*?vEBAqZZTD;+I>u9Qra47XQhuedKa( zAcA`G)c@tX2E^jzTJJvtf3#Q&jZn2~!5Z;?LKv-TpUPat%9+udJXQ zz-#g9aN2>)m?~2E^hIVF}x+o9JpWqEGyPViO%L{()ri zFID0008INkl^KoV!5~XFNjB_v#$Z10T){|8wy~fKm|* zW-x;pECYN=l@@pRvK}G(cw$95)W>UUjdH?9=!kQ33L#U)R6r^o&f{{zfS$O4DrKp^ z`cy~y(Ag(hse>802)0p~mk6n&g7&HWur~Qa_%00bjYhtun^yz2;s&@M@y!r3@J;$zHb5Bs zukf61WP=8*>hZizp#Yoi05DR3OM582<9p4N;NkNCTmcFnS53}j&j2XE_&MullZ(OO zhPy7dR2l*lU>pJTnE?uLB7o~=N^rwNN^tQY1vnO1zpz8$t3KD%Q7f6o7HMvqXAFs>+j45==`0mRN%wL91j&Z!O`la^{8EI6ks9RZoer|fX90K z`uf@AVlaal%-~%DKQ6ytSeg4a#G>!zg*8>R(pjIb%pfbQL@QaH??i~6k+om>fLOsK zcG7)e*lC+qzr$GFm-dCm&xWq;_7MQ+Aihm6#1_*4*gf*YmLz-B!gH>ya3zv&<7(7< zwne!5%p%0<>H)Xllpns4tSS0x=A7bA8!;`Dr6A1trUi&;3IOZLHhJL}WCK|VTR#L- z5Z>SIB5yvCc>rz1Ev>N~TLrGYmld{MRXo5-QcSitU!lQ(;sS6oVCIrN}B@%mLqWzxd&GYJ$JbBLl z#WM?Usaa0g2|M9;2_Cn9_GQDgtR<-cMKfGC+pSyYL!@AO37??#Q<`CX84Cyq1E$A! z1;#Lyp~Of7yl|UTnm|xEePrWfV`P*skQ?Xeio>6e@a86YZGw8s5jUZh4mdAG{SYBi676T*jV_&_17-qeU8vlTdIwAtlS@cAACKGNYlL1) zNCt@(^^#*dXt~v{+krHf-urWQ71R|hYjVk!d?48KVb4XPzn8e8AVf{R(oR}#H7BK0 zo5taziCvE2l)E{lkYu!+mS=G)%Q=;GoXnQx8|E!Ezl8x3numTj+gpIe+q1}L&ROkv zcocxJfJ&9&^^%=%5WKh(c;JV)a=c!)ZyW>HauR?6eJK-g)dM?=`T+E* zBz_>{(X+1D`^L=R<9djHn}QRoke?zuS@SdEMKBy{TmjL}95JNPKx)N~7G((Z46J~C z>0`VADC2Lf1uqvhkuyE&r@sQW1)7H*VCBLNXOC`C?v|7VnD!Lo!V%?y(Hw;asM!i? z^@w+kaxF5s<6l^*1>N4;fT?4(VPa#0730nqf%gDzt;e*am4l9j`Sa3#KZsWw zfXE*&5~dEV$7Gb&0WiJ-uH%*JnNS7cMccZj-SfD5N5G48#pd3i3f+!2SwW(<=x}0a zJAe~DH7ehvuvT0bBpQIOrD^2%+RA+~LP`EEgLiDJ1K{~pKN*ppZOg-&OOZ)E3}Zz) z05J-prlI&yKkw+EG`7$RDB}c=nPp`M tn~=l5g!{3*=h~f<%LzMSC;Se>{{X?#gsHQ~(a!(?002ovPDHLkV1nKZaaI5T literal 0 HcmV?d00001 diff --git a/tests/suite/model/par.typ b/tests/suite/model/par.typ index fa230451d..e76690064 100644 --- a/tests/suite/model/par.typ +++ b/tests/suite/model/par.typ @@ -156,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) From 9665eecdb62ee94cd9fcf4dfc61e2c70ba9391fb Mon Sep 17 00:00:00 2001 From: Andrew Voynov <37143421+Andrew15-5@users.noreply.github.com> Date: Tue, 28 Jan 2025 13:08:12 +0300 Subject: [PATCH 27/34] Fixed typo in the new outline docs (#5772) --- crates/typst-library/src/model/outline.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/typst-library/src/model/outline.rs b/crates/typst-library/src/model/outline.rs index 1214f2b0e..f413189ba 100644 --- a/crates/typst-library/src/model/outline.rs +++ b/crates/typst-library/src/model/outline.rs @@ -445,9 +445,9 @@ impl OutlineEntry { /// /// 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 with 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. + /// 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 @@ -461,13 +461,13 @@ impl OutlineEntry { /// The `prefix` is aligned with the `inner` content of entries that /// have level one less. /// - /// In the default show rule, this is just to `it.prefix()`, but it can - /// be freely customized. + /// 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 to `it.inner()`, but it can - /// be freely customized. + /// 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] From 1b2719c94c6422112508cfad24bdd9504541c363 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 29 Jan 2025 15:20:30 +0100 Subject: [PATCH 28/34] Resolve bound name of bare import statically (#5773) --- crates/typst-eval/src/import.rs | 49 ++++++++--- crates/typst-ide/src/matchers.rs | 88 +++++++++++-------- crates/typst-library/src/foundations/mod.rs | 4 +- .../typst-library/src/foundations/module.rs | 28 ++++-- crates/typst-library/src/foundations/scope.rs | 9 +- crates/typst-library/src/foundations/value.rs | 15 ++-- crates/typst-library/src/lib.rs | 4 +- crates/typst-library/src/pdf/mod.rs | 2 +- crates/typst-syntax/src/ast.rs | 52 ++++++++++- tests/suite/scripting/import.typ | 66 +++++++++++++- tests/suite/scripting/modules/with space.typ | 1 + 11 files changed, 234 insertions(+), 84 deletions(-) create mode 100644 tests/suite/scripting/modules/with space.typ diff --git a/crates/typst-eval/src/import.rs b/crates/typst-eval/src/import.rs index 2060d25f1..2bbc7e41c 100644 --- a/crates/typst-eval/src/import.rs +++ b/crates/typst-eval/src/import.rs @@ -6,7 +6,7 @@ use typst_library::diag::{ use typst_library::engine::Engine; use typst_library::foundations::{Content, Module, Value}; use typst_library::World; -use typst_syntax::ast::{self, AstNode}; +use typst_syntax::ast::{self, AstNode, BareImportError}; use typst_syntax::package::{PackageManifest, PackageSpec}; use typst_syntax::{FileId, Span, VirtualPath}; @@ -16,11 +16,11 @@ impl Eval for ast::ModuleImport<'_> { type Output = Value; fn eval(self, vm: &mut Vm) -> SourceResult { - let source = self.source(); - let source_span = source.span(); - let mut source = source.eval(vm)?; - let new_name = self.new_name(); - let imports = self.imports(); + let source_expr = self.source(); + let source_span = source_expr.span(); + + let mut source = source_expr.eval(vm)?; + let mut is_str = false; match &source { Value::Func(func) => { @@ -32,6 +32,7 @@ impl Eval for ast::ModuleImport<'_> { Value::Module(_) => {} Value::Str(path) => { source = Value::Module(import(&mut vm.engine, path, source_span)?); + is_str = true; } v => { bail!( @@ -42,9 +43,12 @@ impl Eval for ast::ModuleImport<'_> { } } + // Source itself is imported if there is no import list or a rename. + let bare_name = self.bare_name(); + let new_name = self.new_name(); if let Some(new_name) = new_name { - if let ast::Expr::Ident(ident) = self.source() { - if ident.as_str() == new_name.as_str() { + if let Ok(source_name) = &bare_name { + if source_name == new_name.as_str() { // Warn on `import x as x` vm.engine.sink.warn(warning!( new_name.span(), @@ -58,12 +62,33 @@ impl Eval for ast::ModuleImport<'_> { } let scope = source.scope().unwrap(); - match imports { + match self.imports() { None => { - // Only import here if there is no rename. if new_name.is_none() { - let name: EcoString = source.name().unwrap().into(); - vm.scopes.top.define(name, source); + match self.bare_name() { + // Bare dynamic string imports are not allowed. + Ok(name) + if !is_str || matches!(source_expr, ast::Expr::Str(_)) => + { + if matches!(source_expr, ast::Expr::Ident(_)) { + vm.engine.sink.warn(warning!( + source_expr.span(), + "this import has no effect", + )); + } + vm.scopes.top.define_spanned(name, source, source_span); + } + Ok(_) | Err(BareImportError::Dynamic) => bail!( + source_span, "dynamic import requires an explicit name"; + hint: "you can name the import with `as`" + ), + Err(BareImportError::PathInvalid) => bail!( + source_span, "module name would not be a valid identifier"; + hint: "you can rename the import with `as`", + ), + // Bad package spec would have failed the import already. + Err(BareImportError::PackageInvalid) => unreachable!(), + } } } Some(ast::Imports::Wildcard) => { diff --git a/crates/typst-ide/src/matchers.rs b/crates/typst-ide/src/matchers.rs index b92cbf557..ef8288f2a 100644 --- a/crates/typst-ide/src/matchers.rs +++ b/crates/typst-ide/src/matchers.rs @@ -1,7 +1,7 @@ use ecow::EcoString; use typst::foundations::{Module, Value}; use typst::syntax::ast::AstNode; -use typst::syntax::{ast, LinkedNode, Span, SyntaxKind, SyntaxNode}; +use typst::syntax::{ast, LinkedNode, Span, SyntaxKind}; use crate::{analyze_import, IdeWorld}; @@ -30,38 +30,38 @@ pub fn named_items( if let Some(v) = node.cast::() { let imports = v.imports(); - let source = node - .children() - .find(|child| child.is::()) - .and_then(|source: LinkedNode| { - Some((analyze_import(world, &source)?, source)) - }); - let source = source.as_ref(); + let source = v.source(); + + let source_value = node + .find(source.span()) + .and_then(|source| analyze_import(world, &source)); + let source_value = source_value.as_ref(); + + let module = source_value.and_then(|value| match value { + Value::Module(module) => Some(module), + _ => None, + }); + + let name_and_span = match (imports, v.new_name()) { + // ```plain + // import "foo" as name + // import "foo" as name: .. + // ``` + (_, Some(name)) => Some((name.get().clone(), name.span())), + // ```plain + // import "foo" + // ``` + (None, None) => v.bare_name().ok().map(|name| (name, source.span())), + // ```plain + // import "foo": .. + // ``` + (Some(..), None) => None, + }; // Seeing the module itself. - if let Some((value, source)) = source { - let site = match (imports, v.new_name()) { - // ```plain - // import "foo" as name; - // import "foo" as name: ..; - // ``` - (_, Some(name)) => Some(name.to_untyped()), - // ```plain - // import "foo"; - // ``` - (None, None) => Some(source.get()), - // ```plain - // import "foo": ..; - // ``` - (Some(..), None) => None, - }; - - if let Some((site, value)) = - site.zip(value.clone().cast::().ok()) - { - if let Some(res) = recv(NamedItem::Module(&value, site)) { - return Some(res); - } + if let Some((name, span)) = name_and_span { + if let Some(res) = recv(NamedItem::Module(&name, span, module)) { + return Some(res); } } @@ -75,7 +75,7 @@ pub fn named_items( // import "foo": *; // ``` Some(ast::Imports::Wildcard) => { - if let Some(scope) = source.and_then(|(value, _)| value.scope()) { + if let Some(scope) = source_value.and_then(Value::scope) { for (name, value, span) in scope.iter() { let item = NamedItem::Import(name, span, Some(value)); if let Some(res) = recv(item) { @@ -92,7 +92,7 @@ pub fn named_items( let bound = item.bound_name(); let (span, value) = item.path().iter().fold( - (bound.span(), source.map(|(value, _)| value)), + (bound.span(), source_value), |(span, value), path_ident| { let scope = value.and_then(|v| v.scope()); let span = scope @@ -175,8 +175,8 @@ pub enum NamedItem<'a> { Var(ast::Ident<'a>), /// A function item. Fn(ast::Ident<'a>), - /// A (imported) module item. - Module(&'a Module, &'a SyntaxNode), + /// A (imported) module. + Module(&'a EcoString, Span, Option<&'a Module>), /// An imported item. Import(&'a EcoString, Span, Option<&'a Value>), } @@ -186,7 +186,7 @@ impl<'a> NamedItem<'a> { match self { NamedItem::Var(ident) => ident.get(), NamedItem::Fn(ident) => ident.get(), - NamedItem::Module(value, _) => value.name(), + NamedItem::Module(name, _, _) => name, NamedItem::Import(name, _, _) => name, } } @@ -194,7 +194,7 @@ impl<'a> NamedItem<'a> { pub(crate) fn value(&self) -> Option { match self { NamedItem::Var(..) | NamedItem::Fn(..) => None, - NamedItem::Module(value, _) => Some(Value::Module((*value).clone())), + NamedItem::Module(_, _, value) => value.cloned().map(Value::Module), NamedItem::Import(_, _, value) => value.cloned(), } } @@ -202,7 +202,7 @@ impl<'a> NamedItem<'a> { pub(crate) fn span(&self) -> Span { match *self { NamedItem::Var(name) | NamedItem::Fn(name) => name.span(), - NamedItem::Module(_, site) => site.span(), + NamedItem::Module(_, span, _) => span, NamedItem::Import(_, span, _) => span, } } @@ -356,7 +356,17 @@ mod tests { #[test] fn test_named_items_import() { - test("#import \"foo.typ\": a; #(a);", 2).must_include(["a"]); + test("#import \"foo.typ\"", 2).must_include(["foo"]); + test("#import \"foo.typ\" as bar", 2) + .must_include(["bar"]) + .must_exclude(["foo"]); + } + + #[test] + fn test_named_items_import_items() { + test("#import \"foo.typ\": a; #(a);", 2) + .must_include(["a"]) + .must_exclude(["foo"]); let world = TestWorld::new("#import \"foo.typ\": a.b; #(b);") .with_source("foo.typ", "#import \"a.typ\"") diff --git a/crates/typst-library/src/foundations/mod.rs b/crates/typst-library/src/foundations/mod.rs index 2c3730d53..2921481bc 100644 --- a/crates/typst-library/src/foundations/mod.rs +++ b/crates/typst-library/src/foundations/mod.rs @@ -122,8 +122,8 @@ pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) { if features.is_enabled(Feature::Html) { global.define_func::(); } - global.define_module(calc::module()); - global.define_module(sys::module(inputs)); + global.define("calc", calc::module()); + global.define("sys", sys::module(inputs)); } /// Fails with an error. diff --git a/crates/typst-library/src/foundations/module.rs b/crates/typst-library/src/foundations/module.rs index a476d6af1..2001aca16 100644 --- a/crates/typst-library/src/foundations/module.rs +++ b/crates/typst-library/src/foundations/module.rs @@ -32,7 +32,7 @@ use crate::foundations::{repr, ty, Content, Scope, Value}; #[allow(clippy::derived_hash_with_manual_eq)] pub struct Module { /// The module's name. - name: EcoString, + name: Option, /// The reference-counted inner fields. inner: Arc, } @@ -52,14 +52,22 @@ impl Module { /// Create a new module. pub fn new(name: impl Into, scope: Scope) -> Self { Self { - name: name.into(), + name: Some(name.into()), + inner: Arc::new(Repr { scope, content: Content::empty(), file_id: None }), + } + } + + /// Create a new anonymous module without a name. + pub fn anonymous(scope: Scope) -> Self { + Self { + name: None, inner: Arc::new(Repr { scope, content: Content::empty(), file_id: None }), } } /// Update the module's name. pub fn with_name(mut self, name: impl Into) -> Self { - self.name = name.into(); + self.name = Some(name.into()); self } @@ -82,8 +90,8 @@ impl Module { } /// Get the module's name. - pub fn name(&self) -> &EcoString { - &self.name + pub fn name(&self) -> Option<&EcoString> { + self.name.as_ref() } /// Access the module's scope. @@ -105,8 +113,9 @@ impl Module { /// Try to access a definition in the module. pub fn field(&self, name: &str) -> StrResult<&Value> { - self.scope().get(name).ok_or_else(|| { - eco_format!("module `{}` does not contain `{name}`", self.name()) + self.scope().get(name).ok_or_else(|| match &self.name { + Some(module) => eco_format!("module `{module}` does not contain `{name}`"), + None => eco_format!("module does not contain `{name}`"), }) } @@ -131,7 +140,10 @@ impl Debug for Module { impl repr::Repr for Module { fn repr(&self) -> EcoString { - eco_format!("", self.name()) + match &self.name { + Some(module) => eco_format!(""), + None => "".into(), + } } } diff --git a/crates/typst-library/src/foundations/scope.rs b/crates/typst-library/src/foundations/scope.rs index b51f8caaf..99c9a37e6 100644 --- a/crates/typst-library/src/foundations/scope.rs +++ b/crates/typst-library/src/foundations/scope.rs @@ -12,8 +12,8 @@ use typst_utils::Static; use crate::diag::{bail, HintedStrResult, HintedString, StrResult}; use crate::foundations::{ - Element, Func, IntoValue, Module, NativeElement, NativeFunc, NativeFuncData, - NativeType, Type, Value, + Element, Func, IntoValue, NativeElement, NativeFunc, NativeFuncData, NativeType, + Type, Value, }; use crate::Library; @@ -252,11 +252,6 @@ impl Scope { self.define(data.name, Element::from(data)); } - /// Define a module. - pub fn define_module(&mut self, module: Module) { - self.define(module.name().clone(), module); - } - /// Try to access a variable immutably. pub fn get(&self, var: &str) -> Option<&Value> { self.map.get(var).map(Slot::read) diff --git a/crates/typst-library/src/foundations/value.rs b/crates/typst-library/src/foundations/value.rs index 8d9f59332..d99027728 100644 --- a/crates/typst-library/src/foundations/value.rs +++ b/crates/typst-library/src/foundations/value.rs @@ -181,16 +181,6 @@ impl Value { } } - /// The name, if this is a function, type, or module. - pub fn name(&self) -> Option<&str> { - match self { - Self::Func(func) => func.name(), - Self::Type(ty) => Some(ty.short_name()), - Self::Module(module) => Some(module.name()), - _ => None, - } - } - /// Try to extract documentation for the value. pub fn docs(&self) -> Option<&'static str> { match self { @@ -730,6 +720,11 @@ mod tests { assert_eq!(value.into_value().repr(), exp); } + #[test] + fn test_value_size() { + assert!(std::mem::size_of::() <= 32); + } + #[test] fn test_value_debug() { // Primitives. diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs index 2ea77eaa5..22f3a62a3 100644 --- a/crates/typst-library/src/lib.rs +++ b/crates/typst-library/src/lib.rs @@ -244,7 +244,7 @@ fn global(math: Module, inputs: Dict, features: &Features) -> Module { self::model::define(&mut global); self::text::define(&mut global); global.reset_category(); - global.define_module(math); + global.define("math", math); self::layout::define(&mut global); self::visualize::define(&mut global); self::introspection::define(&mut global); @@ -253,7 +253,7 @@ fn global(math: Module, inputs: Dict, features: &Features) -> Module { self::pdf::define(&mut global); global.reset_category(); if features.is_enabled(Feature::Html) { - global.define_module(self::html::module()); + global.define("html", self::html::module()); } prelude(&mut global); Module::new("global", global) diff --git a/crates/typst-library/src/pdf/mod.rs b/crates/typst-library/src/pdf/mod.rs index 669835d4c..ec0754631 100644 --- a/crates/typst-library/src/pdf/mod.rs +++ b/crates/typst-library/src/pdf/mod.rs @@ -13,7 +13,7 @@ pub static PDF: Category; /// Hook up the `pdf` module. pub(super) fn define(global: &mut Scope) { global.category(PDF); - global.define_module(module()); + global.define("pdf", module()); } /// Hook up all `pdf` definitions. diff --git a/crates/typst-syntax/src/ast.rs b/crates/typst-syntax/src/ast.rs index 014e8392e..640138e77 100644 --- a/crates/typst-syntax/src/ast.rs +++ b/crates/typst-syntax/src/ast.rs @@ -4,11 +4,14 @@ use std::num::NonZeroUsize; use std::ops::Deref; +use std::path::Path; +use std::str::FromStr; use ecow::EcoString; use unscanny::Scanner; -use crate::{is_newline, Span, SyntaxKind, SyntaxNode}; +use crate::package::PackageSpec; +use crate::{is_ident, is_newline, Span, SyntaxKind, SyntaxNode}; /// A typed AST node. pub trait AstNode<'a>: Sized { @@ -2064,6 +2067,41 @@ impl<'a> ModuleImport<'a> { }) } + /// The name that will be bound for a bare import. This name must be + /// statically known. It can come from: + /// - an identifier + /// - a field access + /// - a string that is a valid file path where the file stem is a valid + /// identifier + /// - a string that is a valid package spec + pub fn bare_name(self) -> Result { + match self.source() { + Expr::Ident(ident) => Ok(ident.get().clone()), + Expr::FieldAccess(access) => Ok(access.field().get().clone()), + Expr::Str(string) => { + let string = string.get(); + let name = if string.starts_with('@') { + PackageSpec::from_str(&string) + .map_err(|_| BareImportError::PackageInvalid)? + .name + } else { + Path::new(string.as_str()) + .file_stem() + .and_then(|path| path.to_str()) + .ok_or(BareImportError::PathInvalid)? + .into() + }; + + if !is_ident(&name) { + return Err(BareImportError::PathInvalid); + } + + Ok(name) + } + _ => Err(BareImportError::Dynamic), + } + } + /// The name this module was assigned to, if it was renamed with `as` /// (`renamed` in `import "..." as renamed`). pub fn new_name(self) -> Option> { @@ -2074,6 +2112,18 @@ impl<'a> ModuleImport<'a> { } } +/// Reasons why a bare name cannot be determined for an import source. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum BareImportError { + /// There is no statically resolvable binding name. + Dynamic, + /// The import source is not a valid path or the path stem not a valid + /// identifier. + PathInvalid, + /// The import source is not a valid package spec. + PackageInvalid, +} + /// The items that ought to be imported from a file. #[derive(Debug, Copy, Clone, Hash)] pub enum Imports<'a> { diff --git a/tests/suite/scripting/import.typ b/tests/suite/scripting/import.typ index 95214db76..03e2efc6b 100644 --- a/tests/suite/scripting/import.typ +++ b/tests/suite/scripting/import.typ @@ -145,6 +145,34 @@ #test(module.item(1, 2), 3) #test(module.push(2), 3) +--- import-from-file-bare-invalid --- +// Error: 9-33 module name would not be a valid identifier +// Hint: 9-33 you can rename the import with `as` +#import "modules/with space.typ" + +--- import-from-file-bare-dynamic --- +// Error: 9-26 dynamic import requires an explicit name +// Hint: 9-26 you can name the import with `as` +#import "mod" + "ule.typ" + +--- import-from-var-bare --- +#let p = "module.typ" +// Error: 9-10 dynamic import requires an explicit name +// Hint: 9-10 you can name the import with `as` +#import p +#test(p.b, 1) + +--- import-from-dict-field-bare --- +#let d = (p: "module.typ") +// Error: 9-12 dynamic import requires an explicit name +// Hint: 9-12 you can name the import with `as` +#import d.p +#test(p.b, 1) + +--- import-from-file-renamed-dynamic --- +#import "mod" + "ule.typ" as mod +#test(mod.b, 1) + --- import-from-file-renamed --- // A renamed module import without items. #import "module.typ" as other @@ -160,6 +188,10 @@ #test(item(1, 2), 3) #test(newname.item(1, 2), 3) +--- import-from-function-scope-bare --- +// Warning: 9-13 this import has no effect +#import enum + --- import-from-function-scope-renamed --- // Renamed module import with function scopes. #import enum as othernum @@ -171,6 +203,23 @@ #import asrt: ne as asne #asne(1, 2) +--- import-from-module-bare --- +#import "modules/chap1.typ" as mymod +// Warning: 9-14 this import has no effect +#import mymod +// The name `chap1` is not bound. +// Error: 2-7 unknown variable: chap1 +#chap1 + +--- import-module-nested --- +#import std.calc: pi +#test(pi, calc.pi) + +--- import-module-nested-bare --- +#import "module.typ" +#import module.chap2 +#test(chap2.name, "Peter") + --- import-module-item-name-mutating --- // Edge case for module access that isn't fixed. #import "module.typ" @@ -214,10 +263,14 @@ // Warning: 31-35 unnecessary import rename to same name #import enum as enum: item as item ---- import-item-rename-unnecessary-but-ok --- -// No warning on a case that isn't obviously pathological +--- import-item-rename-unnecessary-string --- +// Warning: 25-31 unnecessary import rename to same name #import "module.typ" as module +--- import-item-rename-unnecessary-but-ok --- +#import "modul" + "e.typ" as module +#test(module.b, 1) + --- import-from-closure-invalid --- // Can't import from closures. #let f(x) = x @@ -359,6 +412,15 @@ This is never reached. #import "@test/adder:0.1.0" #test(adder.add(2, 8), 10) +--- import-from-package-dynamic --- +// Error: 9-33 dynamic import requires an explicit name +// Hint: 9-33 you can name the import with `as` +#import "@test/" + "adder:0.1.0" + +--- import-from-package-renamed-dynamic --- +#import "@test/" + "adder:0.1.0" as adder +#test(adder.add(2, 8), 10) + --- import-from-package-items --- // Test import with items. #import "@test/adder:0.1.0": add diff --git a/tests/suite/scripting/modules/with space.typ b/tests/suite/scripting/modules/with space.typ new file mode 100644 index 000000000..9138f3c3f --- /dev/null +++ b/tests/suite/scripting/modules/with space.typ @@ -0,0 +1 @@ +// SKIP From 7a0d7092bc00ee4f5c0d4887ea3ccf3fbceb2426 Mon Sep 17 00:00:00 2001 From: TwoF1nger <140991913+TwoF1nger@users.noreply.github.com> Date: Thu, 30 Jan 2025 10:11:03 +0000 Subject: [PATCH 29/34] Fix typo in scripting.md (#5783) --- docs/reference/scripting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/scripting.md b/docs/reference/scripting.md index 6c7a7b338..5e0f1555e 100644 --- a/docs/reference/scripting.md +++ b/docs/reference/scripting.md @@ -363,7 +363,7 @@ and can be achieved using functions from the | `{not in}` | Check if not in collection | Binary | 4 | | `{not}` | Logical "not" | Unary | 3 | | `{and}` | Short-circuiting logical "and" | Binary | 3 | -| `{or}` | Short-circuiting logical "or | Binary | 2 | +| `{or}` | Short-circuiting logical "or" | Binary | 2 | | `{=}` | Assignment | Binary | 1 | | `{+=}` | Add-Assignment | Binary | 1 | | `{-=}` | Subtraction-Assignment | Binary | 1 | From be1fa91a00a9bff6c5eb9744266f252b8cc23fe4 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 30 Jan 2025 14:36:15 +0100 Subject: [PATCH 30/34] Modular, multi-threaded, transitioning plugins (#5779) --- Cargo.lock | 2 +- Cargo.toml | 2 +- crates/typst-eval/src/call.rs | 12 +- crates/typst-ide/src/complete.rs | 10 - crates/typst-library/src/foundations/func.rs | 59 +- crates/typst-library/src/foundations/mod.rs | 7 +- .../typst-library/src/foundations/module.rs | 20 +- crates/typst-library/src/foundations/ops.rs | 1 - .../typst-library/src/foundations/plugin.rs | 562 +++++++++++++----- crates/typst-library/src/foundations/scope.rs | 8 + crates/typst-library/src/foundations/value.rs | 11 +- tests/suite/foundations/plugin.typ | 31 + tools/test-helper/package.json | 209 +++---- 13 files changed, 618 insertions(+), 316 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8aa7c0ec1..3343c246b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2766,7 +2766,7 @@ dependencies = [ [[package]] name = "typst-dev-assets" version = "0.12.0" -source = "git+https://github.com/typst/typst-dev-assets?rev=b07d156#b07d1560143d6883887358d30edb25cb12fcf5b9" +source = "git+https://github.com/typst/typst-dev-assets?rev=7f8999d#7f8999d19907cd6e1148b295efbc844921c0761c" [[package]] name = "typst-docs" diff --git a/Cargo.toml b/Cargo.toml index 1be7816a7..6b592cd39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ typst-syntax = { path = "crates/typst-syntax", version = "0.12.0" } typst-timing = { path = "crates/typst-timing", version = "0.12.0" } typst-utils = { path = "crates/typst-utils", version = "0.12.0" } typst-assets = { git = "https://github.com/typst/typst-assets", rev = "8cccef9" } -typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "b07d156" } +typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "7f8999d" } arrayvec = "0.7.4" az = "1.2" base64 = "0.22" diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs index f59235c78..2a2223e15 100644 --- a/crates/typst-eval/src/call.rs +++ b/crates/typst-eval/src/call.rs @@ -6,8 +6,8 @@ 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, SymbolElem, Value, + Arg, Args, Capturer, Closure, Content, Context, Func, NativeElement, Scope, Scopes, + SymbolElem, Value, }; use typst_library::introspection::Introspector; use typst_library::math::LrElem; @@ -315,13 +315,7 @@ fn eval_field_call( (target, args) }; - if let Value::Plugin(plugin) = &target { - // Call plugins by converting args to bytes. - let bytes = args.all::()?; - args.finish()?; - let value = plugin.call(&field, bytes).at(span)?.into_value(); - Ok(FieldCall::Resolved(value)) - } else if let Some(callee) = target.ty().scope().get(&field) { + 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 { diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index 0f8abddb7..24b76537a 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -452,16 +452,6 @@ fn field_access_completions( } } } - Value::Plugin(plugin) => { - for name in plugin.iter() { - ctx.completions.push(Completion { - kind: CompletionKind::Func, - label: name.clone(), - apply: None, - detail: None, - }) - } - } _ => {} } } diff --git a/crates/typst-library/src/foundations/func.rs b/crates/typst-library/src/foundations/func.rs index cb3eba161..a05deb1f3 100644 --- a/crates/typst-library/src/foundations/func.rs +++ b/crates/typst-library/src/foundations/func.rs @@ -9,11 +9,11 @@ use ecow::{eco_format, EcoString}; use typst_syntax::{ast, Span, SyntaxNode}; use typst_utils::{singleton, LazyHash, Static}; -use crate::diag::{bail, SourceResult, StrResult}; +use crate::diag::{bail, At, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, repr, scope, ty, Args, CastInfo, Content, Context, Element, IntoArgs, Scope, - Selector, Type, Value, + cast, repr, scope, ty, Args, Bytes, CastInfo, Content, Context, Element, IntoArgs, + PluginFunc, Scope, Selector, Type, Value, }; /// A mapping from argument values to a return value. @@ -151,6 +151,8 @@ enum Repr { Element(Element), /// A user-defined closure. Closure(Arc>), + /// A plugin WebAssembly function. + Plugin(Arc), /// A nested function with pre-applied arguments. With(Arc<(Func, Args)>), } @@ -164,6 +166,7 @@ impl Func { Repr::Native(native) => Some(native.name), Repr::Element(elem) => Some(elem.name()), Repr::Closure(closure) => closure.name(), + Repr::Plugin(func) => Some(func.name()), Repr::With(with) => with.0.name(), } } @@ -176,6 +179,7 @@ impl Func { Repr::Native(native) => Some(native.title), Repr::Element(elem) => Some(elem.title()), Repr::Closure(_) => None, + Repr::Plugin(_) => None, Repr::With(with) => with.0.title(), } } @@ -186,6 +190,7 @@ impl Func { Repr::Native(native) => Some(native.docs), Repr::Element(elem) => Some(elem.docs()), Repr::Closure(_) => None, + Repr::Plugin(_) => None, Repr::With(with) => with.0.docs(), } } @@ -204,6 +209,7 @@ impl Func { Repr::Native(native) => Some(&native.0.params), Repr::Element(elem) => Some(elem.params()), Repr::Closure(_) => None, + Repr::Plugin(_) => None, Repr::With(with) => with.0.params(), } } @@ -221,6 +227,7 @@ impl Func { Some(singleton!(CastInfo, CastInfo::Type(Type::of::()))) } Repr::Closure(_) => None, + Repr::Plugin(_) => None, Repr::With(with) => with.0.returns(), } } @@ -231,6 +238,7 @@ impl Func { Repr::Native(native) => native.keywords, Repr::Element(elem) => elem.keywords(), Repr::Closure(_) => &[], + Repr::Plugin(_) => &[], Repr::With(with) => with.0.keywords(), } } @@ -241,6 +249,7 @@ impl Func { Repr::Native(native) => Some(&native.0.scope), Repr::Element(elem) => Some(elem.scope()), Repr::Closure(_) => None, + Repr::Plugin(_) => None, Repr::With(with) => with.0.scope(), } } @@ -266,6 +275,14 @@ impl Func { } } + /// Extract the plugin function, if it is one. + pub fn to_plugin(&self) -> Option<&PluginFunc> { + match &self.repr { + Repr::Plugin(func) => Some(func), + _ => None, + } + } + /// Call the function with the given context and arguments. pub fn call( &self, @@ -307,6 +324,12 @@ impl Func { context, args, ), + Repr::Plugin(func) => { + let inputs = args.all::()?; + let output = func.call(inputs).at(args.span)?; + args.finish()?; + Ok(Value::Bytes(output)) + } Repr::With(with) => { args.items = with.1.items.iter().cloned().chain(args.items).collect(); with.0.call(engine, context, args) @@ -425,12 +448,30 @@ impl From for Func { } } +impl From<&'static NativeFuncData> for Func { + fn from(data: &'static NativeFuncData) -> Self { + Repr::Native(Static(data)).into() + } +} + impl From for Func { fn from(func: Element) -> Self { Repr::Element(func).into() } } +impl From for Func { + fn from(closure: Closure) -> Self { + Repr::Closure(Arc::new(LazyHash::new(closure))).into() + } +} + +impl From for Func { + fn from(func: PluginFunc) -> Self { + Repr::Plugin(Arc::new(func)).into() + } +} + /// A Typst function that is defined by a native Rust type that shadows a /// native Rust function. pub trait NativeFunc { @@ -466,12 +507,6 @@ pub struct NativeFuncData { pub returns: LazyLock, } -impl From<&'static NativeFuncData> for Func { - fn from(data: &'static NativeFuncData) -> Self { - Repr::Native(Static(data)).into() - } -} - cast! { &'static NativeFuncData, self => Func::from(self).into_value(), @@ -525,12 +560,6 @@ impl Closure { } } -impl From for Func { - fn from(closure: Closure) -> Self { - Repr::Closure(Arc::new(LazyHash::new(closure))).into() - } -} - cast! { Closure, self => Value::Func(self.into()), diff --git a/crates/typst-library/src/foundations/mod.rs b/crates/typst-library/src/foundations/mod.rs index 2921481bc..a790da4f4 100644 --- a/crates/typst-library/src/foundations/mod.rs +++ b/crates/typst-library/src/foundations/mod.rs @@ -25,7 +25,8 @@ mod int; mod label; mod module; mod none; -mod plugin; +#[path = "plugin.rs"] +mod plugin_; mod scope; mod selector; mod str; @@ -56,7 +57,7 @@ pub use self::int::*; pub use self::label::*; pub use self::module::*; pub use self::none::*; -pub use self::plugin::*; +pub use self::plugin_::*; pub use self::repr::Repr; pub use self::scope::*; pub use self::selector::*; @@ -114,11 +115,11 @@ pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) { global.define_type::(); global.define_type::(); global.define_type::(); - global.define_type::(); global.define_func::(); global.define_func::(); global.define_func::(); global.define_func::(); + global.define_func::(); if features.is_enabled(Feature::Html) { global.define_func::(); } diff --git a/crates/typst-library/src/foundations/module.rs b/crates/typst-library/src/foundations/module.rs index 2001aca16..3ee59c106 100644 --- a/crates/typst-library/src/foundations/module.rs +++ b/crates/typst-library/src/foundations/module.rs @@ -7,14 +7,20 @@ use typst_syntax::FileId; use crate::diag::StrResult; use crate::foundations::{repr, ty, Content, Scope, Value}; -/// An evaluated module, either built-in or resulting from a file. +/// An module of definitions. /// -/// You can access definitions from the module using -/// [field access notation]($scripting/#fields) and interact with it using the -/// [import and include syntaxes]($scripting/#modules). Alternatively, it is -/// possible to convert a module to a dictionary, and therefore access its -/// contents dynamically, using the -/// [dictionary constructor]($dictionary/#constructor). +/// A module +/// - be built-in +/// - stem from a [file import]($scripting/#modules) +/// - stem from a [package import]($scripting/#packages) (and thus indirectly +/// its entrypoint file) +/// - result from a call to the [plugin]($plugin) function +/// +/// You can access definitions from the module using [field access +/// notation]($scripting/#fields) and interact with it using the [import and +/// include syntaxes]($scripting/#modules). Alternatively, it is possible to +/// convert a module to a dictionary, and therefore access its contents +/// dynamically, using the [dictionary constructor]($dictionary/#constructor). /// /// # Example /// ```example diff --git a/crates/typst-library/src/foundations/ops.rs b/crates/typst-library/src/foundations/ops.rs index 7dbdde8ff..6c2408446 100644 --- a/crates/typst-library/src/foundations/ops.rs +++ b/crates/typst-library/src/foundations/ops.rs @@ -447,7 +447,6 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool { (Args(a), Args(b)) => a == b, (Type(a), Type(b)) => a == b, (Module(a), Module(b)) => a == b, - (Plugin(a), Plugin(b)) => a == b, (Datetime(a), Datetime(b)) => a == b, (Duration(a), Duration(b)) => a == b, (Dyn(a), Dyn(b)) => a == b, diff --git a/crates/typst-library/src/foundations/plugin.rs b/crates/typst-library/src/foundations/plugin.rs index d41261edc..cbc0f52de 100644 --- a/crates/typst-library/src/foundations/plugin.rs +++ b/crates/typst-library/src/foundations/plugin.rs @@ -4,43 +4,27 @@ use std::sync::{Arc, Mutex}; use ecow::{eco_format, EcoString}; use typst_syntax::Spanned; -use wasmi::{AsContext, AsContextMut}; +use wasmi::Memory; use crate::diag::{bail, At, SourceResult, StrResult}; use crate::engine::Engine; -use crate::foundations::{func, repr, scope, ty, Bytes}; +use crate::foundations::{cast, func, scope, Bytes, Func, Module, Scope, Value}; use crate::loading::{DataSource, Load}; -/// A WebAssembly plugin. +/// Loads a WebAssembly module. /// -/// Typst is capable of interfacing with plugins compiled to WebAssembly. Plugin -/// functions may accept multiple [byte buffers]($bytes) as arguments and return -/// a single byte buffer. They should typically be wrapped in idiomatic Typst -/// functions that perform the necessary conversions between native Typst types -/// and bytes. +/// The resulting [module] will contain one Typst [function] for each function +/// export of the loaded WebAssembly module. /// -/// Plugins run in isolation from your system, which means that printing, -/// reading files, or anything like that will not be supported for security -/// reasons. To run as a plugin, a program needs to be compiled to a 32-bit -/// shared WebAssembly library. Many compilers will use the -/// [WASI ABI](https://wasi.dev/) by default or as their only option (e.g. -/// emscripten), which allows printing, reading files, etc. This ABI will not -/// directly work with Typst. You will either need to compile to a different -/// target or [stub all functions](https://github.com/astrale-sharp/wasm-minimal-protocol/tree/master/crates/wasi-stub). +/// Typst WebAssembly plugins need to follow a specific +/// [protocol]($plugin/#protocol). To run as a plugin, a program needs to be +/// compiled to a 32-bit shared WebAssembly library. Plugin functions may accept +/// multiple [byte buffers]($bytes) as arguments and return a single byte +/// buffer. They should typically be wrapped in idiomatic Typst functions that +/// perform the necessary conversions between native Typst types and bytes. /// -/// # Plugins and Packages -/// Plugins are distributed as packages. A package can make use of a plugin -/// simply by including a WebAssembly file and loading it. Because the -/// byte-based plugin interface is quite low-level, plugins are typically -/// exposed through wrapper functions, that also live in the same package. -/// -/// # Purity -/// Plugin functions must be pure: Given the same arguments, they must always -/// return the same value. The reason for this is that Typst functions must be -/// pure (which is quite fundamental to the language design) and, since Typst -/// function can call plugin functions, this requirement is inherited. In -/// particular, if a plugin function is called twice with the same arguments, -/// Typst might cache the results and call your function only once. +/// For security reasons, plugins run in isolation from your system. This means +/// that printing, reading files, or similar things are not supported. /// /// # Example /// ```example @@ -55,6 +39,50 @@ use crate::loading::{DataSource, Load}; /// #concat("hello", "world") /// ``` /// +/// Since the plugin function returns a module, it can be used with import +/// syntax: +/// ```typ +/// #import plugin("hello.wasm"): concatenate +/// ``` +/// +/// # Purity +/// Plugin functions **must be pure:** A plugin function call most not have any +/// observable side effects on future plugin calls and given the same arguments, +/// it must always return the same value. +/// +/// The reason for this is that Typst functions must be pure (which is quite +/// fundamental to the language design) and, since Typst function can call +/// plugin functions, this requirement is inherited. In particular, if a plugin +/// function is called twice with the same arguments, Typst might cache the +/// results and call your function only once. Moreover, Typst may run multiple +/// instances of your plugin in multiple threads, with no state shared between +/// them. +/// +/// Typst does not enforce plugin function purity (for efficiency reasons), but +/// calling an impure function will lead to unpredictable and irreproducible +/// results and must be avoided. +/// +/// That said, mutable operations _can be_ useful for plugins that require +/// costly runtime initialization. Due to the purity requirement, such +/// initialization cannot be performed through a normal function call. Instead, +/// Typst exposes a [plugin transition API]($plugin.transition), which executes +/// a function call and then creates a derived module with new functions which +/// will observe the side effects produced by the transition call. The original +/// plugin remains unaffected. +/// +/// # Plugins and Packages +/// Any Typst code can make use of a plugin simply by including a WebAssembly +/// file and loading it. However, because the byte-based plugin interface is +/// quite low-level, plugins are typically exposed through a package containing +/// the plugin and idiomatic wrapper functions. +/// +/// # WASI +/// Many compilers will use the [WASI ABI](https://wasi.dev/) by default or as +/// their only option (e.g. emscripten), which allows printing, reading files, +/// etc. This ABI will not directly work with Typst. You will either need to +/// compile to a different target or [stub all +/// functions](https://github.com/astrale-sharp/wasm-minimal-protocol/tree/master/crates/wasi-stub). +/// /// # Protocol /// To be used as a plugin, a WebAssembly module must conform to the following /// protocol: @@ -67,8 +95,8 @@ use crate::loading::{DataSource, Load}; /// lengths, so `usize/size_t` may be preferable), and return one 32-bit /// integer. /// -/// - The function should first allocate a buffer `buf` of length -/// `a_1 + a_2 + ... + a_n`, and then call +/// - The function should first allocate a buffer `buf` of length `a_1 + a_2 + +/// ... + a_n`, and then call /// `wasm_minimal_protocol_write_args_to_buffer(buf.ptr)`. /// /// - The `a_1` first bytes of the buffer now constitute the first argument, the @@ -85,19 +113,21 @@ use crate::loading::{DataSource, Load}; /// then interpreted as an UTF-8 encoded error message. /// /// ## Imports -/// Plugin modules need to import two functions that are provided by the runtime. -/// (Types and functions are described using WAT syntax.) +/// Plugin modules need to import two functions that are provided by the +/// runtime. (Types and functions are described using WAT syntax.) /// -/// - `(import "typst_env" "wasm_minimal_protocol_write_args_to_buffer" (func (param i32)))` +/// - `(import "typst_env" "wasm_minimal_protocol_write_args_to_buffer" (func +/// (param i32)))` /// /// Writes the arguments for the current function into a plugin-allocated -/// buffer. When a plugin function is called, it -/// [receives the lengths](#exports) of its input buffers as arguments. It -/// should then allocate a buffer whose capacity is at least the sum of these -/// lengths. It should then call this function with a `ptr` to the buffer to -/// fill it with the arguments, one after another. +/// buffer. When a plugin function is called, it [receives the +/// lengths](#exports) of its input buffers as arguments. It should then +/// allocate a buffer whose capacity is at least the sum of these lengths. It +/// should then call this function with a `ptr` to the buffer to fill it with +/// the arguments, one after another. /// -/// - `(import "typst_env" "wasm_minimal_protocol_send_result_to_host" (func (param i32 i32)))` +/// - `(import "typst_env" "wasm_minimal_protocol_send_result_to_host" (func +/// (param i32 i32)))` /// /// Sends the output of the current function to the host (Typst). The first /// parameter shall be a pointer to a buffer (`ptr`), while the second is the @@ -106,72 +136,147 @@ use crate::loading::{DataSource, Load}; /// interpreted as an error message, it should be encoded as UTF-8. /// /// # Resources -/// For more resources, check out the -/// [wasm-minimal-protocol repository](https://github.com/astrale-sharp/wasm-minimal-protocol). -/// It contains: +/// For more resources, check out the [wasm-minimal-protocol +/// repository](https://github.com/astrale-sharp/wasm-minimal-protocol). It +/// contains: /// /// - A list of example plugin implementations and a test runner for these /// examples /// - Wrappers to help you write your plugin in Rust (Zig wrapper in /// development) /// - A stubber for WASI -#[ty(scope, cast)] -#[derive(Clone)] -pub struct Plugin(Arc); - -/// The internal representation of a plugin. -struct Repr { - /// The raw WebAssembly bytes. - bytes: Bytes, - /// The function defined by the WebAssembly module. - functions: Vec<(EcoString, wasmi::Func)>, - /// Owns all data associated with the WebAssembly module. - store: Mutex, -} - -/// Owns all data associated with the WebAssembly module. -type Store = wasmi::Store; - -/// If there was an error reading/writing memory, keep the offset + length to -/// display an error message. -struct MemoryError { - offset: u32, - length: u32, - write: bool, -} -/// The persistent store data used for communication between store and host. -#[derive(Default)] -struct StoreData { - args: Vec, - output: Vec, - memory_error: Option, +#[func(scope)] +pub fn plugin( + engine: &mut Engine, + /// A path to a WebAssembly file or raw WebAssembly bytes. + /// + /// For more details about paths, see the [Paths section]($syntax/#paths). + source: Spanned, +) -> SourceResult { + let data = source.load(engine.world)?; + Plugin::module(data).at(source.span) } #[scope] -impl Plugin { - /// Creates a new plugin from a WebAssembly file. - #[func(constructor)] - pub fn construct( - engine: &mut Engine, - /// A path to a WebAssembly file or raw WebAssembly bytes. - /// - /// For more details about paths, see the [Paths section]($syntax/#paths). - source: Spanned, - ) -> SourceResult { - let data = source.load(engine.world)?; - Plugin::new(data).at(source.span) +impl plugin { + /// Calls a plugin function that has side effects and returns a new module + /// with plugin functions that are guaranteed to have observed the results + /// of the mutable call. + /// + /// Note that calling an impure function through a normal function call + /// (without use of the transition API) is forbidden and leads to + /// unpredictable behaviour. Read the [section on purity]($plugin/#purity) + /// for more details. + /// + /// In the example below, we load the plugin `hello-mut.wasm` which exports + /// two functions: The `get()` function retrieves a global array as a + /// string. The `add(value)` function adds a value to the global array. + /// + /// We call `add` via the transition API. The call `mutated.get()` on the + /// derived module will observe the addition. Meanwhile the original module + /// remains untouched as demonstrated by the `base.get()` call. + /// + /// _Note:_ Due to limitations in the internal WebAssembly implementation, + /// the transition API can only guarantee to reflect changes in the plugin's + /// memory, not in WebAssembly globals. If your plugin relies on changes to + /// globals being visible after transition, you might want to avoid use of + /// the transition API for now. We hope to lift this limitation in the + /// future. + /// + /// ```typ + /// #let base = plugin("hello-mut.wasm") + /// #assert.eq(base.get(), "[]") + /// + /// #let mutated = plugin.transition(base.add, "hello") + /// #assert.eq(base.get(), "[]") + /// #assert.eq(mutated.get(), "[hello]") + /// ``` + #[func] + pub fn transition( + /// The plugin function to call. + func: PluginFunc, + /// The byte buffers to call the function with. + #[variadic] + arguments: Vec, + ) -> StrResult { + func.transition(arguments) } } +/// A function loaded from a WebAssembly plugin. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct PluginFunc { + /// The underlying plugin, shared by this and the other functions. + plugin: Arc, + /// The name of the plugin function. + name: EcoString, +} + +impl PluginFunc { + /// The name of the plugin function. + pub fn name(&self) -> &str { + &self.name + } + + /// Call the WebAssembly function with the given arguments. + #[comemo::memoize] + #[typst_macros::time(name = "call plugin")] + pub fn call(&self, args: Vec) -> StrResult { + self.plugin.call(&self.name, args) + } + + /// Transition a plugin and turn the result into a module. + #[comemo::memoize] + #[typst_macros::time(name = "transition plugin")] + pub fn transition(&self, args: Vec) -> StrResult { + self.plugin.transition(&self.name, args).map(Plugin::into_module) + } +} + +cast! { + PluginFunc, + self => Value::Func(self.into()), + v: Func => v.to_plugin().ok_or("expected plugin function")?.clone(), +} + +/// A plugin with potentially multiple instances for multi-threaded +/// execution. +struct Plugin { + /// Shared by all variants of the plugin. + base: Arc, + /// A pool of plugin instances. + /// + /// When multiple plugin calls run concurrently due to multi-threading, we + /// create new instances whenever we run out of ones. + pool: Mutex>, + /// A snapshot that new instances should be restored to. + snapshot: Option, + /// A combined hash that incorporates all function names and arguments used + /// in transitions of this plugin, such that this plugin has a deterministic + /// hash and equality check that can differentiate it from "siblings" (same + /// base, different transitions). + fingerprint: u128, +} + impl Plugin { - /// Create a new plugin from raw WebAssembly bytes. + /// Create a plugin and turn it into a module. #[comemo::memoize] #[typst_macros::time(name = "load plugin")] - pub fn new(bytes: Bytes) -> StrResult { + fn module(bytes: Bytes) -> StrResult { + Self::new(bytes).map(Self::into_module) + } + + /// Create a new plugin from raw WebAssembly bytes. + fn new(bytes: Bytes) -> StrResult { let engine = wasmi::Engine::default(); let module = wasmi::Module::new(&engine, bytes.as_slice()) .map_err(|err| format!("failed to load WebAssembly module ({err})"))?; + // Ensure that the plugin exports its memory. + if !matches!(module.get_export("memory"), Some(wasmi::ExternType::Memory(_))) { + bail!("plugin does not export its memory"); + } + let mut linker = wasmi::Linker::new(&engine); linker .func_wrap( @@ -188,58 +293,174 @@ impl Plugin { ) .unwrap(); - let mut store = Store::new(&engine, StoreData::default()); - let instance = linker - .instantiate(&mut store, &module) + let base = Arc::new(PluginBase { bytes, linker, module }); + let instance = PluginInstance::new(&base, None)?; + + Ok(Self { + base, + snapshot: None, + fingerprint: 0, + pool: Mutex::new(vec![instance]), + }) + } + + /// Execute a function with access to an instsance. + fn call(&self, func: &str, args: Vec) -> StrResult { + // Acquire an instance from the pool (potentially creating a new one). + let mut instance = self.acquire()?; + + // Execute the call on an instance from the pool. If the call fails, we + // return early and _don't_ return the instance to the pool as it might + // be irrecoverably damaged. + let output = instance.call(func, args)?; + + // Return the instance to the pool. + self.pool.lock().unwrap().push(instance); + + Ok(output) + } + + /// Call a mutable plugin function, producing a new mutable whose functions + /// are guaranteed to be able to observe the mutation. + fn transition(&self, func: &str, args: Vec) -> StrResult { + // Derive a new transition hash from the old one and the function and arguments. + let fingerprint = typst_utils::hash128(&(self.fingerprint, func, &args)); + + // Execute the mutable call on an instance. + let mut instance = self.acquire()?; + + // Call the function. If the call fails, we return early and _don't_ + // return the instance to the pool as it might be irrecoverably damaged. + instance.call(func, args)?; + + // Snapshot the instance after the mutable call. + let snapshot = instance.snapshot(); + + // Create a new plugin and move (this is important!) the used instance + // into it, so that the old plugin won't observe the mutation. Also + // save the snapshot so that instances that are initialized for the + // transitioned plugin's pool observe the mutation. + Ok(Self { + base: self.base.clone(), + snapshot: Some(snapshot), + fingerprint, + pool: Mutex::new(vec![instance]), + }) + } + + /// Acquire an instance from the pool (or create a new one). + fn acquire(&self) -> StrResult { + // Don't use match to ensure that the lock is released before we create + // a new instance. + if let Some(instance) = self.pool.lock().unwrap().pop() { + return Ok(instance); + } + + PluginInstance::new(&self.base, self.snapshot.as_ref()) + } + + /// Turn a plugin into a Typst module containing plugin functions. + fn into_module(self) -> Module { + let shared = Arc::new(self); + + // Build a scope from the collected functions. + let mut scope = Scope::new(); + for export in shared.base.module.exports() { + if matches!(export.ty(), wasmi::ExternType::Func(_)) { + let name = EcoString::from(export.name()); + let func = PluginFunc { plugin: shared.clone(), name: name.clone() }; + scope.define(name, Func::from(func)); + } + } + + Module::anonymous(scope) + } +} + +impl Debug for Plugin { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.pad("Plugin(..)") + } +} + +impl PartialEq for Plugin { + fn eq(&self, other: &Self) -> bool { + self.base.bytes == other.base.bytes && self.fingerprint == other.fingerprint + } +} + +impl Hash for Plugin { + fn hash(&self, state: &mut H) { + self.base.bytes.hash(state); + self.fingerprint.hash(state); + } +} + +/// Shared by all pooled & transitioned variants of the plugin. +struct PluginBase { + /// The raw WebAssembly bytes. + bytes: Bytes, + /// The compiled WebAssembly module. + module: wasmi::Module, + /// A linker used to create a `Store` for execution. + linker: wasmi::Linker, +} + +/// An single plugin instance for single-threaded execution. +struct PluginInstance { + /// The underlying wasmi instance. + instance: wasmi::Instance, + /// The execution store of this concrete plugin instance. + store: wasmi::Store, +} + +/// A snapshot of a plugin instance. +struct Snapshot { + /// The number of pages in the main memory. + mem_pages: u32, + /// The data in the main memory. + mem_data: Vec, +} + +impl PluginInstance { + /// Create a new execution instance of a plugin, potentially restoring + /// a snapshot. + #[typst_macros::time(name = "create plugin instance")] + fn new(base: &PluginBase, snapshot: Option<&Snapshot>) -> StrResult { + let mut store = wasmi::Store::new(base.linker.engine(), CallData::default()); + let instance = base + .linker + .instantiate(&mut store, &base.module) .and_then(|pre_instance| pre_instance.start(&mut store)) .map_err(|e| eco_format!("{e}"))?; - // Ensure that the plugin exports its memory. - if !matches!( - instance.get_export(&store, "memory"), - Some(wasmi::Extern::Memory(_)) - ) { - bail!("plugin does not export its memory"); + let mut instance = PluginInstance { instance, store }; + if let Some(snapshot) = snapshot { + instance.restore(snapshot); } - - // Collect exported functions. - let functions = instance - .exports(&store) - .filter_map(|export| { - let name = export.name().into(); - export.into_func().map(|func| (name, func)) - }) - .collect(); - - Ok(Plugin(Arc::new(Repr { bytes, functions, store: Mutex::new(store) }))) + Ok(instance) } - /// Call the plugin function with the given `name`. - #[comemo::memoize] - #[typst_macros::time(name = "call plugin")] - pub fn call(&self, name: &str, args: Vec) -> StrResult { - // Find the function with the given name. - let func = self - .0 - .functions - .iter() - .find(|(v, _)| v == name) - .map(|&(_, func)| func) - .ok_or_else(|| { - eco_format!("plugin does not contain a function called {name}") - })?; + /// Call a plugin function with byte arguments. + fn call(&mut self, func: &str, args: Vec) -> StrResult { + let handle = self + .instance + .get_export(&self.store, func) + .unwrap() + .into_func() + .unwrap(); + let ty = handle.ty(&self.store); - let mut store = self.0.store.lock().unwrap(); - let ty = func.ty(store.as_context()); - - // Check function signature. + // Check function signature. Do this lazily only when a function is called + // because there might be exported functions like `_initialize` that don't + // match the schema. if ty.params().iter().any(|&v| v != wasmi::core::ValType::I32) { bail!( - "plugin function `{name}` has a parameter that is not a 32-bit integer" + "plugin function `{func}` has a parameter that is not a 32-bit integer" ); } if ty.results() != [wasmi::core::ValType::I32] { - bail!("plugin function `{name}` does not return exactly one 32-bit integer"); + bail!("plugin function `{func}` does not return exactly one 32-bit integer"); } // Check inputs. @@ -260,23 +481,26 @@ impl Plugin { .collect::>(); // Store the input data. - store.data_mut().args = args; + self.store.data_mut().args = args; // Call the function. let mut code = wasmi::Val::I32(-1); - func.call(store.as_context_mut(), &lengths, std::slice::from_mut(&mut code)) + handle + .call(&mut self.store, &lengths, std::slice::from_mut(&mut code)) .map_err(|err| eco_format!("plugin panicked: {err}"))?; + if let Some(MemoryError { offset, length, write }) = - store.data_mut().memory_error.take() + self.store.data_mut().memory_error.take() { return Err(eco_format!( - "plugin tried to {kind} out of bounds: pointer {offset:#x} is out of bounds for {kind} of length {length}", + "plugin tried to {kind} out of bounds: \ + pointer {offset:#x} is out of bounds for {kind} of length {length}", kind = if write { "write" } else { "read" } )); } // Extract the returned data. - let output = std::mem::take(&mut store.data_mut().output); + let output = std::mem::take(&mut self.store.data_mut().output); // Parse the functions return value. match code { @@ -293,39 +517,63 @@ impl Plugin { Ok(Bytes::new(output)) } - /// An iterator over all the function names defined by the plugin. - pub fn iter(&self) -> impl Iterator { - self.0.functions.as_slice().iter().map(|(func_name, _)| func_name) + /// Creates a snapshot of this instance from which another one can be + /// initialized. + #[typst_macros::time(name = "save snapshot")] + fn snapshot(&self) -> Snapshot { + let memory = self.memory(); + let mem_pages = memory.size(&self.store); + let mem_data = memory.data(&self.store).to_vec(); + Snapshot { mem_pages, mem_data } + } + + /// Restores the instance to a snapshot. + #[typst_macros::time(name = "restore snapshot")] + fn restore(&mut self, snapshot: &Snapshot) { + let memory = self.memory(); + let current_size = memory.size(&self.store); + if current_size < snapshot.mem_pages { + memory + .grow(&mut self.store, snapshot.mem_pages - current_size) + .unwrap(); + } + + memory.data_mut(&mut self.store)[..snapshot.mem_data.len()] + .copy_from_slice(&snapshot.mem_data); + } + + /// Retrieves a handle to the plugin's main memory. + fn memory(&self) -> Memory { + self.instance + .get_export(&self.store, "memory") + .unwrap() + .into_memory() + .unwrap() } } -impl Debug for Plugin { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.pad("Plugin(..)") - } +/// The persistent store data used for communication between store and host. +#[derive(Default)] +struct CallData { + /// Arguments for a current call. + args: Vec, + /// The results of the current call. + output: Vec, + /// A memory error that occured during execution of the current call. + memory_error: Option, } -impl repr::Repr for Plugin { - fn repr(&self) -> EcoString { - "plugin(..)".into() - } -} - -impl PartialEq for Plugin { - fn eq(&self, other: &Self) -> bool { - self.0.bytes == other.0.bytes - } -} - -impl Hash for Plugin { - fn hash(&self, state: &mut H) { - self.0.bytes.hash(state); - } +/// If there was an error reading/writing memory, keep the offset + length to +/// display an error message. +struct MemoryError { + offset: u32, + length: u32, + write: bool, } /// Write the arguments to the plugin function into the plugin's memory. fn wasm_minimal_protocol_write_args_to_buffer( - mut caller: wasmi::Caller, + mut caller: wasmi::Caller, ptr: u32, ) { let memory = caller.get_export("memory").unwrap().into_memory().unwrap(); @@ -346,7 +594,7 @@ fn wasm_minimal_protocol_write_args_to_buffer( /// Extracts the output of the plugin function from the plugin's memory. fn wasm_minimal_protocol_send_result_to_host( - mut caller: wasmi::Caller, + mut caller: wasmi::Caller, ptr: u32, len: u32, ) { diff --git a/crates/typst-library/src/foundations/scope.rs b/crates/typst-library/src/foundations/scope.rs index 99c9a37e6..b7b4a6d9d 100644 --- a/crates/typst-library/src/foundations/scope.rs +++ b/crates/typst-library/src/foundations/scope.rs @@ -167,6 +167,14 @@ impl Scope { Default::default() } + /// Create a new scope with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self { + map: IndexMap::with_capacity(capacity), + ..Default::default() + } + } + /// Create a new scope with duplication prevention. pub fn deduplicating() -> Self { Self { deduplicate: true, ..Default::default() } diff --git a/crates/typst-library/src/foundations/value.rs b/crates/typst-library/src/foundations/value.rs index d99027728..4fa380b46 100644 --- a/crates/typst-library/src/foundations/value.rs +++ b/crates/typst-library/src/foundations/value.rs @@ -15,8 +15,8 @@ use crate::diag::{HintedStrResult, HintedString, StrResult}; 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, SymbolElem, Type, Version, + NativeElement, NativeType, NoneValue, Reflect, Repr, Resolve, Scope, Str, Styles, + Symbol, SymbolElem, Type, Version, }; use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel}; use crate::text::{RawContent, RawElem, TextElem}; @@ -84,8 +84,6 @@ pub enum Value { Type(Type), /// A module. Module(Module), - /// A WebAssembly plugin. - Plugin(Plugin), /// A dynamic value. Dyn(Dynamic), } @@ -147,7 +145,6 @@ impl Value { Self::Args(_) => Type::of::(), Self::Type(_) => Type::of::(), Self::Module(_) => Type::of::(), - Self::Plugin(_) => Type::of::(), Self::Dyn(v) => v.ty(), } } @@ -251,7 +248,6 @@ impl Debug for Value { Self::Args(v) => Debug::fmt(v, f), Self::Type(v) => Debug::fmt(v, f), Self::Module(v) => Debug::fmt(v, f), - Self::Plugin(v) => Debug::fmt(v, f), Self::Dyn(v) => Debug::fmt(v, f), } } @@ -289,7 +285,6 @@ impl Repr for Value { Self::Args(v) => v.repr(), Self::Type(v) => v.repr(), Self::Module(v) => v.repr(), - Self::Plugin(v) => v.repr(), Self::Dyn(v) => v.repr(), } } @@ -340,7 +335,6 @@ impl Hash for Value { Self::Args(v) => v.hash(state), Self::Type(v) => v.hash(state), Self::Module(v) => v.hash(state), - Self::Plugin(v) => v.hash(state), Self::Dyn(v) => v.hash(state), } } @@ -661,7 +655,6 @@ primitive! { primitive! { Args: "arguments", Args } primitive! { Type: "type", Type } primitive! { Module: "module", Module } -primitive! { Plugin: "plugin", Plugin } impl Reflect for Arc { fn input() -> CastInfo { diff --git a/tests/suite/foundations/plugin.typ b/tests/suite/foundations/plugin.typ index 0842980ec..9feacc030 100644 --- a/tests/suite/foundations/plugin.typ +++ b/tests/suite/foundations/plugin.typ @@ -9,6 +9,37 @@ bytes("value3-value1-value2"), ) +--- plugin-func --- +#let p = plugin("/assets/plugins/hello.wasm") +#test(type(p.hello), function) +#test(("a", "b").map(bytes).map(p.double_it), ("a.a", "b.b").map(bytes)) + +--- plugin-import --- +#import plugin("/assets/plugins/hello.wasm"): hello, double_it + +#test(hello(), bytes("Hello from wasm!!!")) +#test(double_it(bytes("hey!")), bytes("hey!.hey!")) + +--- plugin-transition --- +#let empty = plugin("/assets/plugins/hello-mut.wasm") +#test(str(empty.get()), "[]") + +#let hello = plugin.transition(empty.add, bytes("hello")) +#test(str(empty.get()), "[]") +#test(str(hello.get()), "[hello]") + +#let world = plugin.transition(empty.add, bytes("world")) +#let hello_you = plugin.transition(hello.add, bytes("you")) + +#test(str(empty.get()), "[]") +#test(str(hello.get()), "[hello]") +#test(str(world.get()), "[world]") +#test(str(hello_you.get()), "[hello, you]") + +#let hello2 = plugin.transition(empty.add, bytes("hello")) +#test(hello == world, false) +#test(hello == hello2, true) + --- plugin-wrong-number-of-arguments --- #let p = plugin("/assets/plugins/hello.wasm") diff --git a/tools/test-helper/package.json b/tools/test-helper/package.json index d34213fb0..08a60fa31 100644 --- a/tools/test-helper/package.json +++ b/tools/test-helper/package.json @@ -1,104 +1,107 @@ { - "name": "typst-test-helper", - "publisher": "typst", - "displayName": "Typst Test Helper", - "description": "Helps to run, compare and update Typst tests.", - "version": "0.0.1", - "categories": [ - "Other" - ], - "activationEvents": [ - "workspaceContains:tests/suite/playground.typ" - ], - "main": "./dist/extension.js", - "contributes": { - "commands": [ - { - "command": "typst-test-helper.refreshFromPreview", - "title": "Refresh preview", - "category": "Typst Test Helper", - "icon": "$(refresh)" - }, - { - "command": "typst-test-helper.runFromPreview", - "title": "Run test", - "category": "Typst Test Helper", - "icon": "$(debug-start)", - "enablement": "typst-test-helper.runButtonEnabled" - }, - { - "command": "typst-test-helper.saveFromPreview", - "title": "Run and save reference output", - "category": "Typst Test Helper", - "icon": "$(save)", - "enablement": "typst-test-helper.runButtonEnabled" - }, - { - "command": "typst-test-helper.copyImageFilePathFromPreviewContext", - "title": "Copy image file path", - "category": "Typst Test Helper" - }, - { - "command": "typst-test-helper.increaseResolution", - "title": "Render at higher resolution", - "category": "Typst Test Helper", - "icon": "$(zoom-in)", - "enablement": "typst-test-helper.runButtonEnabled" - }, - { - "command": "typst-test-helper.decreaseResolution", - "title": "Render at lower resolution", - "category": "Typst Test Helper", - "icon": "$(zoom-out)", - "enablement": "typst-test-helper.runButtonEnabled" - } - ], - "menus": { - "editor/title": [ - { - "when": "activeWebviewPanelId == typst-test-helper.preview", - "command": "typst-test-helper.refreshFromPreview", - "group": "navigation@1" - }, - { - "when": "activeWebviewPanelId == typst-test-helper.preview", - "command": "typst-test-helper.runFromPreview", - "group": "navigation@2" - }, - { - "when": "activeWebviewPanelId == typst-test-helper.preview", - "command": "typst-test-helper.saveFromPreview", - "group": "navigation@3" - }, - { - "when": "activeWebviewPanelId == typst-test-helper.preview", - "command": "typst-test-helper.increaseResolution", - "group": "navigation@4" - }, - { - "when": "activeWebviewPanelId == typst-test-helper.preview", - "command": "typst-test-helper.decreaseResolution", - "group": "navigation@4" - } - ], - "webview/context": [ - { - "command": "typst-test-helper.copyImageFilePathFromPreviewContext", - "when": "webviewId == typst-test-helper.preview && (webviewSection == png || webviewSection == ref)" - } - ] - } - }, - "scripts": { - "build": "tsc -p ./", - "watch": "tsc -watch -p ./" - }, - "devDependencies": { - "@types/node": "18.x", - "@types/vscode": "^1.88.0", - "typescript": "^5.3.3" - }, - "engines": { - "vscode": "^1.88.0" - } -} + "name": "typst-test-helper", + "publisher": "typst", + "displayName": "Typst Test Helper", + "description": "Helps to run, compare and update Typst tests.", + "version": "0.0.1", + "categories": [ + "Other" + ], + "activationEvents": [ + "workspaceContains:tests/suite/playground.typ" + ], + "main": "./dist/extension.js", + "contributes": { + "commands": [ + { + "command": "typst-test-helper.refreshFromPreview", + "title": "Refresh preview", + "category": "Typst Test Helper", + "icon": "$(refresh)" + }, + { + "command": "typst-test-helper.runFromPreview", + "title": "Run test", + "category": "Typst Test Helper", + "icon": "$(debug-start)", + "enablement": "typst-test-helper.runButtonEnabled" + }, + { + "command": "typst-test-helper.saveFromPreview", + "title": "Run and save reference output", + "category": "Typst Test Helper", + "icon": "$(save)", + "enablement": "typst-test-helper.runButtonEnabled" + }, + { + "command": "typst-test-helper.copyImageFilePathFromPreviewContext", + "title": "Copy image file path", + "category": "Typst Test Helper" + }, + { + "command": "typst-test-helper.increaseResolution", + "title": "Render at higher resolution", + "category": "Typst Test Helper", + "icon": "$(zoom-in)", + "enablement": "typst-test-helper.runButtonEnabled" + }, + { + "command": "typst-test-helper.decreaseResolution", + "title": "Render at lower resolution", + "category": "Typst Test Helper", + "icon": "$(zoom-out)", + "enablement": "typst-test-helper.runButtonEnabled" + } + ], + "menus": { + "editor/title": [ + { + "when": "activeWebviewPanelId == typst-test-helper.preview", + "command": "typst-test-helper.refreshFromPreview", + "group": "navigation@1" + }, + { + "when": "activeWebviewPanelId == typst-test-helper.preview", + "command": "typst-test-helper.runFromPreview", + "group": "navigation@2" + }, + { + "when": "activeWebviewPanelId == typst-test-helper.preview", + "command": "typst-test-helper.saveFromPreview", + "group": "navigation@3" + }, + { + "when": "activeWebviewPanelId == typst-test-helper.preview", + "command": "typst-test-helper.increaseResolution", + "group": "navigation@4" + }, + { + "when": "activeWebviewPanelId == typst-test-helper.preview", + "command": "typst-test-helper.decreaseResolution", + "group": "navigation@4" + } + ], + "webview/context": [ + { + "command": "typst-test-helper.copyImageFilePathFromPreviewContext", + "when": "webviewId == typst-test-helper.preview && (webviewSection == png || webviewSection == ref)" + } + ] + } + }, + "scripts": { + "build": "tsc -p ./", + "watch": "tsc -watch -p ./" + }, + "devDependencies": { + "@types/node": "18.x", + "@types/vscode": "^1.88.0", + "typescript": "^5.3.3" + }, + "engines": { + "vscode": "^1.88.0" + }, + "__metadata": { + "size": 35098973 + } +} \ No newline at end of file From 3eb6e87af1d8870a38cc5914e345d07373e1e8c1 Mon Sep 17 00:00:00 2001 From: frozolotl <44589151+frozolotl@users.noreply.github.com> Date: Fri, 31 Jan 2025 10:56:25 +0100 Subject: [PATCH 31/34] Include images from raw pixmaps and more (#5632) Co-authored-by: PgBiel <9021226+PgBiel@users.noreply.github.com> Co-authored-by: Laurenz --- Cargo.lock | 5 +- Cargo.toml | 2 +- crates/typst-layout/src/image.rs | 41 ++- crates/typst-library/src/text/font/color.rs | 22 +- .../typst-library/src/visualize/image/mod.rs | 216 ++++++++++----- .../src/visualize/image/raster.rs | 261 ++++++++++++++---- .../typst-library/src/visualize/image/svg.rs | 2 + crates/typst-pdf/src/image.rs | 163 ++++++----- crates/typst-render/src/image.rs | 40 ++- crates/typst-svg/Cargo.toml | 1 + crates/typst-svg/src/image.rs | 48 +++- crates/typst-svg/src/text.rs | 10 +- tests/ref/baseline-box.png | Bin 3896 -> 4021 bytes tests/ref/box-clip-outset.png | Bin 1442 -> 1492 bytes tests/ref/box-clip-radius-without-stroke.png | Bin 1225 -> 1255 bytes tests/ref/box-clip-radius.png | Bin 1245 -> 1250 bytes .../closure-path-resolve-in-layout-phase.png | Bin 2193 -> 2256 bytes tests/ref/coma.png | Bin 28740 -> 28615 bytes tests/ref/footnote-in-caption.png | Bin 6111 -> 6154 bytes tests/ref/footnote-in-table.png | Bin 12380 -> 12727 bytes tests/ref/image-baseline-with-box.png | Bin 6375 -> 6375 bytes tests/ref/image-decode-detect-format.png | Bin 10648 -> 11032 bytes tests/ref/image-decode-specify-format.png | Bin 10648 -> 11032 bytes tests/ref/image-fit.png | Bin 10302 -> 10390 bytes tests/ref/image-pixmap-luma8.png | Bin 0 -> 321 bytes tests/ref/image-pixmap-lumaa8.png | Bin 0 -> 299 bytes tests/ref/image-pixmap-rgb8.png | Bin 0 -> 1220 bytes tests/ref/image-pixmap-rgba8.png | Bin 0 -> 854 bytes tests/ref/image-scaling-methods.png | Bin 0 -> 1539 bytes tests/ref/image-sizing.png | Bin 8662 -> 8925 bytes tests/ref/issue-4361-transparency-leak.png | Bin 3515 -> 3738 bytes tests/ref/pad-followed-by-content.png | Bin 11897 -> 12071 bytes tests/suite/visualize/image.typ | 128 +++++++++ 33 files changed, 689 insertions(+), 250 deletions(-) create mode 100644 tests/ref/image-pixmap-luma8.png create mode 100644 tests/ref/image-pixmap-lumaa8.png create mode 100644 tests/ref/image-pixmap-rgb8.png create mode 100644 tests/ref/image-pixmap-rgba8.png create mode 100644 tests/ref/image-scaling-methods.png diff --git a/Cargo.lock b/Cargo.lock index 3343c246b..ada3a3d4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1122,9 +1122,9 @@ checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" [[package]] name = "image" -version = "0.25.2" +version = "0.25.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" +checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" dependencies = [ "bytemuck", "byteorder-lite", @@ -3036,6 +3036,7 @@ dependencies = [ "comemo", "ecow", "flate2", + "image", "ttf-parser", "typst-library", "typst-macros", diff --git a/Cargo.toml b/Cargo.toml index 6b592cd39..d03bfa6d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,7 +67,7 @@ icu_provider_adapters = "1.4" icu_provider_blob = "1.4" icu_segmenter = { version = "1.4", features = ["serde"] } if_chain = "1" -image = { version = "0.25.2", default-features = false, features = ["png", "jpeg", "gif"] } +image = { version = "0.25.5", default-features = false, features = ["png", "jpeg", "gif"] } indexmap = { version = "2", features = ["serde"] } kamadak-exif = "0.5" kurbo = "0.11" diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index e521b993f..503c30820 100644 --- a/crates/typst-layout/src/image.rs +++ b/crates/typst-layout/src/image.rs @@ -10,7 +10,8 @@ use typst_library::layout::{ use typst_library::loading::DataSource; use typst_library::text::families; use typst_library::visualize::{ - Curve, Image, ImageElem, ImageFit, ImageFormat, RasterFormat, VectorFormat, + Curve, ExchangeFormat, Image, ImageElem, ImageFit, ImageFormat, ImageKind, + RasterImage, SvgImage, VectorFormat, }; /// Layout the image. @@ -49,15 +50,27 @@ pub fn layout_image( } // Construct the image itself. - let image = Image::with_fonts( - data.clone(), - format, - elem.alt(styles), - engine.world, - &families(styles).map(|f| f.as_str()).collect::>(), - elem.flatten_text(styles), - ) - .at(span)?; + let kind = match format { + ImageFormat::Raster(format) => ImageKind::Raster( + RasterImage::new( + data.clone(), + format, + elem.icc(styles).as_ref().map(|icc| icc.derived.clone()), + ) + .at(span)?, + ), + ImageFormat::Vector(VectorFormat::Svg) => ImageKind::Svg( + SvgImage::with_fonts( + data.clone(), + engine.world, + elem.flatten_text(styles), + &families(styles).map(|f| f.as_str()).collect::>(), + ) + .at(span)?, + ), + }; + + let image = Image::new(kind, elem.alt(styles), elem.scaling(styles)); // Determine the image's pixel aspect ratio. let pxw = image.width(); @@ -129,10 +142,10 @@ fn determine_format(source: &DataSource, data: &Bytes) -> StrResult .to_lowercase(); 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)), + "png" => return Ok(ExchangeFormat::Png.into()), + "jpg" | "jpeg" => return Ok(ExchangeFormat::Jpg.into()), + "gif" => return Ok(ExchangeFormat::Gif.into()), + "svg" | "svgz" => return Ok(VectorFormat::Svg.into()), _ => {} } } diff --git a/crates/typst-library/src/text/font/color.rs b/crates/typst-library/src/text/font/color.rs index e3183e885..0a7b13c97 100644 --- a/crates/typst-library/src/text/font/color.rs +++ b/crates/typst-library/src/text/font/color.rs @@ -10,7 +10,9 @@ use xmlwriter::XmlWriter; use crate::foundations::Bytes; use crate::layout::{Abs, Frame, FrameItem, Point, Size}; use crate::text::{Font, Glyph}; -use crate::visualize::{FixedStroke, Geometry, Image, RasterFormat, VectorFormat}; +use crate::visualize::{ + ExchangeFormat, FixedStroke, Geometry, Image, RasterImage, SvgImage, +}; /// Whether this glyph should be rendered via simple outlining instead of via /// `glyph_frame`. @@ -102,12 +104,8 @@ fn draw_raster_glyph( upem: Abs, raster_image: ttf_parser::RasterGlyphImage, ) -> Option<()> { - let image = Image::new( - Bytes::new(raster_image.data.to_vec()), - RasterFormat::Png.into(), - None, - ) - .ok()?; + let data = Bytes::new(raster_image.data.to_vec()); + let image = Image::plain(RasterImage::plain(data, ExchangeFormat::Png).ok()?); // Apple Color emoji doesn't provide offset information (or at least // not in a way ttf-parser understands), so we artificially shift their @@ -178,9 +176,8 @@ fn draw_colr_glyph( ttf.paint_color_glyph(glyph_id, 0, RgbaColor::new(0, 0, 0, 255), &mut glyph_painter)?; svg.end_element(); - let data = svg.end_document().into_bytes(); - - let image = Image::new(Bytes::new(data), VectorFormat::Svg.into(), None).ok()?; + let data = Bytes::from_string(svg.end_document()); + let image = Image::plain(SvgImage::new(data).ok()?); let y_shift = Abs::pt(upem.to_pt() - y_max); let position = Point::new(Abs::pt(x_min), y_shift); @@ -255,9 +252,8 @@ fn draw_svg_glyph( ty = -top, ); - let image = - Image::new(Bytes::new(wrapper_svg.into_bytes()), VectorFormat::Svg.into(), None) - .ok()?; + let data = Bytes::from_string(wrapper_svg); + let image = Image::plain(SvgImage::new(data).ok()?); let position = Point::new(Abs::pt(left), Abs::pt(top) + upem); let size = Size::new(Abs::pt(width), Abs::pt(height)); diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 77f8426e4..0e5c9e329 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -3,13 +3,14 @@ mod raster; mod svg; -pub use self::raster::{RasterFormat, RasterImage}; +pub use self::raster::{ + ExchangeFormat, PixelEncoding, PixelFormat, RasterFormat, RasterImage, +}; pub use self::svg::SvgImage; use std::fmt::{self, Debug, Formatter}; use std::sync::Arc; -use comemo::Tracked; use ecow::EcoString; use typst_syntax::{Span, Spanned}; use typst_utils::LazyHash; @@ -24,7 +25,6 @@ use crate::layout::{BlockElem, Length, Rel, Sizing}; use crate::loading::{DataSource, Load, Readable}; use crate::model::Figurable; use crate::text::LocalName; -use crate::World; /// A raster or vector graphic. /// @@ -46,7 +46,8 @@ use crate::World; /// ``` #[elem(scope, Show, LocalName, Figurable)] pub struct ImageElem { - /// A path to an image file or raw bytes making up an encoded image. + /// A path to an image file or raw bytes making up an image in one of the + /// supported [formats]($image.format). /// /// For more details about paths, see the [Paths section]($syntax/#paths). #[required] @@ -57,10 +58,50 @@ pub struct ImageElem { )] pub source: Derived, - /// The image's format. Detected automatically by default. + /// The image's format. /// - /// Supported formats are PNG, JPEG, GIF, and SVG. Using a PDF as an image - /// is [not currently supported](https://github.com/typst/typst/issues/145). + /// By default, the format is detected automatically. Typically, you thus + /// only need to specify this when providing raw bytes as the + /// [`source`]($image.source) (even then, Typst will try to figure out the + /// format automatically, but that's not always possible). + /// + /// Supported formats are `{"png"}`, `{"jpg"}`, `{"gif"}`, `{"svg"}` as well + /// as raw pixel data. Embedding PDFs as images is + /// [not currently supported](https://github.com/typst/typst/issues/145). + /// + /// When providing raw pixel data as the `source`, you must specify a + /// dictionary with the following keys as the `format`: + /// - `encoding` ([str]): The encoding of the pixel data. One of: + /// - `{"rgb8"}` (three 8-bit channels: red, green, blue) + /// - `{"rgba8"}` (four 8-bit channels: red, green, blue, alpha) + /// - `{"luma8"}` (one 8-bit channel) + /// - `{"lumaa8"}` (two 8-bit channels: luma and alpha) + /// - `width` ([int]): The pixel width of the image. + /// - `height` ([int]): The pixel height of the image. + /// + /// The pixel width multiplied by the height multiplied by the channel count + /// for the specified encoding must then match the `source` data. + /// + /// ```example + /// #image( + /// read( + /// "tetrahedron.svg", + /// encoding: none, + /// ), + /// format: "svg", + /// width: 2cm, + /// ) + /// + /// #image( + /// bytes(range(16).map(x => x * 16)), + /// format: ( + /// encoding: "luma8", + /// width: 4, + /// height: 4, + /// ), + /// width: 2cm, + /// ) + /// ``` pub format: Smart, /// The width of the image. @@ -86,6 +127,30 @@ pub struct ImageElem { #[default(ImageFit::Cover)] pub fit: ImageFit, + /// A hint to viewers how they should scale the image. + /// + /// When set to `{auto}`, the default is left up to the viewer. For PNG + /// export, Typst will default to smooth scaling, like most PDF and SVG + /// viewers. + /// + /// _Note:_ The exact look may differ across PDF viewers. + pub scaling: Smart, + + /// An ICC profile for the image. + /// + /// ICC profiles define how to interpret the colors in an image. When set + /// to `{auto}`, Typst will try to extract an ICC profile from the image. + #[parse(match args.named::>>("icc")? { + Some(Spanned { v: Smart::Custom(source), span }) => Some(Smart::Custom({ + let data = Spanned::new(&source, span).load(engine.world)?; + Derived::new(source, data) + })), + Some(Spanned { v: Smart::Auto, .. }) => Some(Smart::Auto), + None => None, + })] + #[borrowed] + pub icc: Smart>, + /// Whether text in SVG images should be converted into curves before /// embedding. This will result in the text becoming unselectable in the /// output. @@ -94,6 +159,7 @@ pub struct ImageElem { } #[scope] +#[allow(clippy::too_many_arguments)] impl ImageElem { /// Decode a raster or vector graphic from bytes or a string. /// @@ -130,6 +196,13 @@ impl ImageElem { /// How the image should adjust itself to a given area. #[named] fit: Option, + /// A hint to viewers how they should scale the image. + #[named] + scaling: Option>, + /// Whether text in SVG images should be converted into curves before + /// embedding. + #[named] + flatten_text: Option, ) -> StrResult { let bytes = data.into_bytes(); let source = Derived::new(DataSource::Bytes(bytes.clone()), bytes); @@ -149,6 +222,12 @@ impl ImageElem { if let Some(fit) = fit { elem.push_fit(fit); } + if let Some(scaling) = scaling { + elem.push_scaling(scaling); + } + if let Some(flatten_text) = flatten_text { + elem.push_flatten_text(flatten_text); + } Ok(elem.pack().spanned(span)) } } @@ -199,15 +278,8 @@ struct Repr { kind: ImageKind, /// A text describing the image. alt: Option, -} - -/// A kind of image. -#[derive(Hash)] -pub enum ImageKind { - /// A raster image. - Raster(RasterImage), - /// An SVG image. - Svg(SvgImage), + /// The scaling algorithm to use. + scaling: Smart, } impl Image { @@ -218,55 +290,29 @@ impl Image { /// Should always be the same as the default DPI used by usvg. pub const USVG_DEFAULT_DPI: f64 = 96.0; - /// Create an image from a buffer and a format. - #[comemo::memoize] - #[typst_macros::time(name = "load image")] + /// Create an image from a `RasterImage` or `SvgImage`. pub fn new( - data: Bytes, - format: ImageFormat, + kind: impl Into, alt: Option, - ) -> StrResult { - let kind = match format { - ImageFormat::Raster(format) => { - ImageKind::Raster(RasterImage::new(data, format)?) - } - ImageFormat::Vector(VectorFormat::Svg) => { - ImageKind::Svg(SvgImage::new(data)?) - } - }; - - Ok(Self(Arc::new(LazyHash::new(Repr { kind, alt })))) + scaling: Smart, + ) -> Self { + Self::new_impl(kind.into(), alt, scaling) } - /// Create a possibly font-dependent image from a buffer and a format. + /// Create an image with optional properties set to the default. + pub fn plain(kind: impl Into) -> Self { + Self::new(kind, None, Smart::Auto) + } + + /// The internal, non-generic implementation. This is memoized to reuse + /// the `Arc` and `LazyHash`. #[comemo::memoize] - #[typst_macros::time(name = "load image")] - pub fn with_fonts( - data: Bytes, - format: ImageFormat, + fn new_impl( + kind: ImageKind, alt: Option, - world: Tracked, - families: &[&str], - flatten_text: bool, - ) -> StrResult { - let kind = match format { - ImageFormat::Raster(format) => { - ImageKind::Raster(RasterImage::new(data, format)?) - } - ImageFormat::Vector(VectorFormat::Svg) => { - ImageKind::Svg(SvgImage::with_fonts(data, world, flatten_text, families)?) - } - }; - - Ok(Self(Arc::new(LazyHash::new(Repr { kind, alt })))) - } - - /// The raw image data. - pub fn data(&self) -> &Bytes { - match &self.0.kind { - ImageKind::Raster(raster) => raster.data(), - ImageKind::Svg(svg) => svg.data(), - } + scaling: Smart, + ) -> Image { + Self(Arc::new(LazyHash::new(Repr { kind, alt, scaling }))) } /// The format of the image. @@ -306,6 +352,11 @@ impl Image { self.0.alt.as_deref() } + /// The image scaling algorithm to use for this image. + pub fn scaling(&self) -> Smart { + self.0.scaling + } + /// The decoded image. pub fn kind(&self) -> &ImageKind { &self.0.kind @@ -319,10 +370,32 @@ impl Debug for Image { .field("width", &self.width()) .field("height", &self.height()) .field("alt", &self.alt()) + .field("scaling", &self.scaling()) .finish() } } +/// A kind of image. +#[derive(Clone, Hash)] +pub enum ImageKind { + /// A raster image. + Raster(RasterImage), + /// An SVG image. + Svg(SvgImage), +} + +impl From for ImageKind { + fn from(image: RasterImage) -> Self { + Self::Raster(image) + } +} + +impl From for ImageKind { + fn from(image: SvgImage) -> Self { + Self::Svg(image) + } +} + /// A raster or vector image format. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum ImageFormat { @@ -335,8 +408,8 @@ pub enum ImageFormat { 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)); + if let Some(format) = ExchangeFormat::detect(data) { + return Some(Self::Raster(RasterFormat::Exchange(format))); } // SVG or compressed SVG. @@ -355,9 +428,12 @@ pub enum VectorFormat { Svg, } -impl From for ImageFormat { - fn from(format: RasterFormat) -> Self { - Self::Raster(format) +impl From for ImageFormat +where + R: Into, +{ + fn from(format: R) -> Self { + Self::Raster(format.into()) } } @@ -371,8 +447,18 @@ cast! { ImageFormat, self => match self { Self::Raster(v) => v.into_value(), - Self::Vector(v) => v.into_value() + Self::Vector(v) => v.into_value(), }, v: RasterFormat => Self::Raster(v), v: VectorFormat => Self::Vector(v), } + +/// The image scaling algorithm a viewer should use. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum ImageScaling { + /// Scale with a smoothing algorithm such as bilinear interpolation. + Smooth, + /// Scale with nearest neighbor or a similar algorithm to preserve the + /// pixelated look of the image. + Pixelated, +} diff --git a/crates/typst-library/src/visualize/image/raster.rs b/crates/typst-library/src/visualize/image/raster.rs index 098843a25..d43b15486 100644 --- a/crates/typst-library/src/visualize/image/raster.rs +++ b/crates/typst-library/src/visualize/image/raster.rs @@ -7,10 +7,12 @@ use ecow::{eco_format, EcoString}; use image::codecs::gif::GifDecoder; use image::codecs::jpeg::JpegDecoder; use image::codecs::png::PngDecoder; -use image::{guess_format, DynamicImage, ImageDecoder, ImageResult, Limits}; +use image::{ + guess_format, DynamicImage, ImageBuffer, ImageDecoder, ImageResult, Limits, Pixel, +}; use crate::diag::{bail, StrResult}; -use crate::foundations::{Bytes, Cast}; +use crate::foundations::{cast, dict, Bytes, Cast, Dict, Smart, Value}; /// A decoded raster image. #[derive(Clone, Hash)] @@ -21,43 +23,118 @@ struct Repr { data: Bytes, format: RasterFormat, dynamic: image::DynamicImage, - icc: Option>, + icc: Option, dpi: Option, } impl RasterImage { /// Decode a raster image. + pub fn new( + data: Bytes, + format: impl Into, + icc: Smart, + ) -> StrResult { + Self::new_impl(data, format.into(), icc) + } + + /// Create a raster image with optional properties set to the default. + pub fn plain(data: Bytes, format: impl Into) -> StrResult { + Self::new(data, format, Smart::Auto) + } + + /// The internal, non-generic implementation. #[comemo::memoize] - pub fn new(data: Bytes, format: RasterFormat) -> StrResult { - fn decode_with( - decoder: ImageResult, - ) -> ImageResult<(image::DynamicImage, Option>)> { - let mut decoder = decoder?; - let icc = decoder.icc_profile().ok().flatten().filter(|icc| !icc.is_empty()); - decoder.set_limits(Limits::default())?; - let dynamic = image::DynamicImage::from_decoder(decoder)?; - Ok((dynamic, icc)) - } + #[typst_macros::time(name = "load raster image")] + fn new_impl( + data: Bytes, + format: RasterFormat, + icc: Smart, + ) -> StrResult { + let (dynamic, icc, dpi) = match format { + RasterFormat::Exchange(format) => { + fn decode( + decoder: ImageResult, + icc: Smart, + ) -> ImageResult<(image::DynamicImage, Option)> { + let mut decoder = decoder?; + let icc = icc.custom().or_else(|| { + decoder + .icc_profile() + .ok() + .flatten() + .filter(|icc| !icc.is_empty()) + .map(Bytes::new) + }); + decoder.set_limits(Limits::default())?; + let dynamic = image::DynamicImage::from_decoder(decoder)?; + Ok((dynamic, icc)) + } - let cursor = io::Cursor::new(&data); - let (mut dynamic, icc) = match format { - RasterFormat::Jpg => decode_with(JpegDecoder::new(cursor)), - RasterFormat::Png => decode_with(PngDecoder::new(cursor)), - RasterFormat::Gif => decode_with(GifDecoder::new(cursor)), - } - .map_err(format_image_error)?; + let cursor = io::Cursor::new(&data); + let (mut dynamic, icc) = match format { + ExchangeFormat::Jpg => decode(JpegDecoder::new(cursor), icc), + ExchangeFormat::Png => decode(PngDecoder::new(cursor), icc), + ExchangeFormat::Gif => decode(GifDecoder::new(cursor), icc), + } + .map_err(format_image_error)?; - let exif = exif::Reader::new() - .read_from_container(&mut std::io::Cursor::new(&data)) - .ok(); + let exif = exif::Reader::new() + .read_from_container(&mut std::io::Cursor::new(&data)) + .ok(); - // Apply rotation from EXIF metadata. - if let Some(rotation) = exif.as_ref().and_then(exif_rotation) { - apply_rotation(&mut dynamic, rotation); - } + // Apply rotation from EXIF metadata. + if let Some(rotation) = exif.as_ref().and_then(exif_rotation) { + apply_rotation(&mut dynamic, rotation); + } - // Extract pixel density. - let dpi = determine_dpi(&data, exif.as_ref()); + // Extract pixel density. + let dpi = determine_dpi(&data, exif.as_ref()); + + (dynamic, icc, dpi) + } + + RasterFormat::Pixel(format) => { + if format.width == 0 || format.height == 0 { + bail!("zero-sized images are not allowed"); + } + + let channels = match format.encoding { + PixelEncoding::Rgb8 => 3, + PixelEncoding::Rgba8 => 4, + PixelEncoding::Luma8 => 1, + PixelEncoding::Lumaa8 => 2, + }; + + let Some(expected_size) = format + .width + .checked_mul(format.height) + .and_then(|size| size.checked_mul(channels)) + else { + bail!("pixel dimensions are too large"); + }; + + if expected_size as usize != data.len() { + bail!("pixel dimensions and pixel data do not match"); + } + + fn to>( + data: &Bytes, + format: PixelFormat, + ) -> ImageBuffer> { + ImageBuffer::from_raw(format.width, format.height, data.to_vec()) + .unwrap() + } + + let dynamic = match format.encoding { + PixelEncoding::Rgb8 => to::>(&data, format).into(), + PixelEncoding::Rgba8 => to::>(&data, format).into(), + PixelEncoding::Luma8 => to::>(&data, format).into(), + PixelEncoding::Lumaa8 => to::>(&data, format).into(), + }; + + (dynamic, icc.custom(), None) + } + }; Ok(Self(Arc::new(Repr { data, format, dynamic, icc, dpi }))) } @@ -93,60 +170,141 @@ impl RasterImage { } /// Access the ICC profile, if any. - pub fn icc(&self) -> Option<&[u8]> { - self.0.icc.as_deref() + pub fn icc(&self) -> Option<&Bytes> { + self.0.icc.as_ref() } } impl Hash for Repr { fn hash(&self, state: &mut H) { - // The image is fully defined by data and format. + // The image is fully defined by data, format, and ICC profile. self.data.hash(state); self.format.hash(state); + self.icc.hash(state); } } /// A raster graphics format. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum RasterFormat { + /// A format typically used in image exchange. + Exchange(ExchangeFormat), + /// A format of raw pixel data. + Pixel(PixelFormat), +} + +impl From for RasterFormat { + fn from(format: ExchangeFormat) -> Self { + Self::Exchange(format) + } +} + +impl From for RasterFormat { + fn from(format: PixelFormat) -> Self { + Self::Pixel(format) + } +} + +cast! { + RasterFormat, + self => match self { + Self::Exchange(v) => v.into_value(), + Self::Pixel(v) => v.into_value(), + }, + v: ExchangeFormat => Self::Exchange(v), + v: PixelFormat => Self::Pixel(v), +} + +/// A raster format typically used in image exchange, with efficient encoding. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum ExchangeFormat { /// Raster format for illustrations and transparent graphics. Png, /// Lossy raster format suitable for photos. Jpg, - /// Raster format that is typically used for short animated clips. + /// Raster format that is typically used for short animated clips. Typst can + /// load GIFs, but they will become static. Gif, } -impl RasterFormat { +impl ExchangeFormat { /// Try to detect the format of data in a buffer. pub fn detect(data: &[u8]) -> Option { guess_format(data).ok().and_then(|format| format.try_into().ok()) } } -impl From for image::ImageFormat { - fn from(format: RasterFormat) -> Self { +impl From for image::ImageFormat { + fn from(format: ExchangeFormat) -> Self { match format { - RasterFormat::Png => image::ImageFormat::Png, - RasterFormat::Jpg => image::ImageFormat::Jpeg, - RasterFormat::Gif => image::ImageFormat::Gif, + ExchangeFormat::Png => image::ImageFormat::Png, + ExchangeFormat::Jpg => image::ImageFormat::Jpeg, + ExchangeFormat::Gif => image::ImageFormat::Gif, } } } -impl TryFrom for RasterFormat { +impl TryFrom for ExchangeFormat { type Error = EcoString; fn try_from(format: image::ImageFormat) -> StrResult { Ok(match format { - image::ImageFormat::Png => RasterFormat::Png, - image::ImageFormat::Jpeg => RasterFormat::Jpg, - image::ImageFormat::Gif => RasterFormat::Gif, - _ => bail!("Format not yet supported."), + image::ImageFormat::Png => ExchangeFormat::Png, + image::ImageFormat::Jpeg => ExchangeFormat::Jpg, + image::ImageFormat::Gif => ExchangeFormat::Gif, + _ => bail!("format not yet supported"), }) } } +/// Information that is needed to understand a pixmap buffer. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct PixelFormat { + /// The channel encoding. + encoding: PixelEncoding, + /// The pixel width. + width: u32, + /// The pixel height. + height: u32, +} + +/// Determines the channel encoding of raw pixel data. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum PixelEncoding { + /// Three 8-bit channels: Red, green, blue. + Rgb8, + /// Four 8-bit channels: Red, green, blue, alpha. + Rgba8, + /// One 8-bit channel. + Luma8, + /// Two 8-bit channels: Luma and alpha. + Lumaa8, +} + +cast! { + PixelFormat, + self => Value::Dict(self.into()), + mut dict: Dict => { + let format = Self { + encoding: dict.take("encoding")?.cast()?, + width: dict.take("width")?.cast()?, + height: dict.take("height")?.cast()?, + }; + dict.finish(&["encoding", "width", "height"])?; + format + } +} + +impl From for Dict { + fn from(format: PixelFormat) -> Self { + dict! { + "encoding" => format.encoding, + "width" => format.width, + "height" => format.height, + } + } +} + /// Try to get the rotation from the EXIF metadata. fn exif_rotation(exif: &exif::Exif) -> Option { exif.get_field(exif::Tag::Orientation, exif::In::PRIMARY)? @@ -266,21 +424,20 @@ fn format_image_error(error: image::ImageError) -> EcoString { #[cfg(test)] mod tests { - use super::{RasterFormat, RasterImage}; - use crate::foundations::Bytes; + use super::*; #[test] fn test_image_dpi() { #[track_caller] - fn test(path: &str, format: RasterFormat, dpi: f64) { + fn test(path: &str, format: ExchangeFormat, dpi: f64) { let data = typst_dev_assets::get(path).unwrap(); let bytes = Bytes::new(data); - let image = RasterImage::new(bytes, format).unwrap(); + let image = RasterImage::plain(bytes, format).unwrap(); assert_eq!(image.dpi().map(f64::round), Some(dpi)); } - test("images/f2t.jpg", RasterFormat::Jpg, 220.0); - test("images/tiger.jpg", RasterFormat::Jpg, 72.0); - test("images/graph.png", RasterFormat::Png, 144.0); + test("images/f2t.jpg", ExchangeFormat::Jpg, 220.0); + test("images/tiger.jpg", ExchangeFormat::Jpg, 72.0); + test("images/graph.png", ExchangeFormat::Png, 144.0); } } diff --git a/crates/typst-library/src/visualize/image/svg.rs b/crates/typst-library/src/visualize/image/svg.rs index 089f05430..dcc55077b 100644 --- a/crates/typst-library/src/visualize/image/svg.rs +++ b/crates/typst-library/src/visualize/image/svg.rs @@ -30,6 +30,7 @@ struct Repr { impl SvgImage { /// Decode an SVG image without fonts. #[comemo::memoize] + #[typst_macros::time(name = "load svg")] pub fn new(data: Bytes) -> StrResult { let tree = usvg::Tree::from_data(&data, &base_options()).map_err(format_usvg_error)?; @@ -44,6 +45,7 @@ impl SvgImage { /// Decode an SVG image with access to fonts. #[comemo::memoize] + #[typst_macros::time(name = "load svg")] pub fn with_fonts( data: Bytes, world: Tracked, diff --git a/crates/typst-pdf/src/image.rs b/crates/typst-pdf/src/image.rs index bff7bfefa..550f60a4b 100644 --- a/crates/typst-pdf/src/image.rs +++ b/crates/typst-pdf/src/image.rs @@ -5,8 +5,10 @@ use ecow::eco_format; use image::{DynamicImage, GenericImageView, Rgba}; use pdf_writer::{Chunk, Filter, Finish, Ref}; use typst_library::diag::{At, SourceResult, StrResult}; +use typst_library::foundations::Smart; use typst_library::visualize::{ - ColorSpace, Image, ImageKind, RasterFormat, RasterImage, SvgImage, + ColorSpace, ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat, + RasterImage, SvgImage, }; use typst_utils::Deferred; @@ -32,11 +34,13 @@ pub fn write_images( EncodedImage::Raster { data, filter, - has_color, + color_space, + bits_per_component, width, height, - icc, + compressed_icc, alpha, + interpolate, } => { let image_ref = chunk.alloc(); out.insert(image.clone(), image_ref); @@ -45,23 +49,18 @@ pub fn write_images( image.filter(*filter); image.width(*width as i32); image.height(*height as i32); - image.bits_per_component(8); + image.bits_per_component(i32::from(*bits_per_component)); + image.interpolate(*interpolate); let mut icc_ref = None; let space = image.color_space(); - if icc.is_some() { + if compressed_icc.is_some() { let id = chunk.alloc.bump(); space.icc_based(id); icc_ref = Some(id); - } else if *has_color { - color::write( - ColorSpace::Srgb, - space, - &context.globals.color_functions, - ); } else { color::write( - ColorSpace::D65Gray, + *color_space, space, &context.globals.color_functions, ); @@ -79,20 +78,27 @@ pub fn write_images( mask.width(*width as i32); mask.height(*height as i32); mask.color_space().device_gray(); - mask.bits_per_component(8); + mask.bits_per_component(i32::from(*bits_per_component)); + mask.interpolate(*interpolate); } else { image.finish(); } - if let (Some(icc), Some(icc_ref)) = (icc, icc_ref) { - let mut stream = chunk.icc_profile(icc_ref, icc); + if let (Some(compressed_icc), Some(icc_ref)) = + (compressed_icc, icc_ref) + { + let mut stream = chunk.icc_profile(icc_ref, compressed_icc); stream.filter(Filter::FlateDecode); - if *has_color { - stream.n(3); - stream.alternate().srgb(); - } else { - stream.n(1); - stream.alternate().d65_gray(); + match color_space { + ColorSpace::Srgb => { + stream.n(3); + stream.alternate().srgb(); + } + ColorSpace::D65Gray => { + stream.n(1); + stream.alternate().d65_gray(); + } + _ => unimplemented!(), } } } @@ -122,35 +128,17 @@ pub fn deferred_image( ) -> (Deferred>, Option) { let color_space = match image.kind() { ImageKind::Raster(raster) if raster.icc().is_none() => { - if raster.dynamic().color().channel_count() > 2 { - Some(ColorSpace::Srgb) - } else { - Some(ColorSpace::D65Gray) - } + Some(to_color_space(raster.dynamic().color())) } _ => None, }; + // PDF/A does not appear to allow interpolation. + // See https://github.com/typst/typst/issues/2942. + let interpolate = !pdfa && image.scaling() == Smart::Custom(ImageScaling::Smooth); + let deferred = Deferred::new(move || match image.kind() { - ImageKind::Raster(raster) => { - let raster = raster.clone(); - let (width, height) = (raster.width(), raster.height()); - let (data, filter, has_color) = encode_raster_image(&raster); - let icc = raster.icc().map(deflate); - - let alpha = - raster.dynamic().color().has_alpha().then(|| encode_alpha(&raster)); - - Ok(EncodedImage::Raster { - data, - filter, - has_color, - width, - height, - icc, - alpha, - }) - } + ImageKind::Raster(raster) => Ok(encode_raster_image(raster, interpolate)), ImageKind::Svg(svg) => { let (chunk, id) = encode_svg(svg, pdfa) .map_err(|err| eco_format!("failed to convert SVG to PDF: {err}"))?; @@ -161,42 +149,51 @@ pub fn deferred_image( (deferred, color_space) } -/// Encode an image with a suitable filter and return the data, filter and -/// whether the image has color. -/// -/// Skips the alpha channel as that's encoded separately. +/// Encode an image with a suitable filter. #[typst_macros::time(name = "encode raster image")] -fn encode_raster_image(image: &RasterImage) -> (Vec, Filter, bool) { +fn encode_raster_image(image: &RasterImage, interpolate: bool) -> EncodedImage { let dynamic = image.dynamic(); - let channel_count = dynamic.color().channel_count(); - let has_color = channel_count > 2; + let color_space = to_color_space(dynamic.color()); - if image.format() == RasterFormat::Jpg { - let mut data = Cursor::new(vec![]); - dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap(); - (data.into_inner(), Filter::DctDecode, has_color) - } else { - // TODO: Encode flate streams with PNG-predictor? - let data = match (dynamic, channel_count) { - (DynamicImage::ImageLuma8(luma), _) => deflate(luma.as_raw()), - (DynamicImage::ImageRgb8(rgb), _) => deflate(rgb.as_raw()), - // Grayscale image - (_, 1 | 2) => deflate(dynamic.to_luma8().as_raw()), - // Anything else - _ => deflate(dynamic.to_rgb8().as_raw()), + let (filter, data, bits_per_component) = + if image.format() == RasterFormat::Exchange(ExchangeFormat::Jpg) { + let mut data = Cursor::new(vec![]); + dynamic.write_to(&mut data, image::ImageFormat::Jpeg).unwrap(); + (Filter::DctDecode, data.into_inner(), 8) + } else { + // TODO: Encode flate streams with PNG-predictor? + let (data, bits_per_component) = match (dynamic, color_space) { + // RGB image. + (DynamicImage::ImageRgb8(rgb), _) => (deflate(rgb.as_raw()), 8), + // Grayscale image + (DynamicImage::ImageLuma8(luma), _) => (deflate(luma.as_raw()), 8), + (_, ColorSpace::D65Gray) => (deflate(dynamic.to_luma8().as_raw()), 8), + // Anything else + _ => (deflate(dynamic.to_rgb8().as_raw()), 8), + }; + (Filter::FlateDecode, data, bits_per_component) }; - (data, Filter::FlateDecode, has_color) + + let compressed_icc = image.icc().map(|data| deflate(data)); + let alpha = dynamic.color().has_alpha().then(|| encode_alpha(dynamic)); + + EncodedImage::Raster { + data, + filter, + color_space, + bits_per_component, + width: image.width(), + height: image.height(), + compressed_icc, + alpha, + interpolate, } } /// Encode an image's alpha channel if present. #[typst_macros::time(name = "encode alpha")] -fn encode_alpha(raster: &RasterImage) -> (Vec, Filter) { - let pixels: Vec<_> = raster - .dynamic() - .pixels() - .map(|(_, _, Rgba([_, _, _, a]))| a) - .collect(); +fn encode_alpha(image: &DynamicImage) -> (Vec, Filter) { + let pixels: Vec<_> = image.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect(); (deflate(&pixels), Filter::FlateDecode) } @@ -224,19 +221,33 @@ pub enum EncodedImage { data: Vec, /// The filter to use for the image. filter: Filter, - /// Whether the image has color. - has_color: bool, + /// Which color space this image is encoded in. + color_space: ColorSpace, + /// How many bits of each color component are stored. + bits_per_component: u8, /// The image's width. width: u32, /// The image's height. height: u32, - /// The image's ICC profile, pre-deflated, if any. - icc: Option>, + /// The image's ICC profile, deflated, if any. + compressed_icc: Option>, /// The alpha channel of the image, pre-deflated, if any. alpha: Option<(Vec, Filter)>, + /// Whether image interpolation should be enabled. + interpolate: bool, }, /// A vector graphic. /// /// The chunk is the SVG converted to PDF objects. Svg(Chunk, Ref), } + +/// Matches an [`image::ColorType`] to [`ColorSpace`]. +fn to_color_space(color: image::ColorType) -> ColorSpace { + use image::ColorType::*; + match color { + L8 | La8 | L16 | La16 => ColorSpace::D65Gray, + Rgb8 | Rgba8 | Rgb16 | Rgba16 | Rgb32F | Rgba32F => ColorSpace::Srgb, + _ => unimplemented!(), + } +} diff --git a/crates/typst-render/src/image.rs b/crates/typst-render/src/image.rs index 27b039113..7425bdd2f 100644 --- a/crates/typst-render/src/image.rs +++ b/crates/typst-render/src/image.rs @@ -3,8 +3,9 @@ use std::sync::Arc; use image::imageops::FilterType; use image::{GenericImageView, Rgba}; use tiny_skia as sk; +use typst_library::foundations::Smart; use typst_library::layout::Size; -use typst_library::visualize::{Image, ImageKind}; +use typst_library::visualize::{Image, ImageKind, ImageScaling}; use crate::{AbsExt, State}; @@ -34,7 +35,7 @@ pub fn render_image( let w = (scale_x * view_width.max(aspect * view_height)).ceil() as u32; let h = ((w as f32) / aspect).ceil() as u32; - let pixmap = scaled_texture(image, w, h)?; + let pixmap = build_texture(image, w, h)?; let paint_scale_x = view_width / pixmap.width() as f32; let paint_scale_y = view_height / pixmap.height() as f32; @@ -57,29 +58,42 @@ pub fn render_image( /// Prepare a texture for an image at a scaled size. #[comemo::memoize] -fn scaled_texture(image: &Image, w: u32, h: u32) -> Option> { - let mut pixmap = sk::Pixmap::new(w, h)?; +fn build_texture(image: &Image, w: u32, h: u32) -> Option> { + let mut texture = sk::Pixmap::new(w, h)?; match image.kind() { ImageKind::Raster(raster) => { - let downscale = w < raster.width(); - let filter = - if downscale { FilterType::Lanczos3 } else { FilterType::CatmullRom }; - let buf = raster.dynamic().resize(w, h, filter); - for ((_, _, src), dest) in buf.pixels().zip(pixmap.pixels_mut()) { + let w = texture.width(); + let h = texture.height(); + + let buf; + let dynamic = raster.dynamic(); + let resized = if (w, h) == (dynamic.width(), dynamic.height()) { + // Small optimization to not allocate in case image is not resized. + dynamic + } else { + let upscale = w > dynamic.width(); + let filter = match image.scaling() { + Smart::Custom(ImageScaling::Pixelated) => FilterType::Nearest, + _ if upscale => FilterType::CatmullRom, + _ => FilterType::Lanczos3, // downscale + }; + buf = dynamic.resize_exact(w, h, filter); + &buf + }; + + for ((_, _, src), dest) in resized.pixels().zip(texture.pixels_mut()) { let Rgba([r, g, b, a]) = src; *dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply(); } } - // Safety: We do not keep any references to tree nodes beyond the scope - // of `with`. ImageKind::Svg(svg) => { let tree = svg.tree(); let ts = tiny_skia::Transform::from_scale( w as f32 / tree.size().width(), h as f32 / tree.size().height(), ); - resvg::render(tree, ts, &mut pixmap.as_mut()) + resvg::render(tree, ts, &mut texture.as_mut()); } } - Some(Arc::new(pixmap)) + Some(Arc::new(texture)) } diff --git a/crates/typst-svg/Cargo.toml b/crates/typst-svg/Cargo.toml index 41d355659..5416621e5 100644 --- a/crates/typst-svg/Cargo.toml +++ b/crates/typst-svg/Cargo.toml @@ -21,6 +21,7 @@ base64 = { workspace = true } comemo = { workspace = true } ecow = { workspace = true } flate2 = { workspace = true } +image = { workspace = true } ttf-parser = { workspace = true } xmlparser = { workspace = true } xmlwriter = { workspace = true } diff --git a/crates/typst-svg/src/image.rs b/crates/typst-svg/src/image.rs index ede4e76e3..d74432026 100644 --- a/crates/typst-svg/src/image.rs +++ b/crates/typst-svg/src/image.rs @@ -1,7 +1,11 @@ use base64::Engine; use ecow::{eco_format, EcoString}; +use image::{codecs::png::PngEncoder, ImageEncoder}; +use typst_library::foundations::Smart; use typst_library::layout::{Abs, Axes}; -use typst_library::visualize::{Image, ImageFormat, RasterFormat, VectorFormat}; +use typst_library::visualize::{ + ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat, +}; use crate::SVGRenderer; @@ -14,6 +18,17 @@ impl SVGRenderer { self.xml.write_attribute("width", &size.x.to_pt()); self.xml.write_attribute("height", &size.y.to_pt()); self.xml.write_attribute("preserveAspectRatio", "none"); + match image.scaling() { + Smart::Auto => {} + Smart::Custom(ImageScaling::Smooth) => { + // This is still experimental and not implemented in all major browsers. + // https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering#browser_compatibility + self.xml.write_attribute("style", "image-rendering: smooth") + } + Smart::Custom(ImageScaling::Pixelated) => { + self.xml.write_attribute("style", "image-rendering: pixelated") + } + } self.xml.end_element(); } } @@ -22,19 +37,32 @@ impl SVGRenderer { /// `data:image/{format};base64,`. #[comemo::memoize] pub fn convert_image_to_base64_url(image: &Image) -> EcoString { - let format = match image.format() { - ImageFormat::Raster(f) => match f { - RasterFormat::Png => "png", - RasterFormat::Jpg => "jpeg", - RasterFormat::Gif => "gif", - }, - ImageFormat::Vector(f) => match f { - VectorFormat::Svg => "svg+xml", + let mut buf; + let (format, data): (&str, &[u8]) = match image.kind() { + ImageKind::Raster(raster) => match raster.format() { + RasterFormat::Exchange(format) => ( + match format { + ExchangeFormat::Png => "png", + ExchangeFormat::Jpg => "jpeg", + ExchangeFormat::Gif => "gif", + }, + raster.data(), + ), + RasterFormat::Pixel(_) => ("png", { + buf = vec![]; + let mut encoder = PngEncoder::new(&mut buf); + if let Some(icc_profile) = raster.icc() { + encoder.set_icc_profile(icc_profile.to_vec()).ok(); + } + raster.dynamic().write_with_encoder(encoder).unwrap(); + buf.as_slice() + }), }, + ImageKind::Svg(svg) => ("svg+xml", svg.data()), }; let mut url = eco_format!("data:image/{format};base64,"); - let data = base64::engine::general_purpose::STANDARD.encode(image.data()); + let data = base64::engine::general_purpose::STANDARD.encode(data); url.push_str(&data); url } diff --git a/crates/typst-svg/src/text.rs b/crates/typst-svg/src/text.rs index fa471b2ae..e6620a59e 100644 --- a/crates/typst-svg/src/text.rs +++ b/crates/typst-svg/src/text.rs @@ -6,7 +6,9 @@ use ttf_parser::GlyphId; use typst_library::foundations::Bytes; use typst_library::layout::{Abs, Point, Ratio, Size, Transform}; use typst_library::text::{Font, TextItem}; -use typst_library::visualize::{FillRule, Image, Paint, RasterFormat, RelativeTo}; +use typst_library::visualize::{ + ExchangeFormat, FillRule, Image, Paint, RasterImage, RelativeTo, +}; use typst_utils::hash128; use crate::{SVGRenderer, State, SvgMatrix, SvgPathBuilder}; @@ -244,9 +246,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::new(raster.data.to_vec()), RasterFormat::Png.into(), None) - .ok()?; + let image = Image::plain( + RasterImage::plain(Bytes::new(raster.data.to_vec()), ExchangeFormat::Png).ok()?, + ); Some((image, raster.x as f64, raster.y as f64)) } diff --git a/tests/ref/baseline-box.png b/tests/ref/baseline-box.png index 2a9e517580cc73174735d95d87244c7dc8196845..e07e22ea75b069488b4f62d1df1ab6aeabb4d349 100644 GIT binary patch literal 4021 zcmV;m4@&TfP)OXh299yAizlfu0-$^pjj|5K5EjAD6wkV_J=nXOrz1|#Bru#nL?3i4+hg&* zwML~W&mz#_X)m9@LFy<{V{XUDRtyJ?riP;g%g`-*etC5TL9xY^`pS~CaiAZdemXJm zl9kCEcv8S{~BWG92GKa^uACm}Y4xT5oo~ zH@p^!kR&!7V^bh-EpMri6S9e6Lz9pITahXO>e`V0cIG^S$uP)MxEi?SggCrdbUQ6G znd#5P)uExmqo>b`yZ}P22hf_K1rS4{SiSvSqv;Y&og}1*p;%9jHtp>3Z}rQIx^eC^ zk)t=H``BcdgLsq~N=An~2-4h;W+}_f?>*cFxNPJ8T3xX`JW7kXOpg=jI3PXSRqg2r z+Xwa7-pK(#XpN3`^jQ7U`NqhG{u{RRtQXL^Ou{xLibRs^7~gTOtSxp-E6rv24asu* zisr-VMO12nVUXGVz_0%K@Y|bmU<0q0=;=1_i_-71cv9D@wg3f=6U8Wij-eTzp1oRZ z;LLI|f}2_*>>h<&q1A3?1(hJIbC+7CX6v}wS#qnEq-ZCc6FPKYZV{CK1ws4m@%3Z?ZBle6hpBn*{d6#7ifJsR=k;<`Eu^UQk_POvvngcM#eSv_O0Vv zz<1v6TzyUseAv=VfCLWoQ37g?e}2m2NEZn_%XKk`cpg}(=!+%CwF8F7ODoW{4Vv)Q zOY0y>9OoPwNeO5QCCLb%fq^e|Ryl@-6b%BCBngU3>4CA@njaY11l8)QTn-RCS{Rcp zvwz3I7TbDwQ{9I~5R4*-u9=o3Yqsf0l7$3*c%ozR9m7);O*bG(VXg}Z0`#USQ-{MF z6YB-E(gej-H^Qg^g6Z}eMM@}!Saz9Wahl?yTpUA4e@^(~pH8+d?bf|}rp_M6;Cy!A zrioh~`TAe~{?o%J`u2}OiUJ7D(*yuqFJBl#a1!+a3<9T(BaCg@mJL+hXq%3rHgG^& zfvL!41X3ddM^56NUbBf`#BROVsQ>2DH_2pzAxQ@92n>o6Zj2W=mg7iBkZ4b=4}%Cq zQOBuqczn4tZ?uH~;qvMHdI3E;mP}`EY-$x859+mGzSJr&*OJK?OFB62b8L){MVm!m zGOP1)M_o3p**aa1~&N53W8u}W+tD{N2Af7;}-ngci+AF=9@qG;DgXU zbLPyQci#EJ3orP-|9`lyt@u}%2(+9|vS9DngC4vmw*N1Lo?LQxaAPK&j}V!7l)*`) ze|#6Blu|rCHZ(dyD+wTNyycNo9x4`#_uY42Xz$#)6GhQ`?zsoUFz~-=YNk3pwPmrkbvz z*5y6dHvUK$=Ae&UHIUVr`dTrPM2{r3X^ z))fBA;^53xg`((ucJRB43QBYkC(tcK6qst8^u1FD(rSM+Cbv7;P5T_JT_c5|o}H4- za<;N#+h&Sn8kfFeV71iBK@>#d-1(w}AgC1}rY86A`%6XD&o8f_5b1G8I)%$$fBT`l zrfnQd^yWr^`5$qWN~Oic#dqF$C!A+ETwPs#|NZx~+3d5=J{u0hO0_I&^XAPxJv}eK z{BoE>hYufq_0?Cy6joI=j3un*zP`Tx{{Ei=v|$)w`CPN-=jX$t<2cVh|NI+oyb&H# zsZ^LL=g*%H{Uk}=d+)vA2XQ2p&yjH<7UyFS`hyz;nzeY0FG=mbY}D~`^Q&5Xsa;-{ zBMc_Xgt%dFu&)oX>`G}mLKwS8MY?&`Q%Z>l*M{Dz=3$PHB7UWz^lsYaW95aF8b@Oc zL8arAD%+BR1U)oGFtrAa5H3%s;0J}6LWqw&_Sm&_xq9{LE3dq=fB*icpMLt|k3SBJ z_29vSVakUsMV94-g@tFHdFI546W@IE&67_)dHe0Rhv$b+CA`8<0h(pmr=EK1ntk-> z(eQZs^l6%=B}sbjx#vblM_S|%YWinziYzxT$G|lnx@$gY^-@bj< zu3gvrho6Rqk>p6ipi>FXq)*WC2S`Tk)o3b7pIB?Lz*tJSAnM@=T zagGEOiTWs1O+WAVs`XB*S!zfUj*8c3f*^?FIM)jC4~>b5iLn2MA%rNeIc?hxjnKce zv=sWoFvFoJis1i4*o!Z|2>xfi_uhN5EE5C~-ZlKzaqir?R;zX3zyXS)-g@h;YPA~n z4IIbA1`^tL+;PYC4}Nj%(CSK=3NRG65g$W5LpOY!;A&OZ^I6wJ5re$7(6i9QwoXh~ zYWv$uvty&dwuLovFlb!5$;Tm%7_>oXA&qq&_zYJwv8?53+VYjB`%xp;Ng z_B@?S3STVHi&bq!MIL$NvA_Gr57<=yT0_cFW`82^JvYi>%Qb5xpM@xn20rww3qniG zVjDMXdFsJ;{?s-sZ@>Na+S*#U!u+AL)KHryz(`IF!)Hhmi$UMR36uhkV;W|?{nrQg z@=WH--yZwrP=UnC`}btB#8jHrrshjX%vlP+_f=oFYJk{0HhIJG4-ZeR8a@@5RC_2J zPZ7w<%0feG_>L(i#03l=$mRQT)NE~NdBr_ByLij?{y)K;;iHc}`d^)nqDsXgLLeB) zK+mHX5(|97BlNbecG{bAJ#E>ptzH_=khHTr$in85|H)M8&o&C9y=*oi)J(?)BFl8z zEn;|RZ;v>ow$)5BN=4Wxa8uFz;&Qp~#>aHmUb}h<10*d{r>o6bH;6^RnxE`g>*ig@ z_#m&;BTdu8c*O3YhUozi_^ykBfWnF0H~mWG#g=@5z@xQRxq}yeIo@+>`N-)~r;tmD z;(#;1=s=)w@nXxkeDcubsrN|IG6A|_IrDj$+o?P6)UKbP!t zYRgr3^0o&xKK|J^t8USvm>eV+;8KdBpfGS9j1YyP-jTrb49(FrFMyDbL=g;_rbBa- zM=`q5jwO=1tGd1lvElUs8U>iDVGXrHivdFE@vMun4u)x&9KLltAw=n>>d+CCPA12; zj+3-mFRz?zeux0D1NRp0U>3AWv;1DxbpUP#l1;j#>87@FY* zvTdk}LIMjaZ4wZsWkSnbFQ5?u@ez>V(*O(9cAE&i1lvo{e5KZi<5*3r>H*i&%fxsF z1bSd0IK{Oki{&PZk`+0q9zs?-P<_JP9$7 zBiV%I#CeWWRDdILp&*p21`b)(Ru^Vx0^6%DF(d<$LUhNrVX0{wo@uClHbovmTP9L59Y9^xafZyS7ts0ajQ|rkmdK}R-GKl#aoQ(QQ8mnB-*iwut_d9F z+X)=Qk$|2p&Lxs@25+gB#M9AKYzz3A3F-bTtNUxxF6gehpu3>Epu6gV?t<>B3%Uz> bo$KELgpN8z--biX00000NkvXXu0mjf7N69W literal 3896 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 Eg2srySpWb4 delta 1393 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 diff --git a/tests/ref/closure-path-resolve-in-layout-phase.png b/tests/ref/closure-path-resolve-in-layout-phase.png index baaed3564387f4f7ccd5ee63d3f9f7e9055d6a94..a3d6999813b88ef0eb23b49054b8793d1dc6a4d2 100644 GIT binary patch delta 2244 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 diff --git a/tests/ref/coma.png b/tests/ref/coma.png index 2c59ae87039f7de3576f41b8c1e46b6b32145d41..a1d743a49f71209e12f4ee5be99b69bf86e9be55 100644 GIT binary patch delta 28237 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 E0PbK?-T(jq delta 28370 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 DeS~A_ diff --git a/tests/ref/footnote-in-caption.png b/tests/ref/footnote-in-caption.png index 12a5fde5e965edf996b1fdfd5bff04e160077c56..79b2b5d0f955479b46cdab66ffcfb4f936beb893 100644 GIT binary patch literal 6154 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 literal 6111 zcmV<57a-_~P)0ssI2phCI_000-JNklJ$X}-M^TiLOUUJPkceCpM;INs=6Y}(_k%c!a?d@N z&dg*kTYIj1X7-$arlx7?98;$>Q|sTfTWjxcuQlKATWd`tFp)k)0zyEb2{eg76KDcW zBG3eyL^cKF4vZtE&$W5B3C6tJONENB$ko*qSD8!}7Z(SE=lPzVp8o#+jg1YsGcz;8 z!^0gN9k7i?BWfRIyT8A0c^xNob8|B_HFbS`4HrdPT3VWwl?59b92~^!z9UB`P7#Kj)z@C_x!0R|a*jrm$r>Cbdf`WpAgM*!~qVc^O8yl07 zl3rh5;kLK8W0Q-Eiygwv0loXq}kCk3tUIGWxJD^c$3WWkjb8~Z9SsC2;_;@rab_*Lj-`Uy8 z$jAUDgTW9U9=@`&!g1W=+2&(n|{vJWF24fxV za&mH#n3xz96*V_Ehu3d!Z_!~4!|d+vj*N_0N5BE#zLSlPp`oF?ygc+NY-|Z#sZyz` zs;cVi>rrcf3;Nh6`WkZ<`>(95Y;A4r?(W9)v;+aoqN1Y9%ggfeay&(|7Z(>}4QMts zHK8go?k$TE6Qil2p#ixt!Epzmam+%Y5H^HFB_$;|44iaDMFsi^4Z*T%3=MoV8XZ=i zot@Di7W%_NBG3eyM4$;YfhG}X0!<>&1e!pT2sDBI5b-9SGwPL`R;Abel3eXQT__X^ zxlFFnXn39{(ElqVZ`2w%C8tsAbq1r+<|z>CbUKD%q*5u%vIc{JK>uOa3~~|%qA)@(LYb(xl-KS8)e zWzh0M?$c;A(Cw8h2`kf*NpOC8c5-yA$eG8ZYFlSUDm5jUWiUnt>-RrJPj7a1|8+PjGFVPunAHHK%?nL z2%0;$tLM9dw{e|LC%4V&_xqM*ZSq1-Js1pfiwUaNg2oO>Dt~EHT+I%P*pX4Qw*AA-{j8=HL}36Yi#$VKB8zOY33-HQ@gIT|6mg@V6R8CL<0^Y=iWQc%suzaxijbZeH#%Qd5PUC z+AbEvwOXyhwdHa-OMPg#ZLth2m&<3O#%;m3(&KnME*6V_H($4RA4VTbZ)<>cySHeA z13M(ZAukq-ZNY;9o=;9ro~#Dy3=s|JxUFq%sZ?6W@_(9srt7^>v<tri(elN`MF=3e!XW>99$Evc-8c{F z5Bdok3cOBz-ELPVq19^Hb{h;7Kk^VUQgBjRd}9L)bxBP?V8Ii!1hvTR>OS?R*nIc? z=da&;p=dS*153pYu{g|TGo!56>kcI^#L~p%&d<+7r;z1Q&Eugy5c5W(0dfqSyb;a0 zyu1tn^d3C+Vb@yL+G@4ZOE6U=C7n*wBen(f2M!^8=~{QXpwj^CJI@aEPQ@G`@H0h& zl4}r|z-TnOyZ`a!`sVuU_x-Zya2oA)8ieEfr=CYrjlk_d8hbVX z^;EN|=tm0-vq{5LktV=DuV2#dd_EVb0?BvS@Ang`5VL18nVg=UM##|k320y7s3{ur zLNd;E(uCcovY1ss>4Q^%@hW5{!&)J;qnZ%D@AyMvRP#duq6DE%;QF((v!kP<^_Bzg zukb}8#Se2s2qenovOFOd6nadqayyLIyuqiCj4Id}yklwe`1m+##(9ekKOuYA!8SnN zqg0kvN3+>9?}98zQrQZGXEe%GD&;?W2Gii<{q+^86}Gu*WCE1StgUoX0x2kYgKgEawGtERi2aB^`20yIu2X# zL#*Ra4vIb~`k?4P?A=XhT}2cJ@D{oW5|U^ZU1X6EjIYTqge(IIAq0XDK_Or)2%3FF zaU&>Z6;VV%isGUemqr(*l9d#Sk~Hd~E-WJILIi0Azmqm;>9093y?vqYy`)Xwd+iM2 zadPk6nYm}q%$+&^bI#0lL(DxvzTGYXlCqF_obZk91D5MKpA@T}m0jbc3cr#-J3{yyy%Z8(F_-DFy*G?vFqH z_QH#=tghYEy^V~FB)ZIHigqp0;`@ezJ5-lSwx{3yAS4CsfJpTgoxL>}N~ZQ49UYa;b^1@mQn%95 zL0)t`?Nz`iKme*y?56fqj-b3A@hhUVK-di}(YPtxKJ6`KBb^*T1h|LS(4(+_?djr<{dSvW|eN0YH{`||YWe7!rS`_6G1f@RLAkYTAaCiz03=AN!H3Xch8kB{YnwkOy z=-Q|VD`|~^1~W4={?#fol35O0v0j7Sg)^u+wV`n(S`JZy@zkVYDugYyx3`mDqJ#?( zvN*Zfbar<7C5-+3{nY2;aJRL!-LB*??vD{;jN_omAwo+V@4I7I`&E(2s_em(N4>qh zy|c5k^r8`5T;r)gn9F)2BhK--!iel$LYzaKe@f$U$R>RW_~XLeGo*j#kW#W3*D z2QJ3a%3oSqfl7@G7W|YT0YD`}_VgAh|@&NjxiokPC(+&+_(SZ)T27E{8 zLiL6p3K2mAx{7Vc8o})9PbE*%csgE-s_qFP%o@k{v-g)=E!NI{}rAMcqc#;CMIO)@0 z7lf3P#CxYqZEj(UgvFCgjygnfEX|kT7$W?@UejAmE2u=wEm_7c;B@+~I;3Fw&ta)6>%qrR9tjRvBn4 zfIUK~J(gHC#CXJsyO$b6WzpbH4>B11l%Tg<$#4o%7Xn_q zYeEdjRewxOOayG|3Rdcah`NY(+&vNLaS$a3DS6Pk2gjZDt0y3Y{6vi&brmb(T1(!Y z6O2Lf%bXzH)BxQnoBlu~$GQl22kj*&S_mX#OyYHPBOFa5 z2N`2at$YpSpHntSbFeG#yBF6LB@)y%xLheD+ZG<9$!a8VSawzH;|nP zv^OdWM{^Swa}-T7UwIIC56Cym3B3vUcjUQ3AeqlUcf1L9hO9Lzgse-%?j<9p2W^ht zQ+zZNv{lgV24ER>-=7;6a5z&CqP^FJxfMl3$?(hKi)h#WI0sV$E7`Cybb^lOonhV? z=F|v`W{h(7?3WiV{7`1bc_+zY%c?6g`j&L*kH5Y+v-;A@uNIAQt&C6D?tJsZ%S_6^pifEYo1v%NbSHe9G|&^&M8DoCxpfZWvLj%k`9# z&6ws{8p`0VZA&y2t+lo`byoaey!7V>AAbDFr=O>Y#8&&Ze8oI=()Pj3N)b42t41|} zuq5z?c6T-%7P%JSZh7;kueWyxxlIH?6oyZc6Xh64IYRbXA(reR8zk5OOSXt5;u;)= zPx(>HB8hQuf(@>gWwpmWJ>%-`n)j-z%g>(a#}RZqyio#;tE(%pL02nZF^Y_{7^;=@ znba@~gNoDWI@r{TkE~|Hd0#r6equmzVe_1daWhJpi~H zKBVecLC59f26e@dquOqLQ|FQh@?uJw{@Rqaqrge+4M3sIQVbBfaWYy6&gubk!QxOG zA1Y()WnLf4 zpD6&jl8?AubAfH)m*wIhPK<+*U{)mTOD3bUvojQqh0o;jE@B)FqmfK5lW6TvQB-ux zsMJ9wDkF!^As|Z;neRwr>IHQyym)n@y>E5vEOJ!0396Q^sHKb2A+(Ayt6MjTj#_GN z@{p=S6s$?@MV+yhwNHx`Mt!j_nYh?X;|}6(JIErzh$GmS+9f`g9lI+Mgr)5Vdmfr) znIIU8O@9q6EEatECDDG<|}I zx+VH2a%ABB76*?VNBem#ZP~-_`}2u5p$v}Rx0pEAX}a}xVE{5=O}Y{EltT-;6gn%% zV!62rgYKBpYa1U4WkeG605-8rzy z%iIKwg8=}*CfUO;GV{9)*A3YD%9Gap)I-H0Lwvle!;3B#yfYQthOcy<#~nmbG16{W z{a8Z6AU11|eh@0>BNr!F$|I#Nr6&xOl>-Q~fpP&mA5ADoj$hG78)*rYlhh*M2Gu8? z?R0WMXGo0&nM{4<$FEC+S*Vb!8&sYY;EZPvWF|ziBlmXZR^UjC(dF<9&0t|>XvaBu zjSbG3Lx6q5b)5_FaHN5EN2EG%BsYY-5nrq~5|yCWN2qhvAX`$D?Hg5no?luTu=9de2k=`TZ)R0S%0t(E;wtJ z>agor4pJJx4+iYmjLcB+D{K@dJJUoNzT<3~X%KBQPhHVLJ?nwW(t}Y?KX7|f7pSIh zIeFW*U!3#CsO3yqLA&UFJG9@HHjGB|qQOpX^97vyLL7_A{JPgpDw zc!R8m84fd?atwm@=Ol06eR%cyjXDju`#+U<`ax;z*CXg%JuIKUe9bw#J^3>Fu<|_w zY%`UdMA_fN0-2GokWuVeupQh=px`#vR}3SUhYF616qMg1SPhxMZIBiQ&&rNC0BW!S zzQg%4yRx?V3?(@No{}1>joP5;e5}zKDd0=PUx~2cZ(yv7Jpo%t>NAM8uLK^@5L? zGk&w44w#eE;#&^A|6-8?qsgEr0k3Z=~f5P1?P{Awc0t;An8I_}49M zIOv!)@?Yd4$x+MP;dGXGG!jp-9Whs|f#D@6^i5-f^!=vX?pa`*FsPHj(fnBUZVGkX z(x$x6L`J3-JI1IE3lb=Yi*L!4p!1X3bS9f9M=e6{T%nN|5MIs=w%Mo1IcM2B&aZZ2 z6#V|eX(aHjbi_C}84)P2Hn%M29v5nPP5D?k19QCkh|3rc2ia2mUFv zJr`}k6-qIa|FtDf~oF_4( zSJ-Yj&r5krSjacUyMFMo{H2rQ#Gc} z6vb9$R{*t?cj9!*f|40QPZ>dv lphwVCM$jYZDI@4ZmLIRcXa)U1?Tr8c002ovPDHLkV1l%*cy<5) diff --git a/tests/ref/footnote-in-table.png b/tests/ref/footnote-in-table.png index 062a6fc7104e0e4e89de9cb676bc0cd48b0860bb..e110eac6d4cbe9fb78c999cecc4b801cbe2e58ee 100644 GIT binary patch literal 12727 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¥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 Q*8r5fw6av?C)3dX4;RwKWB>pF diff --git a/tests/ref/image-baseline-with-box.png b/tests/ref/image-baseline-with-box.png index dc8e8bc5714b8850a9aae75adeef261f1c77718f..ade90e2f559608c317f3b99b442ebd6b63b12ae1 100644 GIT binary patch literal 6375 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 diff --git a/tests/ref/image-decode-detect-format.png b/tests/ref/image-decode-detect-format.png index 6ecb7dcdaeed44fe450d98c7f2efcefdd98f47ab..cee71bb934d4688812dd42e9bb6b0df697eea8ec 100644 GIT binary patch literal 11032 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 yJ?$y001amNklQzBWmxVT=b5VQX4< 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

    e{DNB=+`MAN;jyhK+PLE4Es72h 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 T?NNt1D{%MD6jvub#*(9r{Y#lSZ^pIU? zR{znuhgyr9eA&AHT)DB;c}H25%*+SB{pJ=0+iJyZEVi1+b+0BhtEzwL-6wkYuQILD vSfG`fr1c}LHdvf-N70sN|389U|9`Fc!u1hl7w(;j0EL35tDnm{r-UW|UEhBi literal 0 HcmV?d00001 diff --git a/tests/ref/image-pixmap-rgb8.png b/tests/ref/image-pixmap-rgb8.png new file mode 100644 index 0000000000000000000000000000000000000000..d905c1eee9a68a63c5f9908784f60cb451608c75 GIT binary patch literal 1220 zcmV;#1UvhQP)TWFhww$L^U{lA8W6Rz3)|_47n&ei<;y}VcrAoX6jDMWIiLYN z#CnFKjx>$8m->Rx3d;dy%{U>%Lh^`1*8&U0t%6U*#ejISL(4sD{pZjE4#vtXEW#QK z!4Vy@kSBD4f-onRhzkTyT#IJfZhH_5eSUkc4)kzfIFK0=b7b{6M_5QRGNMEKLYNXu zDB7>Y1)0zhn%+ZSNf!F+IvQ79>m()g7+E~)N|;F_c|dz82t#5DEDpa^Jyo~g=nh!u zGuv%+EtS#>Q)I58{Y)H@9xV>~OyB;4D>_W10h-(KNwm;k_vg?qHKW|tZ6J!C84fAQd8vzh6zwj$mZJOZiVCzr>?F3tDK&mVUJ@1kE%118U>Lf# zKMsfyX^6ink4^N2(8u znMdZsS}%hwIoD(^167y>!nF#T@Gkme`~eqSgM?>%Og?Enq}tvTtCn4|lkp*#Qa$84 z6(5S{LOOur4WSBv=PvqlBt(mVLIM*h%4kfPO{Ux68kQNcHc)B!T2Kw;>AlkQ|hMraHxGC*l4Ibr6FRL}udWTmK=L~dPX>w`6L4KzRxR6rB#X@ea-utT(gr~$D9e8*1iI5%jLph!sP*RM#? zXL<3-I`(J=e`%g2c+xyeRJ4j#(Ha%4qE)miP|+${^Y29i?E53ax0mqsHGFytAKt^e zkMQOTy!s9=es1{Z3;6yDzPy2t@8JChc>4)ne}(v?v0XoZR?!+2t)ew5`hOB_%v9i3 zB!g%O4MXd_cwPK zA13F0A!m%4mTW?{E_;)O@`5?R8K;a(CdtNUXLB%lp`07rmsv&MO)2|AE|>}}g{3%O zf+-7^1SR(sxj-ts6cdVfMYt?fGF4|>ujD|mreFz&jW;E_*G%g)G({Tcw(N~C$@uEuvi4-EcXl-;H14e7O|izD%{UvXwu%$cD`$D=CxFzb5Jx?)*5twvVw z*I?JkYsR&7Tvp60$JK_0#jJ_fI_;q|`Y_hcH5S++TO!M78LT;33mbzYJa3@iupEYY zU!Yzj24n;#V21q)X1HP4Fm*htqOTCWb(q9q^2F2`wE{6x!`LmW=qp4cPKEPd7;XPd z_=as@u8O`!^w4<1Tgx!xu?SB`uJz{zhXFY3$Rn_x=)>HrYV>bRE$c6pRJ4lLsAv_f gQPC<|MQfgt-{x1c2u5nq3IG5A07*qoM6N<$f){{|;s5{u literal 0 HcmV?d00001 diff --git a/tests/ref/image-scaling-methods.png b/tests/ref/image-scaling-methods.png new file mode 100644 index 0000000000000000000000000000000000000000..9d543e114fc5578525babe90f004745905aa5e6e GIT binary patch literal 1539 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 z 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 diff --git a/tests/ref/issue-4361-transparency-leak.png b/tests/ref/issue-4361-transparency-leak.png index 4060d43ac442e67e674245685f3fbfbe1d555157..660798166355dfb1e8afb091f76ea3dc0b585ff2 100644 GIT binary patch literal 3738 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 delta 3514 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 diff --git a/tests/ref/pad-followed-by-content.png b/tests/ref/pad-followed-by-content.png index 90b48232a9febcfe2c6920848fb21ba1ce8ff659..534a97870e937fef7b91e964c2bbe8588714ecdb 100644 GIT binary patch literal 12071 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 literal 11897 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 x * 16)), + format: ( + encoding: "luma8", + width: 4, + height: 4, + ), + width: 1cm, +) + +--- image-pixmap-lumaa8 --- +#image( + bytes(range(16).map(x => (0x80, x * 16)).flatten()), + format: ( + encoding: "lumaa8", + width: 4, + height: 4, + ), + width: 1cm, +) + +--- image-scaling-methods --- +#let img(scaling) = image( + bytes(( + 0xFF, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0xFF, + 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, + 0x80, 0x80, 0x00, 0x00, 0x80, 0x80, 0x80, 0x00, 0x80, + )), + format: ( + encoding: "rgb8", + width: 3, + height: 3, + ), + width: 1cm, + scaling: scaling, +) + +#stack( + dir: ltr, + spacing: 4pt, + img(auto), + img("smooth"), + img("pixelated"), +) + --- image-natural-dpi-sizing --- // Test that images aren't upscaled. // Image is just 48x80 at 220dpi. It should not be scaled to fit the page @@ -103,6 +179,58 @@ A #box(image("/assets/images/tiger.jpg", height: 1cm, width: 80%)) B // Error: 2-91 failed to decode image (Format error decoding Png: Invalid PNG signature.) #image.decode(read("/assets/images/tiger.jpg", encoding: none), format: "png", width: 80%) +--- image-pixmap-empty --- +// Error: 1:2-8:2 zero-sized images are not allowed +#image( + bytes(()), + format: ( + encoding: "rgb8", + width: 0, + height: 0, + ), +) + +--- image-pixmap-invalid-size --- +// Error: 1:2-8:2 pixel dimensions and pixel data do not match +#image( + bytes((0x00, 0x00, 0x00)), + format: ( + encoding: "rgb8", + width: 16, + height: 16, + ), +) + +--- image-pixmap-unknown-attribute --- +#image( + bytes((0x00, 0x00, 0x00)), + // Error: 1:11-6:4 unexpected key "stowaway", valid keys are "encoding", "width", and "height" + format: ( + encoding: "rgb8", + width: 1, + height: 1, + stowaway: "I do work here, promise", + ), +) + +--- image-pixmap-but-png-format --- +#image( + bytes((0x00, 0x00, 0x00)), + // Error: 1:11-5:4 expected "rgb8", "rgba8", "luma8", or "lumaa8" + format: ( + encoding: "png", + width: 1, + height: 1, + ), +) + +--- image-png-but-pixmap-format --- +#image( + read("/assets/images/tiger.jpg", encoding: none), + // Error: 11-18 expected "png", "jpg", "gif", dictionary, "svg", or auto + format: "rgba8", +) + --- issue-870-image-rotation --- // Ensure that EXIF rotation is applied. // https://github.com/image-rs/image/issues/1045 From a1f263862ca3c9594700f0c95a8e5798baf07ea9 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 31 Jan 2025 10:56:49 +0100 Subject: [PATCH 32/34] Change type repr to short name (#5788) --- crates/typst-library/src/foundations/ty.rs | 2 +- tests/suite/foundations/repr.typ | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/typst-library/src/foundations/ty.rs b/crates/typst-library/src/foundations/ty.rs index a2395f2a7..973c1cb61 100644 --- a/crates/typst-library/src/foundations/ty.rs +++ b/crates/typst-library/src/foundations/ty.rs @@ -136,7 +136,7 @@ impl Repr for Type { } else if *self == Type::of::() { "type(none)" } else { - self.long_name() + self.short_name() } .into() } diff --git a/tests/suite/foundations/repr.typ b/tests/suite/foundations/repr.typ index 36823e98a..2f2c055ad 100644 --- a/tests/suite/foundations/repr.typ +++ b/tests/suite/foundations/repr.typ @@ -37,8 +37,8 @@ #t(() => none, `(..) => ..`) // Types. -#t(int, `integer`) -#t(type("hi"), `string`) +#t(int, `int`) +#t(type("hi"), `str`) #t(type((a: 1)), `dictionary`) // Constants. From 46727878da083eb8186373434997f5f7403cbb66 Mon Sep 17 00:00:00 2001 From: Hong Jiarong Date: Fri, 31 Jan 2025 18:02:42 +0800 Subject: [PATCH 33/34] Disable cjk_latin_spacing in raw by default (#5753) --- crates/typst-library/src/text/raw.rs | 1 + ...sue-5760-disable-cjk-latin-spacing-in-raw.png | Bin 0 -> 2011 bytes tests/suite/text/raw.typ | 11 +++++++++++ 3 files changed, 12 insertions(+) create mode 100644 tests/ref/issue-5760-disable-cjk-latin-spacing-in-raw.png diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs index 01d6d8f01..5bb21e43a 100644 --- a/crates/typst-library/src/text/raw.rs +++ b/crates/typst-library/src/text/raw.rs @@ -475,6 +475,7 @@ impl ShowSet for Packed { out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))); out.set(TextElem::set_size(TextSize(Em::new(0.8).into()))); out.set(TextElem::set_font(FontList(vec![FontFamily::new("DejaVu Sans Mono")]))); + out.set(TextElem::set_cjk_latin_spacing(Smart::Custom(None))); if self.block(styles) { out.set(ParElem::set_justify(false)); } diff --git a/tests/ref/issue-5760-disable-cjk-latin-spacing-in-raw.png b/tests/ref/issue-5760-disable-cjk-latin-spacing-in-raw.png new file mode 100644 index 0000000000000000000000000000000000000000..9624273329b4df3fcd229acdef4757934cbd02f9 GIT binary patch literal 2011 zcmV<12PF83P)6J^w;o7Q? z8Lh{=3e_Xmiqr>AbRBLVX&SuMxBib$e{I=Yr>mh&*^Q&)6LvAo#ix{*Po3*6^M$;k zveVqsW9#BC5{iU4{G=>JY0o{Y>OFa|d5-|Sp?U>hW83AgW(TzU>NVYmtIRw055BwH zeyl7Gw7uxKHSdt~ylQyU4nTWb+I1hLu@9OpiJ<4som*O3I)DEBa5x+c230E6f&~i_ zuNZN;Eqe=;EwKSC92542A(eo&>g-~YMu)i>WB}FO>iauUcnVwhG3bmA9Rqxe>dXGI z?(4?7&(6I0*CnD6yU&(Yy|TLhOh(nxQ<|dXr``~qzi{wTZe&%@3CF~(SkV5^#M{T7 z2lQ)2PYcjZ##+7x7o8o@!C|v${T6R)2fsOSL=S$j()P}m%hV-mErH<6p zRDz6*42?!3DiC6e0w2{JMl*GvzG_Y!B>n*KK{;cF9gDVf|m*(S2~Xc&A%l3(pr&` z06qITdaUp_{qir285=FR2LpNoHO})FT^Fv1vqLM^!Xo1FvWXfp_|!Q%Gc&WJqhkut z;cn+N57nx);h#6mGOh7Yo8Ozj{v==D<>%9%%i{MQ6v32J@;jUz!Up6#8u=(N| zF(h9vekL3Z6Qq{C8r!)t(z~r48>&Ae*j4w~V6 z3~gh^H)BUjtp^IlSpv(x0$+z_YS3!6Iy*ai&YU^y$Ht}}w$p>Qc`XF`@qR!Ph;;mW z=b@7~j_{lmZA4?z$cu%KkvEwerRb@9 z0{X9~TsR_ym5S6@v1j7LW0P(C-4OGUx=XnAyambqq^hGS{6JPR3&;&dlX-Q0Kk`op=GnAku z0ts4zmPLY=pe1No9zf6&J^f6<@uf4pjK|~Y?CgxIY-nhhX+Og6V1>{|id18&!jz=h zR3F<6F@)|waUh=9E@~^q1ahVYk;IfIu9BIH%`-}X_T$S4ZJjLWu)~R>LwW>9MnhJc zHNS8QA3c)2`|f>oZf@s|>%0PGE=Kxx(tHvd5y|fPT4`^m|Xf|Y!nBdK{ zU`^jEU65#{<78>FpeK5@=%mpf^Yov7iS<|PouV{H_vE9h{R4Vt;g-EWYhPQ!-rKBB z1dZljwrp8mULJ=omn%Izedo@dNrDCplQdtbE!wN=>?8;~?GZ301~;W9uOw)po3}XyHcqGWe5ZLEB4CvS@;g#@L}r<3Q_{tYrLJa`N$+(}G^TdiC9C zQiK%uRoSLVYtgVUb1)T}v{;=#>K+CYSXm8;)1(w3uP+T$VWm;hh-t<$=8yWNCte1k z(9^fI(mV!s)@BhLYRQRncuA)YO^m+?N zS8-HSE8E>@Qmj011!$*#oQxqJVytp;ClW!E#91z3vGJ{v=g8*tfC3~7y?S*1ZDnbC z#RuYX$Eu@Pqv0QlrAA`ZL zfB$}h{QP{aR@>OvNRD2#Xc2ph#ge3th9-Q;9!<01e6 literal 0 HcmV?d00001 diff --git a/tests/suite/text/raw.typ b/tests/suite/text/raw.typ index 1ba216302..a7f58a8d0 100644 --- a/tests/suite/text/raw.typ +++ b/tests/suite/text/raw.typ @@ -676,6 +676,17 @@ a b c -------------------- `code` ``` +--- issue-5760-disable-cjk-latin-spacing-in-raw --- + +```typ +#let hi = "你好world" +``` + +#show raw: set text(cjk-latin-spacing: auto) +```typ +#let hi = "你好world" +``` + --- raw-theme-set-to-auto --- ```typ #let hi = "Hello World" From f239b0a6a1e68a016cacf19eeef2df52e4affeb9 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Fri, 31 Jan 2025 11:05:03 +0100 Subject: [PATCH 34/34] =?UTF-8?q?Change=20the=20default=20math=20class=20o?= =?UTF-8?q?f=20U+22A5=20=E2=8A=A5=20UP=20TACK=20to=20Normal=20(#5714)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 1 + crates/typst-layout/src/math/fragment.rs | 9 ++---- crates/typst-library/src/math/matrix.rs | 6 ++-- crates/typst-syntax/src/parser.rs | 3 +- crates/typst-utils/Cargo.toml | 1 + crates/typst-utils/src/lib.rs | 26 ++++++++++++++++++ ...985-up-tack-is-normal-perp-is-relation.png | Bin 0 -> 360 bytes tests/suite/math/class.typ | 5 ++++ 8 files changed, 41 insertions(+), 10 deletions(-) create mode 100644 tests/ref/issue-4985-up-tack-is-normal-perp-is-relation.png diff --git a/Cargo.lock b/Cargo.lock index ada3a3d4e..d2e410e14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3106,6 +3106,7 @@ dependencies = [ "rayon", "siphasher 1.0.1", "thin-vec", + "unicode-math-class", ] [[package]] diff --git a/crates/typst-layout/src/math/fragment.rs b/crates/typst-layout/src/math/fragment.rs index 81b726bad..1b508a349 100644 --- a/crates/typst-layout/src/math/fragment.rs +++ b/crates/typst-layout/src/math/fragment.rs @@ -13,6 +13,7 @@ use typst_library::math::{EquationElem, MathSize}; use typst_library::text::{Font, Glyph, Lang, Region, TextElem, TextItem}; use typst_library::visualize::Paint; use typst_syntax::Span; +use typst_utils::default_math_class; use unicode_math_class::MathClass; use super::{stretch_glyph, MathContext, Scaled}; @@ -275,11 +276,7 @@ impl GlyphFragment { span: Span, ) -> Self { let class = EquationElem::class_in(styles) - .or_else(|| match c { - ':' => Some(MathClass::Relation), - '.' | '/' | '⋯' | '⋱' | '⋰' | '⋮' => Some(MathClass::Normal), - _ => unicode_math_class::class(c), - }) + .or_else(|| default_math_class(c)) .unwrap_or(MathClass::Normal); let mut fragment = Self { @@ -629,7 +626,7 @@ pub enum Limits { impl Limits { /// The default limit configuration if the given character is the base. pub fn for_char(c: char) -> Self { - match unicode_math_class::class(c) { + match default_math_class(c) { Some(MathClass::Large) => { if is_integral_char(c) { Limits::Never diff --git a/crates/typst-library/src/math/matrix.rs b/crates/typst-library/src/math/matrix.rs index c74eb8fad..b6c4654ed 100644 --- a/crates/typst-library/src/math/matrix.rs +++ b/crates/typst-library/src/math/matrix.rs @@ -1,6 +1,6 @@ use smallvec::{smallvec, SmallVec}; use typst_syntax::Spanned; -use typst_utils::Numeric; +use typst_utils::{default_math_class, Numeric}; use unicode_math_class::MathClass; use crate::diag::{bail, At, HintedStrResult, StrResult}; @@ -292,7 +292,7 @@ impl Delimiter { pub fn char(c: char) -> StrResult { if !matches!( - unicode_math_class::class(c), + default_math_class(c), Some(MathClass::Opening | MathClass::Closing | MathClass::Fence), ) { bail!("invalid delimiter: \"{}\"", c) @@ -311,7 +311,7 @@ impl Delimiter { Some(']') => Self(Some('[')), Some('{') => Self(Some('}')), Some('}') => Self(Some('{')), - Some(c) => match unicode_math_class::class(c) { + Some(c) => match default_math_class(c) { Some(MathClass::Opening) => Self(char::from_u32(c as u32 + 1)), Some(MathClass::Closing) => Self(char::from_u32(c as u32 - 1)), _ => Self(Some(c)), diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index 55d5550b6..e187212da 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -3,6 +3,7 @@ use std::mem; use std::ops::{Index, IndexMut, Range}; use ecow::{eco_format, EcoString}; +use typst_utils::default_math_class; use unicode_math_class::MathClass; use crate::set::{syntax_set, SyntaxSet}; @@ -468,7 +469,7 @@ fn math_class(text: &str) -> Option { chars .next() .filter(|_| chars.next().is_none()) - .and_then(unicode_math_class::class) + .and_then(default_math_class) } /// Parse an argument list in math: `(a, b; c, d; size: #50%)`. diff --git a/crates/typst-utils/Cargo.toml b/crates/typst-utils/Cargo.toml index 5f828cff9..360e07d89 100644 --- a/crates/typst-utils/Cargo.toml +++ b/crates/typst-utils/Cargo.toml @@ -18,6 +18,7 @@ portable-atomic = { workspace = true } rayon = { workspace = true } siphasher = { workspace = true } thin-vec = { workspace = true } +unicode-math-class = { workspace = true } [lints] workspace = true diff --git a/crates/typst-utils/src/lib.rs b/crates/typst-utils/src/lib.rs index b59fe2f73..34d6a9432 100644 --- a/crates/typst-utils/src/lib.rs +++ b/crates/typst-utils/src/lib.rs @@ -31,6 +31,7 @@ use std::ops::{Add, Deref, Div, Mul, Neg, Sub}; use std::sync::Arc; use siphasher::sip128::{Hasher128, SipHasher13}; +use unicode_math_class::MathClass; /// Turn a closure into a struct implementing [`Debug`]. pub fn debug(f: F) -> impl Debug @@ -337,3 +338,28 @@ pub trait Numeric: /// Whether `self` consists only of finite parts. fn is_finite(self) -> bool; } + +/// Returns the default math class of a character in Typst, if it has one. +/// +/// This is determined by the Unicode math class, with some manual overrides. +pub fn default_math_class(c: char) -> Option { + match c { + // Better spacing. + // https://github.com/typst/typst/commit/2e039cb052fcb768027053cbf02ce396f6d7a6be + ':' => Some(MathClass::Relation), + + // Better spacing when used alongside + PLUS SIGN. + // https://github.com/typst/typst/pull/1726 + '⋯' | '⋱' | '⋰' | '⋮' => Some(MathClass::Normal), + + // Better spacing. + // https://github.com/typst/typst/pull/1855 + '.' | '/' => Some(MathClass::Normal), + + // ⊥ UP TACK should not be a relation, contrary to ⟂ PERPENDICULAR. + // https://github.com/typst/typst/pull/5714 + '\u{22A5}' => Some(MathClass::Normal), + + c => unicode_math_class::class(c), + } +} diff --git a/tests/ref/issue-4985-up-tack-is-normal-perp-is-relation.png b/tests/ref/issue-4985-up-tack-is-normal-perp-is-relation.png new file mode 100644 index 0000000000000000000000000000000000000000..acadc3be57b0eb584ab45622f4a1f116e7cc039b GIT binary patch literal 360 zcmV-u0hj)XP)goWYpqOi%HjV5;MjDv2vg z-a4Kg2#eR({%>xlpT!HhL0}hz<9ljg>f@EO{||_dPi^>5>o|V8XHUVpJ$wG32;Mb| zja))wi{G6Efpe&WuYp3JMoX?yi-(29-=OrVyL7g=9ZFX%qO-+qP&(xSJuLn`ZT?a; ziw$=zq=UuTC%(C$TKpvVf9@TcTl`b!|A7`%i`UNn@AQ!77Jm!>pTFZEipA@8+zX{c zP;c3=dd62Ey|}Zpq_cAi$So_Te;X~iMlBw-c+}z%U@-tBCHk)`{rg}50000)_a$ $limits(class("normal", ->))_a$ $ scripts(class("relation", x))_a $ + +--- issue-4985-up-tack-is-normal-perp-is-relation --- +$ top = 1 \ + bot = 2 \ + a perp b $