diff --git a/Cargo.lock b/Cargo.lock index 3ea423f5f..550c4141a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -413,7 +413,7 @@ dependencies = [ [[package]] name = "codex" version = "0.1.1" -source = "git+https://github.com/typst/codex?rev=56eb217#56eb2172fc0670f4c1c8b79a63d11f9354e5babe" +source = "git+https://github.com/typst/codex?rev=a5428cb#a5428cb9c81a41354d44b44dbd5a16a710bbd928" [[package]] name = "color-print" @@ -2911,7 +2911,7 @@ dependencies = [ [[package]] name = "typst-dev-assets" version = "0.13.1" -source = "git+https://github.com/typst/typst-dev-assets?rev=fddbf8b#fddbf8b99506bc370ac0edcd4959add603a7fc92" +source = "git+https://github.com/typst/typst-dev-assets?rev=bfa947f#bfa947f3433d7d13a995168c40ae788a2ebfe648" [[package]] name = "typst-docs" diff --git a/Cargo.toml b/Cargo.toml index 3cfb72008..6cc59ee89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" } typst-timing = { path = "crates/typst-timing", version = "0.13.1" } typst-utils = { path = "crates/typst-utils", version = "0.13.1" } typst-assets = { git = "https://github.com/typst/typst-assets", rev = "c1089b4" } -typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "fddbf8b" } +typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "bfa947f" } arrayvec = "0.7.4" az = "1.2" base64 = "0.22" @@ -47,7 +47,7 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] } clap_complete = "4.2.1" clap_mangen = "0.2.10" codespan-reporting = "0.11" -codex = { git = "https://github.com/typst/codex", rev = "56eb217" } +codex = { git = "https://github.com/typst/codex", rev = "a5428cb" } color-print = "0.3.6" comemo = "0.4" csv = "1" diff --git a/crates/typst-eval/src/markup.rs b/crates/typst-eval/src/markup.rs index 5beefa912..9118ded56 100644 --- a/crates/typst-eval/src/markup.rs +++ b/crates/typst-eval/src/markup.rs @@ -205,7 +205,9 @@ impl Eval for ast::Label<'_> { type Output = Value; fn eval(self, _: &mut Vm) -> SourceResult { - Ok(Value::Label(Label::new(PicoStr::intern(self.get())))) + Ok(Value::Label( + Label::new(PicoStr::intern(self.get())).expect("unexpected empty label"), + )) } } @@ -213,7 +215,8 @@ impl Eval for ast::Ref<'_> { type Output = Content; fn eval(self, vm: &mut Vm) -> SourceResult { - let target = Label::new(PicoStr::intern(self.target())); + let target = Label::new(PicoStr::intern(self.target())) + .expect("unexpected empty reference"); let mut elem = RefElem::new(target); if let Some(supplement) = self.supplement() { elem.push_supplement(Smart::Custom(Some(Supplement::Content( diff --git a/crates/typst-html/src/encode.rs b/crates/typst-html/src/encode.rs index 9c7938360..84860dbe9 100644 --- a/crates/typst-html/src/encode.rs +++ b/crates/typst-html/src/encode.rs @@ -3,9 +3,8 @@ use std::fmt::Write; use typst_library::diag::{bail, At, SourceResult, StrResult}; use typst_library::foundations::Repr; use typst_library::html::{ - attr, charsets, tag, HtmlDocument, HtmlElement, HtmlNode, HtmlTag, + attr, charsets, tag, HtmlDocument, HtmlElement, HtmlFrame, HtmlNode, HtmlTag, }; -use typst_library::layout::Frame; use typst_syntax::Span; /// Encodes an HTML document into a string. @@ -304,9 +303,15 @@ fn write_escape(w: &mut Writer, c: char) -> StrResult<()> { } /// Encode a laid out frame into the writer. -fn write_frame(w: &mut Writer, frame: &Frame) { +fn write_frame(w: &mut Writer, frame: &HtmlFrame) { // FIXME: This string replacement is obviously a hack. - let svg = typst_svg::svg_frame(frame) - .replace(" { for modifier in symbol.modifiers() { - if let Ok(modified) = symbol.clone().modified(modifier) { + if let Ok(modified) = symbol.clone().modified((), modifier) { ctx.completions.push(Completion { kind: CompletionKind::Symbol(modified.get()), label: modifier.into(), diff --git a/crates/typst-ide/src/definition.rs b/crates/typst-ide/src/definition.rs index 69d702b3b..ae1ba287b 100644 --- a/crates/typst-ide/src/definition.rs +++ b/crates/typst-ide/src/definition.rs @@ -72,7 +72,8 @@ pub fn definition( // Try to jump to the referenced content. DerefTarget::Ref(node) => { - let label = Label::new(PicoStr::intern(node.cast::()?.target())); + let label = Label::new(PicoStr::intern(node.cast::()?.target())) + .expect("unexpected empty reference"); let selector = Selector::Label(label); let elem = document?.introspector.query_first(&selector)?; return Some(Definition::Span(elem.span())); diff --git a/crates/typst-layout/src/inline/line.rs b/crates/typst-layout/src/inline/line.rs index 1e2aa75b7..b850e50ee 100644 --- a/crates/typst-layout/src/inline/line.rs +++ b/crates/typst-layout/src/inline/line.rs @@ -219,7 +219,7 @@ fn collect_items<'a>( // Add fallback text to expand the line height, if necessary. if !items.iter().any(|item| matches!(item, Item::Text(_))) { if let Some(fallback) = fallback { - items.push(fallback); + items.push(fallback, usize::MAX); } } @@ -270,10 +270,10 @@ fn collect_range<'a>( items: &mut Items<'a>, fallback: &mut Option>, ) { - for (subrange, item) in p.slice(range.clone()) { + for (idx, (subrange, item)) in p.slice(range.clone()).enumerate() { // All non-text items are just kept, they can't be split. let Item::Text(shaped) = item else { - items.push(item); + items.push(item, idx); continue; }; @@ -293,10 +293,10 @@ fn collect_range<'a>( } else if split { // When the item is split in half, reshape it. let reshaped = shaped.reshape(engine, sliced); - items.push(Item::Text(reshaped)); + items.push(Item::Text(reshaped), idx); } else { // When the item is fully contained, just keep it. - items.push(item); + items.push(item, idx); } } } @@ -520,16 +520,16 @@ pub fn commit( // Build the frames and determine the height and baseline. let mut frames = vec![]; - for item in line.items.iter() { - let mut push = |offset: &mut Abs, frame: Frame| { + for &(idx, ref item) in line.items.indexed_iter() { + let mut push = |offset: &mut Abs, frame: Frame, idx: usize| { let width = frame.width(); top.set_max(frame.baseline()); bottom.set_max(frame.size().y - frame.baseline()); - frames.push((*offset, frame)); + frames.push((*offset, frame, idx)); *offset += width; }; - match item { + match &**item { Item::Absolute(v, _) => { offset += *v; } @@ -541,7 +541,7 @@ pub fn commit( layout_box(elem, engine, loc.relayout(), styles, region) })?; apply_shift(&engine.world, &mut frame, *styles); - push(&mut offset, frame); + push(&mut offset, frame, idx); } else { offset += amount; } @@ -553,15 +553,15 @@ pub fn commit( justification_ratio, extra_justification, ); - push(&mut offset, frame); + push(&mut offset, frame, idx); } Item::Frame(frame) => { - push(&mut offset, frame.clone()); + push(&mut offset, frame.clone(), idx); } Item::Tag(tag) => { let mut frame = Frame::soft(Size::zero()); frame.push(Point::zero(), FrameItem::Tag((*tag).clone())); - frames.push((offset, frame)); + frames.push((offset, frame, idx)); } Item::Skip(_) => {} } @@ -580,8 +580,13 @@ pub fn commit( add_par_line_marker(&mut output, marker, engine, locator, top); } + // Ensure that the final frame's items are in logical order rather than in + // visual order. This is important because it affects the order of elements + // during introspection and thus things like counters. + frames.sort_unstable_by_key(|(_, _, idx)| *idx); + // Construct the line's frame. - for (offset, frame) in frames { + for (offset, frame, _) in frames { let x = offset + p.config.align.position(remaining); let y = top - frame.baseline(); output.push_frame(Point::new(x, y), frame); @@ -648,7 +653,7 @@ fn overhang(c: char) -> f64 { } /// A collection of owned or borrowed inline items. -pub struct Items<'a>(Vec>); +pub struct Items<'a>(Vec<(usize, ItemEntry<'a>)>); impl<'a> Items<'a> { /// Create empty items. @@ -657,33 +662,38 @@ impl<'a> Items<'a> { } /// Push a new item. - pub fn push(&mut self, entry: impl Into>) { - self.0.push(entry.into()); + pub fn push(&mut self, entry: impl Into>, idx: usize) { + self.0.push((idx, entry.into())); } - /// Iterate over the items + /// Iterate over the items. pub fn iter(&self) -> impl Iterator> { - self.0.iter().map(|item| &**item) + self.0.iter().map(|(_, item)| &**item) + } + + /// Iterate over the items with indices + pub fn indexed_iter(&self) -> impl Iterator)> { + self.0.iter() } /// Access the first item. pub fn first(&self) -> Option<&Item<'a>> { - self.0.first().map(|item| &**item) + self.0.first().map(|(_, item)| &**item) } /// Access the last item. pub fn last(&self) -> Option<&Item<'a>> { - self.0.last().map(|item| &**item) + self.0.last().map(|(_, item)| &**item) } /// Access the first item mutably, if it is text. pub fn first_text_mut(&mut self) -> Option<&mut ShapedText<'a>> { - self.0.first_mut()?.text_mut() + self.0.first_mut()?.1.text_mut() } /// Access the last item mutably, if it is text. pub fn last_text_mut(&mut self) -> Option<&mut ShapedText<'a>> { - self.0.last_mut()?.text_mut() + self.0.last_mut()?.1.text_mut() } /// Reorder the items starting at the given index to RTL. @@ -694,12 +704,12 @@ impl<'a> Items<'a> { impl<'a> FromIterator> for Items<'a> { fn from_iter>>(iter: I) -> Self { - Self(iter.into_iter().collect()) + Self(iter.into_iter().enumerate().collect()) } } impl<'a> Deref for Items<'a> { - type Target = Vec>; + type Target = Vec<(usize, ItemEntry<'a>)>; fn deref(&self) -> &Self::Target { &self.0 @@ -719,6 +729,10 @@ impl Debug for Items<'_> { } /// A reference to or a boxed item. +/// +/// This is conceptually similar to a [`Cow<'a, Item<'a>>`][std::borrow::Cow], +/// but we box owned items since an [`Item`] is much bigger than +/// a box. pub enum ItemEntry<'a> { Ref(&'a Item<'a>), Box(Box>), diff --git a/crates/typst-library/src/foundations/label.rs b/crates/typst-library/src/foundations/label.rs index 3b9b010c5..b1ac58bf2 100644 --- a/crates/typst-library/src/foundations/label.rs +++ b/crates/typst-library/src/foundations/label.rs @@ -1,7 +1,8 @@ use ecow::{eco_format, EcoString}; use typst_utils::{PicoStr, ResolvedPicoStr}; -use crate::foundations::{func, scope, ty, Repr, Str}; +use crate::diag::StrResult; +use crate::foundations::{bail, func, scope, ty, Repr, Str}; /// A label for an element. /// @@ -27,7 +28,8 @@ use crate::foundations::{func, scope, ty, Repr, Str}; /// # Syntax /// This function also has dedicated syntax: You can create a label by enclosing /// its name in angle brackets. This works both in markup and code. A label's -/// name can contain letters, numbers, `_`, `-`, `:`, and `.`. +/// name can contain letters, numbers, `_`, `-`, `:`, and `.`. A label cannot +/// be empty. /// /// Note that there is a syntactical difference when using the dedicated syntax /// for this function. In the code below, the `[]` terminates the heading and @@ -50,8 +52,11 @@ pub struct Label(PicoStr); impl Label { /// Creates a label from an interned string. - pub fn new(name: PicoStr) -> Self { - Self(name) + /// + /// Returns `None` if the given string is empty. + pub fn new(name: PicoStr) -> Option { + const EMPTY: PicoStr = PicoStr::constant(""); + (name != EMPTY).then_some(Self(name)) } /// Resolves the label to a string. @@ -70,10 +75,14 @@ impl Label { /// Creates a label from a string. #[func(constructor)] pub fn construct( - /// The name of the label. + /// The name of the label. Must not be empty. name: Str, - ) -> Label { - Self(PicoStr::intern(name.as_str())) + ) -> StrResult = (ModifierSet, char, Option); + /// A collection of symbols. #[derive(Clone, Eq, PartialEq, Hash)] enum List { - Static(&'static [(ModifierSet<&'static str>, char)]), - Runtime(Box<[(ModifierSet, char)]>), + Static(&'static [Variant<&'static str>]), + Runtime(Box<[Variant]>), } impl Symbol { @@ -76,14 +80,14 @@ impl Symbol { /// Create a symbol with a static variant list. #[track_caller] - pub const fn list(list: &'static [(ModifierSet<&'static str>, char)]) -> Self { + pub const fn list(list: &'static [Variant<&'static str>]) -> Self { debug_assert!(!list.is_empty()); Self(Repr::Complex(list)) } /// Create a symbol with a runtime variant list. #[track_caller] - pub fn runtime(list: Box<[(ModifierSet, char)]>) -> Self { + pub fn runtime(list: Box<[Variant]>) -> Self { debug_assert!(!list.is_empty()); Self(Repr::Modified(Arc::new((List::Runtime(list), ModifierSet::default())))) } @@ -93,9 +97,11 @@ impl Symbol { match &self.0 { Repr::Single(c) => *c, Repr::Complex(_) => ModifierSet::<&'static str>::default() - .best_match_in(self.variants()) + .best_match_in(self.variants().map(|(m, c, _)| (m, c))) .unwrap(), - Repr::Modified(arc) => arc.1.best_match_in(self.variants()).unwrap(), + Repr::Modified(arc) => { + arc.1.best_match_in(self.variants().map(|(m, c, _)| (m, c))).unwrap() + } } } @@ -128,7 +134,11 @@ impl Symbol { } /// Apply a modifier to the symbol. - pub fn modified(mut self, modifier: &str) -> StrResult { + pub fn modified( + mut self, + sink: impl DeprecationSink, + modifier: &str, + ) -> StrResult { if let Repr::Complex(list) = self.0 { self.0 = Repr::Modified(Arc::new((List::Static(list), ModifierSet::default()))); @@ -137,7 +147,12 @@ impl Symbol { if let Repr::Modified(arc) = &mut self.0 { let (list, modifiers) = Arc::make_mut(arc); modifiers.insert_raw(modifier); - if modifiers.best_match_in(list.variants()).is_some() { + if let Some(deprecation) = + modifiers.best_match_in(list.variants().map(|(m, _, d)| (m, d))) + { + if let Some(message) = deprecation { + sink.emit(message) + } return Ok(self); } } @@ -146,7 +161,7 @@ impl Symbol { } /// The characters that are covered by this symbol. - pub fn variants(&self) -> impl Iterator, char)> { + pub fn variants(&self) -> impl Iterator> { match &self.0 { Repr::Single(c) => Variants::Single(Some(*c).into_iter()), Repr::Complex(list) => Variants::Static(list.iter()), @@ -161,7 +176,7 @@ impl Symbol { _ => ModifierSet::default(), }; self.variants() - .flat_map(|(m, _)| m) + .flat_map(|(m, _, _)| m) .filter(|modifier| !modifier.is_empty() && !modifiers.contains(modifier)) .collect::>() .into_iter() @@ -256,7 +271,7 @@ impl Symbol { let list = variants .into_iter() - .map(|s| (ModifierSet::from_raw_dotted(s.v.0), s.v.1)) + .map(|s| (ModifierSet::from_raw_dotted(s.v.0), s.v.1, None)) .collect(); Ok(Symbol::runtime(list)) } @@ -316,17 +331,17 @@ impl crate::foundations::Repr for Symbol { } fn repr_variants<'a>( - variants: impl Iterator, char)>, + variants: impl Iterator>, applied_modifiers: ModifierSet<&str>, ) -> String { crate::foundations::repr::pretty_array_like( &variants - .filter(|(modifiers, _)| { + .filter(|(modifiers, _, _)| { // Only keep variants that can still be accessed, i.e., variants // that contain all applied modifiers. applied_modifiers.iter().all(|am| modifiers.contains(am)) }) - .map(|(modifiers, c)| { + .map(|(modifiers, c, _)| { let trimmed_modifiers = modifiers.into_iter().filter(|&m| !applied_modifiers.contains(m)); if trimmed_modifiers.clone().all(|m| m.is_empty()) { @@ -379,18 +394,20 @@ cast! { /// Iterator over variants. enum Variants<'a> { Single(std::option::IntoIter), - Static(std::slice::Iter<'static, (ModifierSet<&'static str>, char)>), - Runtime(std::slice::Iter<'a, (ModifierSet, char)>), + Static(std::slice::Iter<'static, Variant<&'static str>>), + Runtime(std::slice::Iter<'a, Variant>), } impl<'a> Iterator for Variants<'a> { - type Item = (ModifierSet<&'a str>, char); + type Item = Variant<&'a str>; fn next(&mut self) -> Option { match self { - Self::Single(iter) => Some((ModifierSet::default(), iter.next()?)), + Self::Single(iter) => Some((ModifierSet::default(), iter.next()?, None)), Self::Static(list) => list.next().copied(), - Self::Runtime(list) => list.next().map(|(m, c)| (m.as_deref(), *c)), + Self::Runtime(list) => { + list.next().map(|(m, c, d)| (m.as_deref(), *c, d.as_deref())) + } } } } diff --git a/crates/typst-library/src/foundations/value.rs b/crates/typst-library/src/foundations/value.rs index 854c2486e..4bcf2d4e3 100644 --- a/crates/typst-library/src/foundations/value.rs +++ b/crates/typst-library/src/foundations/value.rs @@ -157,7 +157,9 @@ impl Value { /// Try to access a field on the value. pub fn field(&self, field: &str, sink: impl DeprecationSink) -> StrResult { match self { - Self::Symbol(symbol) => symbol.clone().modified(field).map(Self::Symbol), + Self::Symbol(symbol) => { + symbol.clone().modified(sink, field).map(Self::Symbol) + } Self::Version(version) => version.component(field).map(Self::Int), Self::Dict(dict) => dict.get(field).cloned(), Self::Content(content) => content.field_by_name(field), diff --git a/crates/typst-library/src/html/dom.rs b/crates/typst-library/src/html/dom.rs index 35d513c10..47bcf9954 100644 --- a/crates/typst-library/src/html/dom.rs +++ b/crates/typst-library/src/html/dom.rs @@ -7,7 +7,7 @@ use typst_utils::{PicoStr, ResolvedPicoStr}; use crate::diag::{bail, HintedStrResult, StrResult}; use crate::foundations::{cast, Dict, Repr, Str}; use crate::introspection::{Introspector, Tag}; -use crate::layout::Frame; +use crate::layout::{Abs, Frame}; use crate::model::DocumentInfo; /// An HTML document. @@ -30,8 +30,8 @@ pub enum HtmlNode { Text(EcoString, Span), /// Another element. Element(HtmlElement), - /// A frame that will be displayed as an embedded SVG. - Frame(Frame), + /// Layouted content that will be embedded into HTML as an SVG. + Frame(HtmlFrame), } impl HtmlNode { @@ -263,6 +263,17 @@ cast! { v: Str => Self::intern(&v)?, } +/// Layouted content that will be embedded into HTML as an SVG. +#[derive(Debug, Clone, Hash)] +pub struct HtmlFrame { + /// The frame that will be displayed as an SVG. + pub inner: Frame, + /// The text size where the frame was defined. This is used to size the + /// frame with em units to make text in and outside of the frame sized + /// consistently. + pub text_size: Abs, +} + /// Defines syntactical properties of HTML tags, attributes, and text. pub mod charsets { /// Check whether a character is in a tag name. diff --git a/crates/typst-library/src/introspection/introspector.rs b/crates/typst-library/src/introspection/introspector.rs index 9751dfcb8..d2ad0525b 100644 --- a/crates/typst-library/src/introspection/introspector.rs +++ b/crates/typst-library/src/introspection/introspector.rs @@ -446,7 +446,7 @@ impl IntrospectorBuilder { HtmlNode::Element(elem) => self.discover_in_html(sink, &elem.children), HtmlNode::Frame(frame) => self.discover_in_frame( sink, - frame, + &frame.inner, NonZeroUsize::ONE, Transform::identity(), ), diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index 8056d4ab3..f56f5813e 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -321,7 +321,11 @@ impl Bibliography { for d in data.iter() { let library = decode_library(d)?; for entry in library { - match map.entry(Label::new(PicoStr::intern(entry.key()))) { + let label = Label::new(PicoStr::intern(entry.key())) + .ok_or("bibliography contains entry with empty key") + .at(d.source.span)?; + + match map.entry(label) { indexmap::map::Entry::Vacant(vacant) => { vacant.insert(entry); } @@ -592,7 +596,7 @@ impl Works { /// Context for generating the bibliography. struct Generator<'a> { - /// The routines that is used to evaluate mathematical material in citations. + /// The routines that are used to evaluate mathematical material in citations. routines: &'a Routines, /// The world that is used to evaluate mathematical material in citations. world: Tracked<'a, dyn World + 'a>, @@ -609,7 +613,7 @@ struct Generator<'a> { /// Details about a group of merged citations. All citations are put into groups /// of adjacent ones (e.g., `@foo @bar` will merge into a group of length two). -/// Even single citations will be put into groups of length ones. +/// Even single citations will be put into groups of length one. struct GroupInfo { /// The group's location. location: Location, diff --git a/crates/typst-library/src/text/deco.rs b/crates/typst-library/src/text/deco.rs index 7aa06e815..d745a48fd 100644 --- a/crates/typst-library/src/text/deco.rs +++ b/crates/typst-library/src/text/deco.rs @@ -2,7 +2,10 @@ use smallvec::smallvec; use crate::diag::SourceResult; use crate::engine::Engine; -use crate::foundations::{elem, Content, Packed, Show, Smart, StyleChain}; +use crate::foundations::{ + elem, Content, NativeElement, Packed, Show, Smart, StyleChain, TargetElem, +}; +use crate::html::{attr, tag, HtmlElem}; use crate::layout::{Abs, Corners, Length, Rel, Sides}; use crate::text::{BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric}; use crate::visualize::{Color, FixedStroke, Paint, Stroke}; @@ -81,6 +84,16 @@ pub struct UnderlineElem { impl Show for Packed { #[typst_macros::time(name = "underline", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { + if TargetElem::target_in(styles).is_html() { + // Note: In modern HTML, `` is not the underline element, but + // rather an "Unarticulated Annotation" element (see HTML spec + // 4.5.22). Using `text-decoration` instead is recommended by MDN. + return Ok(HtmlElem::new(tag::span) + .with_attr(attr::style, "text-decoration: underline") + .with_body(Some(self.body.clone())) + .pack()); + } + Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { line: DecoLine::Underline { stroke: self.stroke(styles).unwrap_or_default(), @@ -173,6 +186,13 @@ pub struct OverlineElem { impl Show for Packed { #[typst_macros::time(name = "overline", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { + if TargetElem::target_in(styles).is_html() { + return Ok(HtmlElem::new(tag::span) + .with_attr(attr::style, "text-decoration: overline") + .with_body(Some(self.body.clone())) + .pack()); + } + Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { line: DecoLine::Overline { stroke: self.stroke(styles).unwrap_or_default(), @@ -250,6 +270,10 @@ pub struct StrikeElem { impl Show for Packed { #[typst_macros::time(name = "strike", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { + if TargetElem::target_in(styles).is_html() { + return Ok(HtmlElem::new(tag::s).with_body(Some(self.body.clone())).pack()); + } + Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { // Note that we do not support evade option for strikethrough. line: DecoLine::Strikethrough { @@ -345,6 +369,12 @@ pub struct HighlightElem { impl Show for Packed { #[typst_macros::time(name = "highlight", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { + if TargetElem::target_in(styles).is_html() { + return Ok(HtmlElem::new(tag::mark) + .with_body(Some(self.body.clone())) + .pack()); + } + Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { line: DecoLine::Highlight { fill: self.fill(styles), diff --git a/crates/typst-library/src/visualize/gradient.rs b/crates/typst-library/src/visualize/gradient.rs index 5d7859a37..4917da68d 100644 --- a/crates/typst-library/src/visualize/gradient.rs +++ b/crates/typst-library/src/visualize/gradient.rs @@ -1285,24 +1285,17 @@ fn process_stops(stops: &[Spanned]) -> SourceResult Color { let t = t.clamp(0.0, 1.0); - let mut low = 0; - let mut high = stops.len(); + let mut j = stops.partition_point(|(_, ratio)| ratio.get() < t); - while low < high { - let mid = (low + high) / 2; - if stops[mid].1.get() < t { - low = mid + 1; - } else { - high = mid; + if j == 0 { + while stops.get(j + 1).is_some_and(|(_, r)| r.is_zero()) { + j += 1; } + return stops[j].0; } - if low == 0 { - low = 1; - } - - let (col_0, pos_0) = stops[low - 1]; - let (col_1, pos_1) = stops[low]; + let (col_0, pos_0) = stops[j - 1]; + let (col_1, pos_1) = stops[j]; let t = (t - pos_0.get()) / (pos_1.get() - pos_0.get()); Color::mix_iter( diff --git a/crates/typst-realize/src/lib.rs b/crates/typst-realize/src/lib.rs index 7d2460a89..526f4631a 100644 --- a/crates/typst-realize/src/lib.rs +++ b/crates/typst-realize/src/lib.rs @@ -18,7 +18,7 @@ use typst_library::foundations::{ SequenceElem, Show, ShowSet, Style, StyleChain, StyledElem, Styles, SymbolElem, Synthesize, Transformation, }; -use typst_library::html::{tag, HtmlElem}; +use typst_library::html::{tag, FrameElem, HtmlElem}; use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem}; use typst_library::layout::{ AlignElem, BoxElem, HElem, InlineElem, PageElem, PagebreakElem, VElem, @@ -237,9 +237,9 @@ fn visit<'a>( return Ok(()); } - // Transformations for math content based on the realization kind. Needs + // Transformations for content based on the realization kind. Needs // to happen before show rules. - if visit_math_rules(s, content, styles)? { + if visit_kind_rules(s, content, styles)? { return Ok(()); } @@ -280,9 +280,8 @@ fn visit<'a>( Ok(()) } -// Handles special cases for math in normal content and nested equations in -// math. -fn visit_math_rules<'a>( +// Handles transformations based on the realization kind. +fn visit_kind_rules<'a>( s: &mut State<'a, '_, '_, '_>, content: &'a Content, styles: StyleChain<'a>, @@ -335,6 +334,13 @@ fn visit_math_rules<'a>( } } + if !s.kind.is_html() { + if let Some(elem) = content.to_packed::() { + visit(s, &elem.body, styles)?; + return Ok(true); + } + } + Ok(false) } diff --git a/crates/typst-syntax/src/ast.rs b/crates/typst-syntax/src/ast.rs index 7b211bfc1..547d53cd8 100644 --- a/crates/typst-syntax/src/ast.rs +++ b/crates/typst-syntax/src/ast.rs @@ -724,6 +724,8 @@ node! { impl<'a> Ref<'a> { /// Get the target. + /// + /// Will not be empty. pub fn target(self) -> &'a str { self.0 .children() diff --git a/crates/typst-syntax/src/lexer.rs b/crates/typst-syntax/src/lexer.rs index 74f14cfeb..82f65cd36 100644 --- a/crates/typst-syntax/src/lexer.rs +++ b/crates/typst-syntax/src/lexer.rs @@ -185,7 +185,7 @@ impl Lexer<'_> { 'h' if self.s.eat_if("ttp://") => self.link(), 'h' if self.s.eat_if("ttps://") => self.link(), '<' if self.s.at(is_id_continue) => self.label(), - '@' => self.ref_marker(), + '@' if self.s.at(is_id_continue) => self.ref_marker(), '.' if self.s.eat_if("..") => SyntaxKind::Shorthand, '-' if self.s.eat_if("--") => SyntaxKind::Shorthand, diff --git a/docs/guides/guide-for-latex-users.md b/docs/guides/guide-for-latex-users.md index fffa6c521..93fa296fc 100644 --- a/docs/guides/guide-for-latex-users.md +++ b/docs/guides/guide-for-latex-users.md @@ -256,8 +256,8 @@ In Typst, the same function can be used both to affect the appearance for the remainder of the document, a block (or scope), or just its arguments. For example, `[#text(weight: "bold")[bold text]]` will only embolden its argument, while `[#set text(weight: "bold")]` will embolden any text until the end of the -current block, or, if there is none, document. The effects of a function are -immediately obvious based on whether it is used in a call or a +current block, or the end of the document, if there is none. The effects of a +function are immediately obvious based on whether it is used in a call or a [set rule.]($styling/#set-rules) ```example diff --git a/docs/guides/page-setup.md b/docs/guides/page-setup.md index 36ed0fa23..1682c1220 100644 --- a/docs/guides/page-setup.md +++ b/docs/guides/page-setup.md @@ -206,7 +206,6 @@ label exists on the current page: ```typ >>> #set page("a5", margin: (x: 2.5cm, y: 3cm)) #set page(header: context { - let page-counter = let matches = query() let current = counter(page).get() let has-table = matches.any(m => @@ -218,7 +217,7 @@ label exists on the current page: #h(1fr) National Academy of Sciences ] -})) +}) #lorem(100) #pagebreak() diff --git a/docs/reference/groups.yml b/docs/reference/groups.yml index c7e3d9964..e01d99dc4 100644 --- a/docs/reference/groups.yml +++ b/docs/reference/groups.yml @@ -181,11 +181,7 @@ [`sys.version`]($category/foundations/sys) can also be very useful. ```typ - #let tiling = if "tiling" in dictionary(std) { - tiling - } else { - pattern - } + #let tiling = if "tiling" in std { tiling } else { pattern } ... ``` diff --git a/docs/src/lib.rs b/docs/src/lib.rs index 9bd21c2e8..dc6b62c72 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -720,18 +720,12 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel { } }; - for (variant, c) in symbol.variants() { + for (variant, c, deprecation) in symbol.variants() { let shorthand = |list: &[(&'static str, char)]| { list.iter().copied().find(|&(_, x)| x == c).map(|(s, _)| s) }; let name = complete(variant); - let deprecation = match name.as_str() { - "integral.sect" => { - Some("`integral.sect` is deprecated, use `integral.inter` instead") - } - _ => binding.deprecation(), - }; list.push(SymbolModel { name, @@ -742,10 +736,10 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel { accent: typst::math::Accent::combine(c).is_some(), alternates: symbol .variants() - .filter(|(other, _)| other != &variant) - .map(|(other, _)| complete(other)) + .filter(|(other, _, _)| other != &variant) + .map(|(other, _, _)| complete(other)) .collect(), - deprecation, + deprecation: deprecation.or_else(|| binding.deprecation()), }); } } diff --git a/tests/ref/html-frame-in-layout.png b/tests/ref/html-frame-in-layout.png new file mode 100644 index 000000000..c3382e432 Binary files /dev/null and b/tests/ref/html-frame-in-layout.png differ diff --git a/tests/ref/html/html-deco.html b/tests/ref/html/html-deco.html new file mode 100644 index 000000000..87f2ab4c8 --- /dev/null +++ b/tests/ref/html/html-deco.html @@ -0,0 +1,11 @@ + + + + + + + +

Struck Highlighted Underlined Overlined

+

Mixed

+ + diff --git a/tests/ref/issue-5775-cite-order-rtl.png b/tests/ref/issue-5775-cite-order-rtl.png new file mode 100644 index 000000000..982ceef39 Binary files /dev/null and b/tests/ref/issue-5775-cite-order-rtl.png differ diff --git a/tests/ref/issue-6162-coincident-gradient-stops-export-png.png b/tests/ref/issue-6162-coincident-gradient-stops-export-png.png new file mode 100644 index 000000000..d269342c7 Binary files /dev/null and b/tests/ref/issue-6162-coincident-gradient-stops-export-png.png differ diff --git a/tests/ref/ref-to-empty-label-not-possible.png b/tests/ref/ref-to-empty-label-not-possible.png new file mode 100644 index 000000000..774b79589 Binary files /dev/null and b/tests/ref/ref-to-empty-label-not-possible.png differ diff --git a/tests/suite/foundations/label.typ b/tests/suite/foundations/label.typ index 3b84c2d70..6eb2a9fdd 100644 --- a/tests/suite/foundations/label.typ +++ b/tests/suite/foundations/label.typ @@ -92,3 +92,7 @@ _Visible_ --- label-non-existent-error --- // Error: 5-10 sequence does not have field "label" #[].label + +--- label-empty --- +// Error: 23-32 label name must not be empty += Something to label #label("") diff --git a/tests/suite/html/frame.typ b/tests/suite/html/frame.typ new file mode 100644 index 000000000..711933d76 --- /dev/null +++ b/tests/suite/html/frame.typ @@ -0,0 +1,8 @@ +// No proper HTML tests here yet because we don't want to test SVG export just +// yet. We'll definitely add tests at some point. + +--- html-frame-in-layout --- +// Ensure that HTML frames are transparent in layout. This is less important for +// actual paged export than for _nested_ HTML frames, which take the same code +// path. +#html.frame[A] diff --git a/tests/suite/layout/align.typ b/tests/suite/layout/align.typ index c4ed9ab95..1c1a08683 100644 --- a/tests/suite/layout/align.typ +++ b/tests/suite/layout/align.typ @@ -34,7 +34,7 @@ To the right! Where the sunlight peeks behind the mountain. #align(start)[Start] #align(end)[Ende] -#set text(lang: "ar") +#set text(lang: "ar", font: "Noto Sans Arabic") #align(start)[يبدأ] #align(end)[نهاية] diff --git a/tests/suite/layout/inline/bidi.typ b/tests/suite/layout/inline/bidi.typ index 5f8712d56..d7601fa1d 100644 --- a/tests/suite/layout/inline/bidi.typ +++ b/tests/suite/layout/inline/bidi.typ @@ -45,6 +45,7 @@ Lריווח #h(1cm) R --- bidi-whitespace-reset --- // Test whether L1 whitespace resetting destroys stuff. +#set text(font: ("Libertinus Serif", "Noto Sans Arabic")) الغالب #h(70pt) ن#" "ة --- bidi-explicit-dir --- @@ -87,7 +88,7 @@ Lריווח #h(1cm) R columns: (1fr, 1fr), lines(6), [ - #text(lang: "ar")[مجرد نص مؤقت لأغراض العرض التوضيحي. ] + #text(lang: "ar", font: ("Libertinus Serif", "Noto Sans Arabic"))[مجرد نص مؤقت لأغراض العرض التوضيحي. ] #text(lang: "ar")[سلام] ], ) diff --git a/tests/suite/layout/inline/shaping.typ b/tests/suite/layout/inline/shaping.typ index dc73100b5..4dfc6eb11 100644 --- a/tests/suite/layout/inline/shaping.typ +++ b/tests/suite/layout/inline/shaping.typ @@ -29,6 +29,7 @@ ABCअपार्टमेंट \ ט --- shaping-font-fallback --- +#set text(font: ("Libertinus Serif", "Noto Sans Arabic")) // Font fallback for emoji. A😀B diff --git a/tests/suite/layout/inline/text.typ b/tests/suite/layout/inline/text.typ index 369aba7fd..a211ffd30 100644 --- a/tests/suite/layout/inline/text.typ +++ b/tests/suite/layout/inline/text.typ @@ -80,7 +80,7 @@ I'm in#text(tracking: 0.15em + 1.5pt)[ spaace]! --- text-tracking-arabic --- // Test tracking in arabic text (makes no sense whatsoever) -#set text(tracking: 0.3em) +#set text(tracking: 0.3em, font: "Noto Sans Arabic") النص --- text-spacing --- diff --git a/tests/suite/layout/repeat.typ b/tests/suite/layout/repeat.typ index a46bf6d28..8ba5d2661 100644 --- a/tests/suite/layout/repeat.typ +++ b/tests/suite/layout/repeat.typ @@ -17,7 +17,7 @@ --- repeat-dots-rtl --- // Test dots with RTL. -#set text(lang: "ar") +#set text(lang: "ar", font: ("Libertinus Serif", "Noto Sans Arabic")) مقدمة #box(width: 1fr, repeat[.]) 15 --- repeat-empty --- @@ -35,7 +35,7 @@ A#box(width: 1fr, repeat(rect(width: 6em, height: 0.7em)))B #set align(center) A#box(width: 1fr, repeat(rect(width: 6em, height: 0.7em)))B -#set text(dir: rtl) +#set text(dir: rtl, font: "Noto Sans Arabic") ريجين#box(width: 1fr, repeat(rect(width: 4em, height: 0.7em)))سون --- repeat-unrestricted --- diff --git a/tests/suite/math/attach.typ b/tests/suite/math/attach.typ index cedc3a4ab..979018478 100644 --- a/tests/suite/math/attach.typ +++ b/tests/suite/math/attach.typ @@ -121,8 +121,8 @@ $a scripts(=)^"def" b quad a scripts(lt.eq)_"really" b quad a scripts(arrow.r.lo --- math-attach-integral --- // Test default of scripts attachments on integrals at display size. -$ integral.sect_a^b quad \u{2a1b}_a^b quad limits(\u{2a1b})_a^b $ -$integral.sect_a^b quad \u{2a1b}_a^b quad limits(\u{2a1b})_a^b$ +$ integral.inter_a^b quad \u{2a1b}_a^b quad limits(\u{2a1b})_a^b $ +$integral.inter_a^b quad \u{2a1b}_a^b quad limits(\u{2a1b})_a^b$ --- math-attach-large-operator --- // Test default of limit attachments on large operators at display size only. @@ -179,7 +179,7 @@ $ a0 + a1 + a0_2 \ #{ let var = $x^1$ for i in range(24) { - var = $var$ + var = $var$ } $var_2$ } diff --git a/tests/suite/model/bibliography.typ b/tests/suite/model/bibliography.typ index 23576c156..6a0c3e3c5 100644 --- a/tests/suite/model/bibliography.typ +++ b/tests/suite/model/bibliography.typ @@ -75,6 +75,14 @@ Now we have multiple bibliographies containing @glacier-melt @keshav2007read // Error: 2-62 CSL style "Alphanumeric" is not suitable for bibliographies #bibliography("/assets/bib/works.bib", style: "alphanumeric") +--- bibliography-empty-key --- +#let src = ```yaml +"": + type: Book +``` +// Error: 15-30 bibliography contains entry with empty key +#bibliography(bytes(src.text)) + --- 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/cite.typ b/tests/suite/model/cite.typ index b328dda49..363b58489 100644 --- a/tests/suite/model/cite.typ +++ b/tests/suite/model/cite.typ @@ -147,3 +147,16 @@ B #cite() #cite(). // Error: 7-17 expected label, found string // Hint: 7-17 use `label("%@&#*!\\")` to create a label #cite("%@&#*!\\") + +--- issue-5775-cite-order-rtl --- +// Test citation order in RTL text. +#set page(width: 300pt) +#set text(font: ("Libertinus Serif", "Noto Sans Arabic")) +@netwok +aaa +این است +@tolkien54 +و این یکی هست +@arrgh + +#bibliography("/assets/bib/works.bib") diff --git a/tests/suite/model/par.typ b/tests/suite/model/par.typ index d2c3416e0..fae0e1f56 100644 --- a/tests/suite/model/par.typ +++ b/tests/suite/model/par.typ @@ -231,7 +231,7 @@ Welcome \ here. Does this work well? --- par-hanging-indent-rtl --- #set par(hanging-indent: 2em) -#set text(dir: rtl) +#set text(dir: rtl, font: ("Libertinus Serif", "Noto Sans Arabic")) لآن وقد أظلم الليل وبدأت النجوم تنضخ وجه الطبيعة التي أعْيَتْ من طول ما انبعثت في النهار diff --git a/tests/suite/model/quote.typ b/tests/suite/model/quote.typ index 4137262a7..1c5954427 100644 --- a/tests/suite/model/quote.typ +++ b/tests/suite/model/quote.typ @@ -2,6 +2,7 @@ --- quote-dir-author-pos --- // Text direction affects author positioning +#set text(font: ("Libertinus Serif", "Noto Sans Arabic")) And I quote: #quote(attribution: [René Descartes])[cogito, ergo sum]. #set text(lang: "ar") @@ -9,6 +10,7 @@ And I quote: #quote(attribution: [René Descartes])[cogito, ergo sum]. --- quote-dir-align --- // Text direction affects block alignment +#set text(font: ("Libertinus Serif", "Noto Sans Arabic")) #set quote(block: true) #quote(attribution: [René Descartes])[cogito, ergo sum] diff --git a/tests/suite/model/ref.typ b/tests/suite/model/ref.typ index 87b1c409a..d48072edb 100644 --- a/tests/suite/model/ref.typ +++ b/tests/suite/model/ref.typ @@ -86,3 +86,14 @@ Text seen on #ref(, form: "page", supplement: "Page"). // Test reference with non-whitespace before it. #figure[] <1> #test([(#ref(<1>))], [(@1)]) + +--- ref-to-empty-label-not-possible --- +// @ without any following label should just produce the symbol in the output +// and not produce a reference to a label with an empty name. +@ + +--- ref-function-empty-label --- +// using ref() should also not be possible +// Error: 6-7 unexpected less-than operator +// Error: 7-8 unexpected greater-than operator +#ref(<>) diff --git a/tests/suite/scripting/ops.typ b/tests/suite/scripting/ops.typ index d17c0117f..561682f05 100644 --- a/tests/suite/scripting/ops.typ +++ b/tests/suite/scripting/ops.typ @@ -264,6 +264,8 @@ #test("Hey" not in "abheyCd", true) #test("a" not /* fun comment? */ in "abc", false) +#test("sys" in std, true) +#test("system" in std, false) --- ops-not-trailing --- // Error: 10 expected keyword `in` diff --git a/tests/suite/syntax/numbers.typ b/tests/suite/syntax/numbers.typ index 1f15ac720..d7e6da4d1 100644 --- a/tests/suite/syntax/numbers.typ +++ b/tests/suite/syntax/numbers.typ @@ -2,6 +2,7 @@ --- numbers --- // Test numbers in text mode. +#set text(font: ("Libertinus Serif", "Noto Sans Arabic")) 12 \ 12.0 \ 3.14 \ diff --git a/tests/suite/text/deco.typ b/tests/suite/text/deco.typ index 07fdb6c19..a1d287d9d 100644 --- a/tests/suite/text/deco.typ +++ b/tests/suite/text/deco.typ @@ -83,3 +83,11 @@ We can also specify a customized value #highlight(stroke: 2pt + blue)[abc] #highlight(stroke: (top: blue, left: red, bottom: green, right: orange))[abc] #highlight(stroke: 1pt, radius: 3pt)[#lorem(5)] + +--- html-deco html --- +#strike[Struck] +#highlight[Highlighted] +#underline[Underlined] +#overline[Overlined] + +#(strike, highlight, underline, overline).fold([Mixed], (it, f) => f(it)) diff --git a/tests/suite/visualize/gradient.typ b/tests/suite/visualize/gradient.typ index 811b8b605..8446ca030 100644 --- a/tests/suite/visualize/gradient.typ +++ b/tests/suite/visualize/gradient.typ @@ -666,3 +666,29 @@ $ A = mat( #let _ = gradient.linear(..my-gradient.stops()) #let my-gradient2 = gradient.linear(red, blue).repeat(5, mirror: true) #let _ = gradient.linear(..my-gradient2.stops()) + +--- issue-6162-coincident-gradient-stops-export-png --- +// Ensure that multiple gradient stops with the same position +// don't cause a panic. +#rect( + fill: gradient.linear( + (red, 0%), + (green, 0%), + (blue, 100%), + ) +) +#rect( + fill: gradient.linear( + (red, 0%), + (green, 100%), + (blue, 100%), + ) +) +#rect( + fill: gradient.linear( + (white, 0%), + (red, 50%), + (green, 50%), + (blue, 100%), + ) +)