From 3fba256405c4aae9f121a07ddaa29cc10b825fc9 Mon Sep 17 00:00:00 2001 From: PgBiel <9021226+PgBiel@users.noreply.github.com> Date: Mon, 10 Feb 2025 07:39:04 -0300 Subject: [PATCH 1/9] Don't crash on image with zero DPI (#5835) --- crates/typst-layout/src/image.rs | 2 ++ crates/typst-library/src/visualize/image/raster.rs | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index d963ea50d..3e5b7d8bd 100644 --- a/crates/typst-layout/src/image.rs +++ b/crates/typst-layout/src/image.rs @@ -95,6 +95,8 @@ pub fn layout_image( } else { // If neither is forced, take the natural image size at the image's // DPI bounded by the available space. + // + // Division by DPI is fine since it's guaranteed to be positive. let dpi = image.dpi().unwrap_or(Image::DEFAULT_DPI); let natural = Axes::new(pxw, pxh).map(|v| Abs::inches(v / dpi)); Size::new( diff --git a/crates/typst-library/src/visualize/image/raster.rs b/crates/typst-library/src/visualize/image/raster.rs index d43b15486..0883fe71d 100644 --- a/crates/typst-library/src/visualize/image/raster.rs +++ b/crates/typst-library/src/visualize/image/raster.rs @@ -160,6 +160,8 @@ impl RasterImage { } /// The image's pixel density in pixels per inch, if known. + /// + /// This is guaranteed to be positive. pub fn dpi(&self) -> Option { self.0.dpi } @@ -334,6 +336,9 @@ fn apply_rotation(image: &mut DynamicImage, rotation: u32) { } /// Try to determine the DPI (dots per inch) of the image. +/// +/// This is guaranteed to be a positive value, or `None` if invalid or +/// unspecified. fn determine_dpi(data: &[u8], exif: Option<&exif::Exif>) -> Option { // Try to extract the DPI from the EXIF metadata. If that doesn't yield // anything, fall back to specialized procedures for extracting JPEG or PNG @@ -341,6 +346,7 @@ fn determine_dpi(data: &[u8], exif: Option<&exif::Exif>) -> Option { exif.and_then(exif_dpi) .or_else(|| jpeg_dpi(data)) .or_else(|| png_dpi(data)) + .filter(|&dpi| dpi > 0.0) } /// Try to get the DPI from the EXIF metadata. From 25e27169e1413c9e14184267be57fdbbb09e7c34 Mon Sep 17 00:00:00 2001 From: Malo <57839069+MDLC01@users.noreply.github.com> Date: Mon, 10 Feb 2025 11:39:32 +0100 Subject: [PATCH 2/9] Add warning for `pdf.embed` elem used with HTML (#5829) --- crates/typst-library/src/pdf/embed.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/crates/typst-library/src/pdf/embed.rs b/crates/typst-library/src/pdf/embed.rs index 001078e5e..f902e7f14 100644 --- a/crates/typst-library/src/pdf/embed.rs +++ b/crates/typst-library/src/pdf/embed.rs @@ -1,9 +1,12 @@ use ecow::EcoString; +use typst_library::foundations::Target; use typst_syntax::Spanned; -use crate::diag::{At, SourceResult}; +use crate::diag::{warning, At, SourceResult}; use crate::engine::Engine; -use crate::foundations::{elem, Bytes, Cast, Content, Derived, Packed, Show, StyleChain}; +use crate::foundations::{ + elem, Bytes, Cast, Content, Derived, Packed, Show, StyleChain, TargetElem, +}; use crate::introspection::Locatable; use crate::World; @@ -78,7 +81,12 @@ pub struct EmbedElem { } impl Show for Packed { - fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { + fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { + if TargetElem::target_in(styles) == Target::Html { + engine + .sink + .warn(warning!(self.span(), "embed was ignored during HTML export")); + } Ok(Content::empty()) } } From ee47cb846924235be6eae968a7853ea7860ccc51 Mon Sep 17 00:00:00 2001 From: TwoF1nger <140991913+TwoF1nger@users.noreply.github.com> Date: Mon, 10 Feb 2025 10:42:16 +0000 Subject: [PATCH 3/9] Add smart quotes for Bulgarian (#5807) --- crates/typst-library/src/text/smartquote.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/typst-library/src/text/smartquote.rs b/crates/typst-library/src/text/smartquote.rs index 2f89fe298..f457a6371 100644 --- a/crates/typst-library/src/text/smartquote.rs +++ b/crates/typst-library/src/text/smartquote.rs @@ -251,6 +251,7 @@ impl<'s> SmartQuotes<'s> { "el" => ("‘", "’", "«", "»"), "he" => ("’", "’", "”", "”"), "hr" => ("‘", "’", "„", "”"), + "bg" => ("’", "’", "„", "“"), _ if lang.dir() == Dir::RTL => ("’", "‘", "”", "“"), _ => default, }; From 89e71acecd4a3a06943d0bd4443fc80a9b8f41e4 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 10 Feb 2025 15:37:19 +0100 Subject: [PATCH 4/9] Respect `par` constructor arguments (#5842) --- crates/typst-layout/src/flow/collect.rs | 13 +- crates/typst-layout/src/flow/mod.rs | 77 ++++--- crates/typst-layout/src/inline/collect.rs | 54 +---- crates/typst-layout/src/inline/finalize.rs | 10 +- crates/typst-layout/src/inline/line.rs | 51 +++-- crates/typst-layout/src/inline/linebreak.rs | 44 ++-- crates/typst-layout/src/inline/mod.rs | 191 +++++++++++++++++- crates/typst-layout/src/inline/prepare.rs | 73 +------ crates/typst-layout/src/math/text.rs | 1 - crates/typst-library/src/model/link.rs | 4 +- crates/typst-library/src/text/mod.rs | 25 +-- crates/typst-library/src/text/raw.rs | 6 +- tests/ref/issue-5831-par-constructor-args.png | Bin 0 -> 1356 bytes tests/suite/model/par.typ | 14 ++ 14 files changed, 314 insertions(+), 249 deletions(-) create mode 100644 tests/ref/issue-5831-par-constructor-args.png diff --git a/crates/typst-layout/src/flow/collect.rs b/crates/typst-layout/src/flow/collect.rs index 34362a6c5..2c14f7a37 100644 --- a/crates/typst-layout/src/flow/collect.rs +++ b/crates/typst-layout/src/flow/collect.rs @@ -124,7 +124,6 @@ impl<'a> Collector<'a, '_, '_> { styles, self.base, self.expand, - None, )? .into_frames(); @@ -133,7 +132,8 @@ impl<'a> Collector<'a, '_, '_> { self.output.push(Child::Tag(&elem.tag)); } - self.lines(lines, styles); + let leading = ParElem::leading_in(styles); + self.lines(lines, leading, styles); for (c, _) in &self.children[end..] { let elem = c.to_packed::().unwrap(); @@ -169,10 +169,12 @@ impl<'a> Collector<'a, '_, '_> { )? .into_frames(); - let spacing = ParElem::spacing_in(styles); + let spacing = elem.spacing(styles); + let leading = elem.leading(styles); + self.output.push(Child::Rel(spacing.into(), 4)); - self.lines(lines, styles); + self.lines(lines, leading, styles); self.output.push(Child::Rel(spacing.into(), 4)); self.par_situation = ParSituation::Consecutive; @@ -181,9 +183,8 @@ impl<'a> Collector<'a, '_, '_> { } /// Collect laid-out lines. - fn lines(&mut self, lines: Vec, styles: StyleChain<'a>) { + fn lines(&mut self, lines: Vec, leading: Abs, 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. diff --git a/crates/typst-layout/src/flow/mod.rs b/crates/typst-layout/src/flow/mod.rs index 2acbbcef3..cba228bcd 100644 --- a/crates/typst-layout/src/flow/mod.rs +++ b/crates/typst-layout/src/flow/mod.rs @@ -197,7 +197,50 @@ pub fn layout_flow<'a>( mode: FlowMode, ) -> SourceResult { // Prepare configuration that is shared across the whole flow. - let config = Config { + let config = configuration(shared, regions, columns, column_gutter, mode); + + // Collect the elements into pre-processed children. These are much easier + // to handle than the raw elements. + let bump = Bump::new(); + let children = collect( + engine, + &bump, + children, + locator.next(&()), + Size::new(config.columns.width, regions.full), + regions.expand.x, + mode, + )?; + + let mut work = Work::new(&children); + let mut finished = vec![]; + + // This loop runs once per region produced by the flow layout. + loop { + let frame = compose(engine, &mut work, &config, locator.next(&()), regions)?; + finished.push(frame); + + // Terminate the loop when everything is processed, though draining the + // backlog if necessary. + if work.done() && (!regions.expand.y || regions.backlog.is_empty()) { + break; + } + + regions.next(); + } + + Ok(Fragment::frames(finished)) +} + +/// Determine the flow's configuration. +fn configuration<'x>( + shared: StyleChain<'x>, + regions: Regions, + columns: NonZeroUsize, + column_gutter: Rel, + mode: FlowMode, +) -> Config<'x> { + Config { mode, shared, columns: { @@ -235,39 +278,7 @@ pub fn layout_flow<'a>( ) }, }), - }; - - // Collect the elements into pre-processed children. These are much easier - // to handle than the raw elements. - let bump = Bump::new(); - let children = collect( - engine, - &bump, - children, - locator.next(&()), - Size::new(config.columns.width, regions.full), - regions.expand.x, - mode, - )?; - - let mut work = Work::new(&children); - let mut finished = vec![]; - - // This loop runs once per region produced by the flow layout. - loop { - let frame = compose(engine, &mut work, &config, locator.next(&()), regions)?; - finished.push(frame); - - // Terminate the loop when everything is processed, though draining the - // backlog if necessary. - if work.done() && (!regions.expand.y || regions.backlog.is_empty()) { - break; - } - - regions.next(); } - - Ok(Fragment::frames(finished)) } /// The work that is left to do by flow layout. diff --git a/crates/typst-layout/src/inline/collect.rs b/crates/typst-layout/src/inline/collect.rs index 14cf2e3b8..5a1b7b4fc 100644 --- a/crates/typst-layout/src/inline/collect.rs +++ b/crates/typst-layout/src/inline/collect.rs @@ -2,10 +2,8 @@ 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, + Abs, 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, @@ -123,40 +121,20 @@ pub fn collect<'a>( children: &[Pair<'a>], engine: &mut Engine<'_>, locator: &mut SplitLocator<'a>, - styles: StyleChain<'a>, + config: &Config, region: Size, - 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 !config.first_line_indent.is_zero() { + collector.push_item(Item::Absolute(config.first_line_indent, false)); + collector.spans.push(1, Span::detached()); + } - if let Some(situation) = situation { - let first_line_indent = ParElem::first_line_indent_in(styles); - if !first_line_indent.amount.is_zero() - && match situation { - // First-line indent for the first paragraph after a list bullet - // just looks bad. - ParSituation::First => first_line_indent.all && !in_list(styles), - ParSituation::Consecutive => true, - ParSituation::Other => first_line_indent.all, - } - && AlignElem::alignment_in(styles).resolve(styles).x - == outer_dir.start().into() - { - collector.push_item(Item::Absolute( - first_line_indent.amount.resolve(styles), - false, - )); - collector.spans.push(1, Span::detached()); - } - - let hang = ParElem::hanging_indent_in(styles); - if !hang.is_zero() { - collector.push_item(Item::Absolute(-hang, false)); - collector.spans.push(1, Span::detached()); - } + if !config.hanging_indent.is_zero() { + collector.push_item(Item::Absolute(-config.hanging_indent, false)); + collector.spans.push(1, Span::detached()); } for &(child, styles) in children { @@ -167,7 +145,7 @@ pub fn collect<'a>( } else if let Some(elem) = child.to_packed::() { collector.build_text(styles, |full| { let dir = TextElem::dir_in(styles); - if dir != outer_dir { + if dir != config.dir { // Insert "Explicit Directional Embedding". match dir { Dir::LTR => full.push_str(LTR_EMBEDDING), @@ -182,7 +160,7 @@ pub fn collect<'a>( full.push_str(&elem.text); } - if dir != outer_dir { + if dir != config.dir { // Insert "Pop Directional Formatting". full.push_str(POP_EMBEDDING); } @@ -265,16 +243,6 @@ pub fn collect<'a>( Ok((collector.full, collector.segments, collector.spans)) } -/// Whether we have a list ancestor. -/// -/// When we support some kind of more general ancestry mechanism, this can -/// become more elegant. -fn in_list(styles: StyleChain) -> bool { - ListElem::depth_in(styles).0 > 0 - || !EnumElem::parents_in(styles).is_empty() - || TermsElem::within_in(styles) -} - /// Collects segments. struct Collector<'a> { full: String, diff --git a/crates/typst-layout/src/inline/finalize.rs b/crates/typst-layout/src/inline/finalize.rs index 7ad287c45..c9de0085e 100644 --- a/crates/typst-layout/src/inline/finalize.rs +++ b/crates/typst-layout/src/inline/finalize.rs @@ -9,7 +9,6 @@ pub fn finalize( engine: &mut Engine, p: &Preparation, lines: &[Line], - styles: StyleChain, region: Size, expand: bool, locator: &mut SplitLocator<'_>, @@ -19,9 +18,10 @@ pub fn finalize( let width = if !region.x.is_finite() || (!expand && lines.iter().all(|line| line.fr().is_zero())) { - region - .x - .min(p.hang + lines.iter().map(|line| line.width).max().unwrap_or_default()) + region.x.min( + p.config.hanging_indent + + lines.iter().map(|line| line.width).max().unwrap_or_default(), + ) } else { region.x }; @@ -29,7 +29,7 @@ pub fn finalize( // Stack the lines into one frame per region. lines .iter() - .map(|line| commit(engine, p, line, width, region.y, locator, styles)) + .map(|line| commit(engine, p, line, width, region.y, locator)) .collect::>() .map(Fragment::frames) } diff --git a/crates/typst-layout/src/inline/line.rs b/crates/typst-layout/src/inline/line.rs index 9f6973807..bd08f30ef 100644 --- a/crates/typst-layout/src/inline/line.rs +++ b/crates/typst-layout/src/inline/line.rs @@ -2,10 +2,9 @@ use std::fmt::{self, Debug, Formatter}; use std::ops::{Deref, DerefMut}; use typst_library::engine::Engine; -use typst_library::foundations::NativeElement; use typst_library::introspection::{SplitLocator, Tag}; use typst_library::layout::{Abs, Dir, Em, Fr, Frame, FrameItem, Point}; -use typst_library::model::{ParLine, ParLineMarker}; +use typst_library::model::ParLineMarker; use typst_library::text::{Lang, TextElem}; use typst_utils::Numeric; @@ -135,7 +134,7 @@ pub fn line<'a>( // Whether the line is justified. let justify = full.ends_with(LINE_SEPARATOR) - || (p.justify && breakpoint != Breakpoint::Mandatory); + || (p.config.justify && breakpoint != Breakpoint::Mandatory); // Process dashes. let dash = if breakpoint.is_hyphen() || full.ends_with(SHY) { @@ -157,14 +156,14 @@ pub fn line<'a>( // Add a hyphen at the line start, if a previous dash should be repeated. if pred.map_or(false, |pred| should_repeat_hyphen(pred, full)) { if let Some(shaped) = items.first_text_mut() { - shaped.prepend_hyphen(engine, p.fallback); + shaped.prepend_hyphen(engine, p.config.fallback); } } // Add a hyphen at the line end, if we ended on a soft hyphen. if dash == Some(Dash::Soft) { if let Some(shaped) = items.last_text_mut() { - shaped.push_hyphen(engine, p.fallback); + shaped.push_hyphen(engine, p.config.fallback); } } @@ -234,13 +233,13 @@ where { // If there is nothing bidirectional going on, skip reordering. let Some(bidi) = &p.bidi else { - f(range, p.dir == Dir::RTL); + f(range, p.config.dir == Dir::RTL); return; }; // The bidi crate panics for empty lines. if range.is_empty() { - f(range, p.dir == Dir::RTL); + f(range, p.config.dir == Dir::RTL); return; } @@ -308,13 +307,13 @@ fn collect_range<'a>( /// punctuation marks at line start or line end. fn adjust_cj_at_line_boundaries(p: &Preparation, text: &str, items: &mut Items) { if text.starts_with(BEGIN_PUNCT_PAT) - || (p.cjk_latin_spacing && text.starts_with(is_of_cj_script)) + || (p.config.cjk_latin_spacing && text.starts_with(is_of_cj_script)) { adjust_cj_at_line_start(p, items); } if text.ends_with(END_PUNCT_PAT) - || (p.cjk_latin_spacing && text.ends_with(is_of_cj_script)) + || (p.config.cjk_latin_spacing && text.ends_with(is_of_cj_script)) { adjust_cj_at_line_end(p, items); } @@ -332,7 +331,10 @@ fn adjust_cj_at_line_start(p: &Preparation, items: &mut Items) { let shrink = glyph.shrinkability().0; glyph.shrink_left(shrink); shaped.width -= shrink.at(shaped.size); - } else if p.cjk_latin_spacing && glyph.is_cj_script() && glyph.x_offset > Em::zero() { + } else if p.config.cjk_latin_spacing + && glyph.is_cj_script() + && glyph.x_offset > Em::zero() + { // If the first glyph is a CJK character adjusted by // [`add_cjk_latin_spacing`], restore the original width. let glyph = shaped.glyphs.to_mut().first_mut().unwrap(); @@ -359,7 +361,7 @@ fn adjust_cj_at_line_end(p: &Preparation, items: &mut Items) { let punct = shaped.glyphs.to_mut().last_mut().unwrap(); punct.shrink_right(shrink); shaped.width -= shrink.at(shaped.size); - } else if p.cjk_latin_spacing + } else if p.config.cjk_latin_spacing && glyph.is_cj_script() && (glyph.x_advance - glyph.x_offset) > Em::one() { @@ -424,16 +426,15 @@ pub fn commit( width: Abs, full: Abs, locator: &mut SplitLocator<'_>, - styles: StyleChain, ) -> SourceResult { - let mut remaining = width - line.width - p.hang; + let mut remaining = width - line.width - p.config.hanging_indent; 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. In an RTL paragraph, the // hanging indent arises naturally due to the line width. - if p.dir == Dir::LTR { - offset += p.hang; + if p.config.dir == Dir::LTR { + offset += p.config.hanging_indent; } // Handle hanging punctuation to the left. @@ -554,11 +555,13 @@ pub fn commit( let mut output = Frame::soft(size); output.set_baseline(top); - add_par_line_marker(&mut output, styles, engine, locator, top); + if let Some(marker) = &p.config.numbering_marker { + add_par_line_marker(&mut output, marker, engine, locator, top); + } // Construct the line's frame. for (offset, frame) in frames { - let x = offset + p.align.position(remaining); + let x = offset + p.config.align.position(remaining); let y = top - frame.baseline(); output.push_frame(Point::new(x, y), frame); } @@ -575,26 +578,18 @@ pub fn commit( /// number in the margin, is aligned to the line's baseline. fn add_par_line_marker( output: &mut Frame, - styles: StyleChain, + marker: &Packed, engine: &mut Engine, locator: &mut SplitLocator, top: Abs, ) { - let Some(numbering) = ParLine::numbering_in(styles) else { return }; - let margin = ParLine::number_margin_in(styles); - let align = ParLine::number_align_in(styles); - - // Delay resolving the number clearance until line numbers are laid out to - // avoid inconsistent spacing depending on varying font size. - let clearance = ParLine::number_clearance_in(styles); - // Elements in tags must have a location for introspection to work. We do // the work here instead of going through all of the realization process // just for this, given we don't need to actually place the marker as we // manually search for it in the frame later (when building a root flow, // where line numbers can be displayed), so we just need it to be in a tag // and to be valid (to have a location). - let mut marker = ParLineMarker::new(numbering, align, margin, clearance).pack(); + let mut marker = marker.clone(); let key = typst_utils::hash128(&marker); let loc = locator.next_location(engine.introspector, key); marker.set_location(loc); @@ -606,7 +601,7 @@ fn add_par_line_marker( // line's general baseline. However, the line number will still need to // manually adjust its own 'y' position based on its own baseline. let pos = Point::with_y(top); - output.push(pos, FrameItem::Tag(Tag::Start(marker))); + output.push(pos, FrameItem::Tag(Tag::Start(marker.pack()))); output.push(pos, FrameItem::Tag(Tag::End(loc, key))); } diff --git a/crates/typst-layout/src/inline/linebreak.rs b/crates/typst-layout/src/inline/linebreak.rs index 87113c689..a9f21188b 100644 --- a/crates/typst-layout/src/inline/linebreak.rs +++ b/crates/typst-layout/src/inline/linebreak.rs @@ -110,15 +110,7 @@ pub fn linebreak<'a>( p: &'a Preparation<'a>, width: Abs, ) -> Vec> { - let linebreaks = p.linebreaks.unwrap_or_else(|| { - if p.justify { - Linebreaks::Optimized - } else { - Linebreaks::Simple - } - }); - - match linebreaks { + match p.config.linebreaks { Linebreaks::Simple => linebreak_simple(engine, p, width), Linebreaks::Optimized => linebreak_optimized(engine, p, width), } @@ -384,7 +376,7 @@ fn linebreak_optimized_approximate( // Whether the line is justified. This is not 100% accurate w.r.t // to line()'s behaviour, but good enough. - let justify = p.justify && breakpoint != Breakpoint::Mandatory; + let justify = p.config.justify && breakpoint != Breakpoint::Mandatory; // We don't really know whether the line naturally ends with a dash // here, so we can miss that case, but it's ok, since all of this @@ -573,7 +565,7 @@ fn raw_ratio( // calculate the extra amount. Also, don't divide by zero. let extra_stretch = (delta - adjustability) / justifiables.max(1) as f64; // Normalize the amount by half the em size. - ratio = 1.0 + extra_stretch / (p.size / 2.0); + ratio = 1.0 + extra_stretch / (p.config.font_size / 2.0); } // The min value must be < MIN_RATIO, but how much smaller doesn't matter @@ -663,9 +655,9 @@ fn breakpoints(p: &Preparation, mut f: impl FnMut(usize, Breakpoint)) { return; } - let hyphenate = p.hyphenate != Some(false); + let hyphenate = p.config.hyphenate != Some(false); let lb = LINEBREAK_DATA.as_borrowed(); - let segmenter = match p.lang { + let segmenter = match p.config.lang { Some(Lang::CHINESE | Lang::JAPANESE) => &CJ_SEGMENTER, _ => &SEGMENTER, }; @@ -830,18 +822,18 @@ fn linebreak_link(link: &str, mut f: impl FnMut(usize)) { /// Whether hyphenation is enabled at the given offset. fn hyphenate_at(p: &Preparation, offset: usize) -> bool { - p.hyphenate - .or_else(|| { - let (_, item) = p.get(offset); - let styles = item.text()?.styles; - Some(TextElem::hyphenate_in(styles)) - }) - .unwrap_or(false) + p.config.hyphenate.unwrap_or_else(|| { + let (_, item) = p.get(offset); + match item.text() { + Some(text) => TextElem::hyphenate_in(text.styles).unwrap_or(p.config.justify), + None => false, + } + }) } /// The text language at the given offset. fn lang_at(p: &Preparation, offset: usize) -> Option { - let lang = p.lang.or_else(|| { + let lang = p.config.lang.or_else(|| { let (_, item) = p.get(offset); let styles = item.text()?.styles; Some(TextElem::lang_in(styles)) @@ -865,13 +857,13 @@ impl CostMetrics { fn compute(p: &Preparation) -> Self { Self { // When justifying, we may stretch spaces below their natural width. - min_ratio: if p.justify { MIN_RATIO } else { 0.0 }, - min_approx_ratio: if p.justify { MIN_APPROX_RATIO } else { 0.0 }, + min_ratio: if p.config.justify { MIN_RATIO } else { 0.0 }, + min_approx_ratio: if p.config.justify { MIN_APPROX_RATIO } else { 0.0 }, // Approximate hyphen width for estimates. - approx_hyphen_width: Em::new(0.33).at(p.size), + approx_hyphen_width: Em::new(0.33).at(p.config.font_size), // Costs. - hyph_cost: DEFAULT_HYPH_COST * p.costs.hyphenation().get(), - runt_cost: DEFAULT_RUNT_COST * p.costs.runt().get(), + hyph_cost: DEFAULT_HYPH_COST * p.config.costs.hyphenation().get(), + runt_cost: DEFAULT_RUNT_COST * p.config.costs.runt().get(), } } diff --git a/crates/typst-layout/src/inline/mod.rs b/crates/typst-layout/src/inline/mod.rs index f8a36368d..5ef820d07 100644 --- a/crates/typst-layout/src/inline/mod.rs +++ b/crates/typst-layout/src/inline/mod.rs @@ -13,12 +13,17 @@ pub use self::box_::layout_box; use comemo::{Track, Tracked, TrackedMut}; use typst_library::diag::SourceResult; use typst_library::engine::{Engine, Route, Sink, Traced}; -use typst_library::foundations::{Packed, StyleChain}; +use typst_library::foundations::{Packed, Resolve, Smart, StyleChain}; use typst_library::introspection::{Introspector, Locator, LocatorLink, SplitLocator}; -use typst_library::layout::{Fragment, Size}; -use typst_library::model::ParElem; +use typst_library::layout::{Abs, AlignElem, Dir, FixedAlignment, Fragment, Size}; +use typst_library::model::{ + EnumElem, FirstLineIndent, Linebreaks, ListElem, ParElem, ParLine, ParLineMarker, + TermsElem, +}; use typst_library::routines::{Arenas, Pair, RealizationKind, Routines}; +use typst_library::text::{Costs, Lang, TextElem}; use typst_library::World; +use typst_utils::{Numeric, SliceExt}; use self::collect::{collect, Item, Segment, SpanMapper}; use self::deco::decorate; @@ -98,7 +103,7 @@ fn layout_par_impl( styles, )?; - layout_inline( + layout_inline_impl( &mut engine, &children, &mut locator, @@ -106,33 +111,134 @@ fn layout_par_impl( region, expand, Some(situation), + &ConfigBase { + justify: elem.justify(styles), + linebreaks: elem.linebreaks(styles), + first_line_indent: elem.first_line_indent(styles), + hanging_indent: elem.hanging_indent(styles), + }, ) } /// 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>, + shared: StyleChain<'a>, + region: Size, + expand: bool, +) -> SourceResult { + layout_inline_impl( + engine, + children, + locator, + shared, + region, + expand, + None, + &ConfigBase { + justify: ParElem::justify_in(shared), + linebreaks: ParElem::linebreaks_in(shared), + first_line_indent: ParElem::first_line_indent_in(shared), + hanging_indent: ParElem::hanging_indent_in(shared), + }, + ) +} + +/// The internal implementation of [`layout_inline`]. +#[allow(clippy::too_many_arguments)] +fn layout_inline_impl<'a>( + engine: &mut Engine, + children: &[Pair<'a>], + locator: &mut SplitLocator<'a>, + shared: StyleChain<'a>, region: Size, expand: bool, par: Option, + base: &ConfigBase, ) -> SourceResult { + // Prepare configuration that is shared across the whole inline layout. + let config = configuration(base, children, shared, par); + // Collect all text into one string for BiDi analysis. - let (text, segments, spans) = - collect(children, engine, locator, styles, region, par)?; + let (text, segments, spans) = collect(children, engine, locator, &config, region)?; // Perform BiDi analysis and performs some preparation steps before we // proceed to line breaking. - let p = prepare(engine, children, &text, segments, spans, styles, par)?; + let p = prepare(engine, &config, &text, segments, spans)?; // Break the text into lines. - let lines = linebreak(engine, &p, region.x - p.hang); + let lines = linebreak(engine, &p, region.x - config.hanging_indent); // Turn the selected lines into frames. - finalize(engine, &p, &lines, styles, region, expand, locator) + finalize(engine, &p, &lines, region, expand, locator) +} + +/// Determine the inline layout's configuration. +fn configuration( + base: &ConfigBase, + children: &[Pair], + shared: StyleChain, + situation: Option, +) -> Config { + let justify = base.justify; + let font_size = TextElem::size_in(shared); + let dir = TextElem::dir_in(shared); + + Config { + justify, + linebreaks: base.linebreaks.unwrap_or_else(|| { + if justify { + Linebreaks::Optimized + } else { + Linebreaks::Simple + } + }), + first_line_indent: { + let FirstLineIndent { amount, all } = base.first_line_indent; + if !amount.is_zero() + && match situation { + // First-line indent for the first paragraph after a list + // bullet just looks bad. + Some(ParSituation::First) => all && !in_list(shared), + Some(ParSituation::Consecutive) => true, + Some(ParSituation::Other) => all, + None => false, + } + && AlignElem::alignment_in(shared).resolve(shared).x == dir.start().into() + { + amount.at(font_size) + } else { + Abs::zero() + } + }, + hanging_indent: if situation.is_some() { + base.hanging_indent + } else { + Abs::zero() + }, + numbering_marker: ParLine::numbering_in(shared).map(|numbering| { + Packed::new(ParLineMarker::new( + numbering, + ParLine::number_align_in(shared), + ParLine::number_margin_in(shared), + // Delay resolving the number clearance until line numbers are + // laid out to avoid inconsistent spacing depending on varying + // font size. + ParLine::number_clearance_in(shared), + )) + }), + align: AlignElem::alignment_in(shared).fix(dir).x, + font_size, + dir, + hyphenate: shared_get(children, shared, TextElem::hyphenate_in) + .map(|uniform| uniform.unwrap_or(justify)), + lang: shared_get(children, shared, TextElem::lang_in), + fallback: TextElem::fallback_in(shared), + cjk_latin_spacing: TextElem::cjk_latin_spacing_in(shared).is_auto(), + costs: TextElem::costs_in(shared), + } } /// Distinguishes between a few different kinds of paragraphs. @@ -148,3 +254,66 @@ pub enum ParSituation { /// Any other kind of paragraph. Other, } + +/// Raw values from a `ParElem` or style chain. Used to initialize a [`Config`]. +struct ConfigBase { + justify: bool, + linebreaks: Smart, + first_line_indent: FirstLineIndent, + hanging_indent: Abs, +} + +/// Shared configuration for the whole inline layout. +struct Config { + /// Whether to justify text. + justify: bool, + /// How to determine line breaks. + linebreaks: Linebreaks, + /// The indent the first line of a paragraph should have. + first_line_indent: Abs, + /// The indent that all but the first line of a paragraph should have. + hanging_indent: Abs, + /// Configuration for line numbering. + numbering_marker: Option>, + /// The resolved horizontal alignment. + align: FixedAlignment, + /// The text size. + font_size: Abs, + /// The dominant direction. + dir: Dir, + /// A uniform hyphenation setting (only `Some(_)` if it's the same for all + /// children, otherwise `None`). + hyphenate: Option, + /// The text language (only `Some(_)` if it's the same for all + /// children, otherwise `None`). + lang: Option, + /// Whether font fallback is enabled. + fallback: bool, + /// Whether to add spacing between CJK and Latin characters. + cjk_latin_spacing: bool, + /// Costs for various layout decisions. + costs: Costs, +} + +/// 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) +} + +/// 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) +} diff --git a/crates/typst-layout/src/inline/prepare.rs b/crates/typst-layout/src/inline/prepare.rs index 0344d4331..5d7fcd7cb 100644 --- a/crates/typst-layout/src/inline/prepare.rs +++ b/crates/typst-layout/src/inline/prepare.rs @@ -1,9 +1,4 @@ -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 typst_library::layout::{Dir, Em}; use unicode_bidi::{BidiInfo, Level as BidiLevel}; use super::*; @@ -17,6 +12,8 @@ use super::*; pub struct Preparation<'a> { /// The full text. pub text: &'a str, + /// Configuration for inline layout. + pub config: &'a Config, /// Bidirectional text embedding levels. /// /// This is `None` if all text directions are uniform (all the base @@ -28,28 +25,6 @@ pub struct Preparation<'a> { pub indices: Vec, /// The span mapper. pub spans: SpanMapper, - /// Whether to hyphenate if it's the same for all children. - pub hyphenate: Option, - /// Costs for various layout decisions. - pub costs: Costs, - /// The dominant direction. - pub dir: Dir, - /// The text language if it's the same for all children. - pub lang: Option, - /// The resolved horizontal alignment. - pub align: FixedAlignment, - /// Whether to justify text. - pub justify: bool, - /// 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. - pub fallback: bool, - /// How to determine line breaks. - pub linebreaks: Smart, - /// The text size. - pub size: Abs, } impl<'a> Preparation<'a> { @@ -80,15 +55,12 @@ impl<'a> Preparation<'a> { #[typst_macros::time] pub fn prepare<'a>( engine: &mut Engine, - children: &[Pair<'a>], + config: &'a Config, text: &'a str, segments: Vec>, spans: SpanMapper, - styles: StyleChain<'a>, - situation: Option, ) -> SourceResult> { - let dir = TextElem::dir_in(styles); - let default_level = match dir { + let default_level = match config.dir { Dir::RTL => BidiLevel::rtl(), _ => BidiLevel::ltr(), }; @@ -124,51 +96,20 @@ pub fn prepare<'a>( indices.extend(range.clone().map(|_| i)); } - let cjk_latin_spacing = TextElem::cjk_latin_spacing_in(styles).is_auto(); - if cjk_latin_spacing { + if config.cjk_latin_spacing { add_cjk_latin_spacing(&mut items); } - // Only apply hanging indent to real paragraphs. - let hang = if situation.is_some() { - ParElem::hanging_indent_in(styles) - } else { - Abs::zero() - }; - Ok(Preparation { + config, text, bidi: is_bidi.then_some(bidi), items, indices, spans, - hyphenate: shared_get(children, styles, TextElem::hyphenate_in), - costs: TextElem::costs_in(styles), - dir, - lang: shared_get(children, styles, TextElem::lang_in), - align: AlignElem::alignment_in(styles).resolve(styles).x, - justify: ParElem::justify_in(styles), - hang, - cjk_latin_spacing, - fallback: TextElem::fallback_in(styles), - linebreaks: ParElem::linebreaks_in(styles), - size: TextElem::size_in(styles), }) } -/// 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/math/text.rs b/crates/typst-layout/src/math/text.rs index 9a64992aa..59ac5b089 100644 --- a/crates/typst-layout/src/math/text.rs +++ b/crates/typst-layout/src/math/text.rs @@ -107,7 +107,6 @@ fn layout_inline_text( styles, Size::splat(Abs::inf()), false, - None, )? .into_frame(); diff --git a/crates/typst-library/src/model/link.rs b/crates/typst-library/src/model/link.rs index 24b746b7e..ea85aa945 100644 --- a/crates/typst-library/src/model/link.rs +++ b/crates/typst-library/src/model/link.rs @@ -11,7 +11,7 @@ use crate::foundations::{ use crate::html::{attr, tag, HtmlElem}; use crate::introspection::Location; use crate::layout::Position; -use crate::text::{Hyphenate, TextElem}; +use crate::text::TextElem; /// Links to a URL or a location in the document. /// @@ -138,7 +138,7 @@ impl Show for Packed { 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.set(TextElem::set_hyphenate(Smart::Custom(false))); out } } diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index 12f4e4c59..30c2ea1d1 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -51,7 +51,6 @@ use crate::foundations::{ }; use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel}; use crate::math::{EquationElem, MathSize}; -use crate::model::ParElem; use crate::visualize::{Color, Paint, RelativeTo, Stroke}; use crate::World; @@ -504,9 +503,8 @@ pub struct TextElem { /// enabling hyphenation can /// improve justification. /// ``` - #[resolve] #[ghost] - pub hyphenate: Hyphenate, + pub hyphenate: Smart, /// The "cost" of various choices when laying out text. A higher cost means /// the layout engine will make the choice less often. Costs are specified @@ -1110,27 +1108,6 @@ impl Resolve for TextDir { } } -/// Whether to hyphenate text. -#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Hyphenate(pub Smart); - -cast! { - Hyphenate, - self => self.0.into_value(), - v: Smart => Self(v), -} - -impl Resolve for Hyphenate { - type Output = bool; - - fn resolve(self, styles: StyleChain) -> Self::Output { - match self.0 { - Smart::Auto => ParElem::justify_in(styles), - Smart::Custom(v) => v, - } - } -} - /// A set of stylistic sets to enable. #[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Hash)] pub struct StylisticSets(u32); diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs index 5bb21e43a..b330c01ef 100644 --- a/crates/typst-library/src/text/raw.rs +++ b/crates/typst-library/src/text/raw.rs @@ -21,9 +21,7 @@ use crate::html::{tag, HtmlElem}; use crate::layout::{BlockBody, BlockElem, Em, HAlignment}; use crate::loading::{DataSource, Load}; use crate::model::{Figurable, ParElem}; -use crate::text::{ - FontFamily, FontList, Hyphenate, LinebreakElem, LocalName, TextElem, TextSize, -}; +use crate::text::{FontFamily, FontList, LinebreakElem, LocalName, TextElem, TextSize}; use crate::visualize::Color; use crate::World; @@ -472,7 +470,7 @@ impl ShowSet for Packed { let mut out = Styles::new(); out.set(TextElem::set_overhang(false)); out.set(TextElem::set_lang(Lang::ENGLISH)); - out.set(TextElem::set_hyphenate(Hyphenate(Smart::Custom(false)))); + out.set(TextElem::set_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))); diff --git a/tests/ref/issue-5831-par-constructor-args.png b/tests/ref/issue-5831-par-constructor-args.png new file mode 100644 index 0000000000000000000000000000000000000000..440b612ba938a714148f598c52501450b6fe85aa GIT binary patch literal 1356 zcmV-S1+)5zP)e!Amty9JdH6Cr0@k9ib)FL`~)DA^aL==tIa*S3aps_WI7!m|Y zxFm8E5iG3+fk2BwOcqTzB4-jpf+07_{z?KbiX9NGm^b@dzf>25RyYT zs0D5-@$jV-NA4G?-k~?$>~K6F+X3hRMP)ZIVm& zAxx^J4#S0*9O?TS@CfpfU{UNgDFHmUkI$(!BY>lI6h>{23jh+X4tI>0JDm0&ivoa3 zkEDw_n+oVx16pIB4xW~dwgFITff2dw6)@Z|6w6sVML1p;_7nh424pd6)y&FRL(HV3$R&Z^hfP+{YE&z$wlW5_yB$yb}sfP;BGNA zTVarFY*PDGlQV5F#T_7Te6Sca2M%JrfV`QKPIm^sxo2>L-i+err>efBd!l=@ZrSZWp0Kgs^ zXTQ(LTg0kfpue)t9B@Y1pSPzz*Ka5#M!eq<&NP3`2Y{CMHPxRE_|*iT$tvNNy%Hvf zK`04}g0MQ8hv0!Dnhb!<+HR8*Mh{HL?daC_po@4hb}3{!I9o|2F%0b!IEXe2(GCDT zgua_FXr3E;OpiL>WC6JoiPy{4{&rTbtLHmG;-yGTooEF-wTl1$lIf4hT`R=Y+36EU z^Ne6ud0H<#4RJ3Ikvv`_a&BcY4XEUo|2?0tJo+%X>xEymLpIqS)pRxas-eSpaDTy zo$7VSKSypN^LkmuIA4JP+2hX?R=8thdmlMhB8kSg7VQKqIFHZKwHXXU8FX6PRIn|Z zziE812*COdBwv?CK==#J(4}rTRE6R7C{1n&1uzD$RpDpevv7>p%HL888u~)4i{Y4n zZ>NBUfvx!6_vUC~ylg5sxl$VdPjd#Ki5B2MUGrY#q$#_eguxdGQNX$s**|GNa*XOz z3lNou$OhSVcyg>k>;w4Vc@RD{YyBo*7M5Du)}zY2+%?Us{xhzC@eJ<7JJ`eK;~Xwh zcj%5q5^>j?aF8WqqyXp6rhRY7+nere+$Ax^aWZk?8yP$u=EWyguWP*-()rsDnT$tp zm!FOzCVxv?j3E2nZUnB~o$=>13dqy1*{Yy|C=JDwzq-+=TGe$j*0TEEPy~07F3sTC=IM0v?AQC=K8$jO zR8(J8+*47kHi;V0AySvvr1$j2w_qGQm5UxVi`wc;)GrJLQ!oX45&j3xlV%AnmsgDd O0000).len(), 1) +--- issue-5831-par-constructor-args --- +// Make sure that all arguments are also respected in the constructor. +A +#par( + leading: 2pt, + spacing: 20pt, + justify: true, + linebreaks: "simple", + first-line-indent: (amount: 1em, all: true), + hanging-indent: 5pt, +)[ + The par function has a constructor and justification. +] + --- show-par-set-block-hint --- // Warning: 2-36 `show par: set block(spacing: ..)` has no effect anymore // Hint: 2-36 this is specific to paragraphs as they are not considered blocks anymore From 81021fa1a277b04e8726d82fdf8f5fe11aeacab6 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 10 Feb 2025 16:39:14 +0100 Subject: [PATCH 5/9] Bump `typst-assets` (#5845) --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 140dccf74..bf69c12af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2753,7 +2753,7 @@ dependencies = [ [[package]] name = "typst-assets" version = "0.12.0" -source = "git+https://github.com/typst/typst-assets?rev=8cccef9#8cccef93b5da73a1c80389722cf2b655b624f577" +source = "git+https://github.com/typst/typst-assets?rev=8536748#8536748e4350198f34e519adff8593f258259cca" [[package]] name = "typst-cli" diff --git a/Cargo.toml b/Cargo.toml index 469439d38..ea9f8510e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ typst-svg = { path = "crates/typst-svg", version = "0.12.0" } 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-assets = { git = "https://github.com/typst/typst-assets", rev = "8536748" } typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "7f8999d" } arrayvec = "0.7.4" az = "1.2" From a0cd89b478437e53ece754a901ccfc035b4f2acf Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 11 Feb 2025 11:30:30 +0100 Subject: [PATCH 6/9] Fix autocomplete and jumps in math (#5849) --- crates/typst-ide/src/complete.rs | 17 +++++++++++++++-- crates/typst-ide/src/jump.rs | 17 +++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index 7df788dc3..564b97bd7 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -306,7 +306,10 @@ fn complete_math(ctx: &mut CompletionContext) -> bool { } // Behind existing atom or identifier: "$a|$" or "$abc|$". - if matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathIdent) { + if matches!( + ctx.leaf.kind(), + SyntaxKind::Text | SyntaxKind::MathText | SyntaxKind::MathIdent + ) { ctx.from = ctx.leaf.offset(); math_completions(ctx); return true; @@ -358,7 +361,7 @@ fn complete_field_accesses(ctx: &mut CompletionContext) -> bool { // Behind an expression plus dot: "emoji.|". if_chain! { if ctx.leaf.kind() == SyntaxKind::Dot - || (ctx.leaf.kind() == SyntaxKind::Text + || (matches!(ctx.leaf.kind(), SyntaxKind::Text | SyntaxKind::MathText) && ctx.leaf.text() == "."); if ctx.leaf.range().end == ctx.cursor; if let Some(prev) = ctx.leaf.prev_sibling(); @@ -1768,4 +1771,14 @@ mod tests { test("#show outline.entry: it => it.\n#outline()\n= Hi", 30) .must_include(["indented", "body", "page"]); } + + #[test] + fn test_autocomplete_symbol_variants() { + test("#sym.arrow.", -1) + .must_include(["r", "dashed"]) + .must_exclude(["cases"]); + test("$ arrow. $", -3) + .must_include(["r", "dashed"]) + .must_exclude(["cases"]); + } } diff --git a/crates/typst-ide/src/jump.rs b/crates/typst-ide/src/jump.rs index ed74df226..428335426 100644 --- a/crates/typst-ide/src/jump.rs +++ b/crates/typst-ide/src/jump.rs @@ -73,7 +73,10 @@ pub fn jump_from_click( let Some(id) = span.id() else { continue }; let source = world.source(id).ok()?; let node = source.find(span)?; - let pos = if node.kind() == SyntaxKind::Text { + let pos = if matches!( + node.kind(), + SyntaxKind::Text | SyntaxKind::MathText + ) { let range = node.range(); let mut offset = range.start + usize::from(span_offset); if (click.x - pos.x) > width / 2.0 { @@ -115,7 +118,7 @@ pub fn jump_from_cursor( cursor: usize, ) -> Vec { fn is_text(node: &LinkedNode) -> bool { - node.get().kind() == SyntaxKind::Text + matches!(node.kind(), SyntaxKind::Text | SyntaxKind::MathText) } let root = LinkedNode::new(source.root()); @@ -261,6 +264,11 @@ mod tests { test_click(s, point(21.0, 12.0), cursor(56)); } + #[test] + fn test_jump_from_click_math() { + test_click("$a + b$", point(28.0, 14.0), cursor(5)); + } + #[test] fn test_jump_from_cursor() { let s = "*Hello* #box[ABC] World"; @@ -268,6 +276,11 @@ mod tests { test_cursor(s, 14, pos(1, 37.55, 16.58)); } + #[test] + fn test_jump_from_cursor_math() { + test_cursor("$a + b$", -3, pos(1, 27.51, 16.83)); + } + #[test] fn test_backlink() { let s = "#footnote[Hi]"; From 83ad407d3ccff4a8de1e7ffe198bfed874f5c0c7 Mon Sep 17 00:00:00 2001 From: +merlan #flirora Date: Wed, 12 Feb 2025 07:35:03 -0500 Subject: [PATCH 7/9] Update documentation for `float.{to-bits, from-bits}` (#5836) --- crates/typst-library/src/foundations/float.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/typst-library/src/foundations/float.rs b/crates/typst-library/src/foundations/float.rs index fcc46b034..21d0a8d81 100644 --- a/crates/typst-library/src/foundations/float.rs +++ b/crates/typst-library/src/foundations/float.rs @@ -110,7 +110,7 @@ impl f64 { f64::signum(self) } - /// Converts bytes to a float. + /// Interprets bytes as a float. /// /// ```example /// #float.from-bytes(bytes((0, 0, 0, 0, 0, 0, 240, 63))) \ @@ -120,8 +120,10 @@ impl f64 { pub fn from_bytes( /// The bytes that should be converted to a float. /// - /// Must be of length exactly 8 so that the result fits into a 64-bit - /// float. + /// Must have a length of either 4 or 8. The bytes are then + /// interpreted in [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754)'s + /// binary32 (single-precision) or binary64 (double-precision) format + /// depending on the length of the bytes. bytes: Bytes, /// The endianness of the conversion. #[named] @@ -158,6 +160,13 @@ impl f64 { #[named] #[default(Endianness::Little)] endian: Endianness, + /// The size of the resulting bytes. + /// + /// This must be either 4 or 8. The call will return the + /// representation of this float in either + /// [IEEE 754](https://en.wikipedia.org/wiki/IEEE_754)'s binary32 + /// (single-precision) or binary64 (double-precision) format + /// depending on the provided size. #[named] #[default(8)] size: u32, From 02cd43e27f2aafd7c332d7672a837e1b11cce120 Mon Sep 17 00:00:00 2001 From: +merlan #flirora Date: Wed, 12 Feb 2025 07:38:40 -0500 Subject: [PATCH 8/9] `Gradient::repeat`: Fix floating-point error in stop calculation (#5837) --- crates/typst-library/src/visualize/gradient.rs | 7 +++---- tests/suite/visualize/gradient.typ | 8 ++++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/typst-library/src/visualize/gradient.rs b/crates/typst-library/src/visualize/gradient.rs index 431f07dd4..d6530dd09 100644 --- a/crates/typst-library/src/visualize/gradient.rs +++ b/crates/typst-library/src/visualize/gradient.rs @@ -582,12 +582,11 @@ impl Gradient { let mut stops = stops .iter() .map(move |&(color, offset)| { - let t = i as f64 / n as f64; let r = offset.get(); if i % 2 == 1 && mirror { - (color, Ratio::new(t + (1.0 - r) / n as f64)) + (color, Ratio::new((i as f64 + 1.0 - r) / n as f64)) } else { - (color, Ratio::new(t + r / n as f64)) + (color, Ratio::new((i as f64 + r) / n as f64)) } }) .collect::>(); @@ -1230,7 +1229,7 @@ fn process_stops(stops: &[Spanned]) -> SourceResult Date: Wed, 12 Feb 2025 16:50:48 +0100 Subject: [PATCH 9/9] Lazy parsing of the package index (#5851) --- Cargo.lock | 2 + crates/typst-kit/Cargo.toml | 2 + crates/typst-kit/src/package.rs | 75 ++++++++++++++++++++++++++++++--- 3 files changed, 73 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bf69c12af..66a1e3a12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2901,6 +2901,8 @@ dependencies = [ "native-tls", "once_cell", "openssl", + "serde", + "serde_json", "tar", "typst-assets", "typst-library", diff --git a/crates/typst-kit/Cargo.toml b/crates/typst-kit/Cargo.toml index 266eba0b4..52aa407c3 100644 --- a/crates/typst-kit/Cargo.toml +++ b/crates/typst-kit/Cargo.toml @@ -23,6 +23,8 @@ flate2 = { workspace = true, optional = true } fontdb = { workspace = true, optional = true } native-tls = { workspace = true, optional = true } once_cell = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } tar = { workspace = true, optional = true } ureq = { workspace = true, optional = true } diff --git a/crates/typst-kit/src/package.rs b/crates/typst-kit/src/package.rs index e7eb71ee4..172d8740a 100644 --- a/crates/typst-kit/src/package.rs +++ b/crates/typst-kit/src/package.rs @@ -5,10 +5,9 @@ use std::path::{Path, PathBuf}; use ecow::eco_format; use once_cell::sync::OnceCell; +use serde::Deserialize; use typst_library::diag::{bail, PackageError, PackageResult, StrResult}; -use typst_syntax::package::{ - PackageInfo, PackageSpec, PackageVersion, VersionlessPackageSpec, -}; +use typst_syntax::package::{PackageSpec, PackageVersion, VersionlessPackageSpec}; use crate::download::{Downloader, Progress}; @@ -32,7 +31,7 @@ pub struct PackageStorage { /// The downloader used for fetching the index and packages. downloader: Downloader, /// The cached index of the default namespace. - index: OnceCell>, + index: OnceCell>, } impl PackageStorage { @@ -42,6 +41,18 @@ impl PackageStorage { package_cache_path: Option, package_path: Option, downloader: Downloader, + ) -> Self { + Self::with_index(package_cache_path, package_path, downloader, OnceCell::new()) + } + + /// Creates a new package storage with a pre-defined index. + /// + /// Useful for testing. + fn with_index( + package_cache_path: Option, + package_path: Option, + downloader: Downloader, + index: OnceCell>, ) -> Self { Self { package_cache_path: package_cache_path.or_else(|| { @@ -51,7 +62,7 @@ impl PackageStorage { dirs::data_dir().map(|data_dir| data_dir.join(DEFAULT_PACKAGES_SUBDIR)) }), downloader, - index: OnceCell::new(), + index, } } @@ -109,6 +120,7 @@ impl PackageStorage { // version. self.download_index()? .iter() + .filter_map(|value| MinimalPackageInfo::deserialize(value).ok()) .filter(|package| package.name == spec.name) .map(|package| package.version) .max() @@ -131,7 +143,7 @@ impl PackageStorage { } /// Download the package index. The result of this is cached for efficiency. - pub fn download_index(&self) -> StrResult<&[PackageInfo]> { + pub fn download_index(&self) -> StrResult<&[serde_json::Value]> { self.index .get_or_try_init(|| { let url = format!("{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/index.json"); @@ -186,3 +198,54 @@ impl PackageStorage { }) } } + +/// Minimal information required about a package to determine its latest +/// version. +#[derive(Deserialize)] +struct MinimalPackageInfo { + name: String, + version: PackageVersion, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn lazy_deser_index() { + let storage = PackageStorage::with_index( + None, + None, + Downloader::new("typst/test"), + OnceCell::with_value(vec![ + serde_json::json!({ + "name": "charged-ieee", + "version": "0.1.0", + "entrypoint": "lib.typ", + }), + serde_json::json!({ + "name": "unequivocal-ams", + // This version number is currently not valid, so this package + // can't be parsed. + "version": "0.2.0-dev", + "entrypoint": "lib.typ", + }), + ]), + ); + + let ieee_version = storage.determine_latest_version(&VersionlessPackageSpec { + namespace: "preview".into(), + name: "charged-ieee".into(), + }); + assert_eq!(ieee_version, Ok(PackageVersion { major: 0, minor: 1, patch: 0 })); + + let ams_version = storage.determine_latest_version(&VersionlessPackageSpec { + namespace: "preview".into(), + name: "unequivocal-ams".into(), + }); + assert_eq!( + ams_version, + Err("failed to find package @preview/unequivocal-ams".into()) + ) + } +}