Merge branch 'main' into bibliography-entry

This commit is contained in:
Kevin K. 2025-02-25 14:04:37 +01:00 committed by GitHub
commit 76004a39d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 265 additions and 118 deletions

View File

@ -30,7 +30,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.83.0 - uses: dtolnay/rust-toolchain@1.85.0
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- run: cargo test --workspace --no-run - run: cargo test --workspace --no-run
- run: cargo test --workspace --no-fail-fast - run: cargo test --workspace --no-fail-fast
@ -59,7 +59,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.83.0 - uses: dtolnay/rust-toolchain@1.85.0
with: with:
components: clippy, rustfmt components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
@ -73,7 +73,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.80.0 - uses: dtolnay/rust-toolchain@1.83.0
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
- run: cargo check --workspace - run: cargo check --workspace

View File

@ -44,7 +44,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.83.0 - uses: dtolnay/rust-toolchain@1.85.0
with: with:
target: ${{ matrix.target }} target: ${{ matrix.target }}

View File

@ -5,7 +5,7 @@ resolver = "2"
[workspace.package] [workspace.package]
version = "0.13.0" version = "0.13.0"
rust-version = "1.80" # also change in ci.yml rust-version = "1.83" # also change in ci.yml
authors = ["The Typst Project Developers"] authors = ["The Typst Project Developers"]
edition = "2021" edition = "2021"
homepage = "https://typst.app" homepage = "https://typst.app"

View File

@ -350,7 +350,7 @@ fn export_image(
.iter() .iter()
.enumerate() .enumerate()
.filter(|(i, _)| { .filter(|(i, _)| {
config.pages.as_ref().map_or(true, |exported_page_ranges| { config.pages.as_ref().is_none_or(|exported_page_ranges| {
exported_page_ranges.includes_page_index(*i) exported_page_ranges.includes_page_index(*i)
}) })
}) })

View File

@ -55,11 +55,11 @@ pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> {
// Perform initial compilation. // Perform initial compilation.
timer.record(&mut world, |world| compile_once(world, &mut config))??; timer.record(&mut world, |world| compile_once(world, &mut config))??;
// Watch all dependencies of the initial compilation.
watcher.update(world.dependencies())?;
// Recompile whenever something relevant happens. // Recompile whenever something relevant happens.
loop { loop {
// Watch all dependencies of the most recent compilation.
watcher.update(world.dependencies())?;
// Wait until anything relevant happens. // Wait until anything relevant happens.
watcher.wait()?; watcher.wait()?;
@ -71,9 +71,6 @@ pub fn watch(timer: &mut Timer, command: &WatchCommand) -> StrResult<()> {
// Evict the cache. // Evict the cache.
comemo::evict(10); comemo::evict(10);
// Adjust the file watching.
watcher.update(world.dependencies())?;
} }
} }
@ -204,6 +201,10 @@ impl Watcher {
let event = event let event = event
.map_err(|err| eco_format!("failed to watch dependencies ({err})"))?; .map_err(|err| eco_format!("failed to watch dependencies ({err})"))?;
if !is_relevant_event_kind(&event.kind) {
continue;
}
// Workaround for notify-rs' implicit unwatch on remove/rename // Workaround for notify-rs' implicit unwatch on remove/rename
// (triggered by some editors when saving files) with the // (triggered by some editors when saving files) with the
// inotify backend. By keeping track of the potentially // inotify backend. By keeping track of the potentially
@ -224,7 +225,17 @@ impl Watcher {
} }
} }
relevant |= self.is_event_relevant(&event); // Don't recompile because the output file changed.
// FIXME: This doesn't work properly for multifile image export.
if event
.paths
.iter()
.all(|path| is_same_file(path, &self.output).unwrap_or(false))
{
continue;
}
relevant = true;
} }
// If we found a relevant event or if any of the missing files now // If we found a relevant event or if any of the missing files now
@ -234,19 +245,11 @@ impl Watcher {
} }
} }
} }
}
/// Whether a watch event is relevant for compilation. /// Whether a kind of watch event is relevant for compilation.
fn is_event_relevant(&self, event: &notify::Event) -> bool { fn is_relevant_event_kind(kind: &notify::EventKind) -> bool {
// Never recompile because the output file changed. match kind {
if event
.paths
.iter()
.all(|path| is_same_file(path, &self.output).unwrap_or(false))
{
return false;
}
match &event.kind {
notify::EventKind::Any => true, notify::EventKind::Any => true,
notify::EventKind::Access(_) => false, notify::EventKind::Access(_) => false,
notify::EventKind::Create(_) => true, notify::EventKind::Create(_) => true,
@ -260,7 +263,6 @@ impl Watcher {
notify::EventKind::Remove(_) => true, notify::EventKind::Remove(_) => true,
notify::EventKind::Other => false, notify::EventKind::Other => false,
} }
}
} }
/// The status in which the watcher can be. /// The status in which the watcher can be.

View File

@ -83,8 +83,8 @@ fn html_document_impl(
)?; )?;
let output = handle_list(&mut engine, &mut locator, children.iter().copied())?; let output = handle_list(&mut engine, &mut locator, children.iter().copied())?;
let introspector = Introspector::html(&output);
let root = root_element(output, &info)?; let root = root_element(output, &info)?;
let introspector = Introspector::html(&root);
Ok(HtmlDocument { info, root, introspector }) Ok(HtmlDocument { info, root, introspector })
} }
@ -307,18 +307,18 @@ fn head_element(info: &DocumentInfo) -> HtmlElement {
/// Determine which kind of output the user generated. /// Determine which kind of output the user generated.
fn classify_output(mut output: Vec<HtmlNode>) -> SourceResult<OutputKind> { fn classify_output(mut output: Vec<HtmlNode>) -> SourceResult<OutputKind> {
let len = output.len(); let count = output.iter().filter(|node| !matches!(node, HtmlNode::Tag(_))).count();
for node in &mut output { for node in &mut output {
let HtmlNode::Element(elem) = node else { continue }; let HtmlNode::Element(elem) = node else { continue };
let tag = elem.tag; let tag = elem.tag;
let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html)); let mut take = || std::mem::replace(elem, HtmlElement::new(tag::html));
match (tag, len) { match (tag, count) {
(tag::html, 1) => return Ok(OutputKind::Html(take())), (tag::html, 1) => return Ok(OutputKind::Html(take())),
(tag::body, 1) => return Ok(OutputKind::Body(take())), (tag::body, 1) => return Ok(OutputKind::Body(take())),
(tag::html | tag::body, _) => bail!( (tag::html | tag::body, _) => bail!(
elem.span, elem.span,
"`{}` element must be the only element in the document", "`{}` element must be the only element in the document",
elem.tag elem.tag,
), ),
_ => {} _ => {}
} }

View File

@ -1455,7 +1455,7 @@ impl<'a> CompletionContext<'a> {
let mut defined = BTreeMap::<EcoString, Option<Value>>::new(); let mut defined = BTreeMap::<EcoString, Option<Value>>::new();
named_items(self.world, self.leaf.clone(), |item| { named_items(self.world, self.leaf.clone(), |item| {
let name = item.name(); let name = item.name();
if !name.is_empty() && item.value().as_ref().map_or(true, filter) { if !name.is_empty() && item.value().as_ref().is_none_or(filter) {
defined.insert(name.clone(), item.value()); defined.insert(name.clone(), item.value());
} }

View File

@ -1377,7 +1377,7 @@ impl<'a> GridLayouter<'a> {
.footer .footer
.as_ref() .as_ref()
.and_then(Repeatable::as_repeated) .and_then(Repeatable::as_repeated)
.map_or(true, |footer| footer.start != header.end) .is_none_or(|footer| footer.start != header.end)
&& self.lrows.last().is_some_and(|row| row.index() < header.end) && self.lrows.last().is_some_and(|row| row.index() < header.end)
&& !in_last_with_offset( && !in_last_with_offset(
self.regions, self.regions,
@ -1446,7 +1446,7 @@ impl<'a> GridLayouter<'a> {
.iter_mut() .iter_mut()
.filter(|rowspan| (rowspan.y..rowspan.y + rowspan.rowspan).contains(&y)) .filter(|rowspan| (rowspan.y..rowspan.y + rowspan.rowspan).contains(&y))
.filter(|rowspan| { .filter(|rowspan| {
rowspan.max_resolved_row.map_or(true, |max_row| y > max_row) rowspan.max_resolved_row.is_none_or(|max_row| y > max_row)
}) })
{ {
// If the first region wasn't defined yet, it will have the // If the first region wasn't defined yet, it will have the
@ -1494,7 +1494,7 @@ impl<'a> GridLayouter<'a> {
// laid out at the first frame of the row). // laid out at the first frame of the row).
// Any rowspans ending before this row are laid out even // Any rowspans ending before this row are laid out even
// on this row's first frame. // on this row's first frame.
if laid_out_footer_start.map_or(true, |footer_start| { if laid_out_footer_start.is_none_or(|footer_start| {
// If this is a footer row, then only lay out this rowspan // If this is a footer row, then only lay out this rowspan
// if the rowspan is contained within the footer. // if the rowspan is contained within the footer.
y < footer_start || rowspan.y >= footer_start y < footer_start || rowspan.y >= footer_start
@ -1580,5 +1580,5 @@ pub(super) fn points(
/// our case, headers). /// our case, headers).
pub(super) fn in_last_with_offset(regions: Regions<'_>, offset: Abs) -> bool { pub(super) fn in_last_with_offset(regions: Regions<'_>, offset: Abs) -> bool {
regions.backlog.is_empty() regions.backlog.is_empty()
&& regions.last.map_or(true, |height| regions.size.y + offset == height) && regions.last.is_none_or(|height| regions.size.y + offset == height)
} }

View File

@ -463,7 +463,7 @@ pub fn hline_stroke_at_column(
// region, we have the last index, and (as a failsafe) we don't have the // region, we have the last index, and (as a failsafe) we don't have the
// last row of cells above us. // last row of cells above us.
let use_bottom_border_stroke = !in_last_region let use_bottom_border_stroke = !in_last_region
&& local_top_y.map_or(true, |top_y| top_y + 1 != grid.rows.len()) && local_top_y.is_none_or(|top_y| top_y + 1 != grid.rows.len())
&& y == grid.rows.len(); && y == grid.rows.len();
let bottom_y = let bottom_y =
if use_bottom_border_stroke { grid.rows.len().saturating_sub(1) } else { y }; if use_bottom_border_stroke { grid.rows.len().saturating_sub(1) } else { y };

View File

@ -588,7 +588,7 @@ impl GridLayouter<'_> {
measurement_data: &CellMeasurementData<'_>, measurement_data: &CellMeasurementData<'_>,
) -> bool { ) -> bool {
if sizes.len() <= 1 if sizes.len() <= 1
&& sizes.first().map_or(true, |&first_frame_size| { && sizes.first().is_none_or(|&first_frame_size| {
first_frame_size <= measurement_data.height_in_this_region first_frame_size <= measurement_data.height_in_this_region
}) })
{ {

View File

@ -154,7 +154,7 @@ pub fn line<'a>(
let mut items = collect_items(engine, p, range, trim); let mut items = collect_items(engine, p, range, trim);
// Add a hyphen at the line start, if a previous dash should be repeated. // 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 pred.is_some_and(|pred| should_repeat_hyphen(pred, full)) {
if let Some(shaped) = items.first_text_mut() { if let Some(shaped) = items.first_text_mut() {
shaped.prepend_hyphen(engine, p.config.fallback); shaped.prepend_hyphen(engine, p.config.fallback);
} }
@ -406,7 +406,7 @@ fn should_repeat_hyphen(pred_line: &Line, text: &str) -> bool {
// //
// See § 4.1.1.1.2.e on the "Ortografía de la lengua española" // See § 4.1.1.1.2.e on the "Ortografía de la lengua española"
// https://www.rae.es/ortografía/como-signo-de-división-de-palabras-a-final-de-línea // https://www.rae.es/ortografía/como-signo-de-división-de-palabras-a-final-de-línea
Lang::SPANISH => text.chars().next().map_or(false, |c| !c.is_uppercase()), Lang::SPANISH => text.chars().next().is_some_and(|c| !c.is_uppercase()),
_ => false, _ => false,
} }

View File

@ -290,7 +290,7 @@ fn linebreak_optimized_bounded<'a>(
} }
// If this attempt is better than what we had before, take it! // If this attempt is better than what we had before, take it!
if best.as_ref().map_or(true, |best| best.total >= total) { if best.as_ref().is_none_or(|best| best.total >= total) {
best = Some(Entry { pred: pred_index, total, line: attempt, end }); best = Some(Entry { pred: pred_index, total, line: attempt, end });
} }
} }
@ -423,7 +423,7 @@ fn linebreak_optimized_approximate(
let total = pred.total + line_cost; let total = pred.total + line_cost;
// If this attempt is better than what we had before, take it! // If this attempt is better than what we had before, take it!
if best.as_ref().map_or(true, |best| best.total >= total) { if best.as_ref().is_none_or(|best| best.total >= total) {
best = Some(Entry { best = Some(Entry {
pred: pred_index, pred: pred_index,
total, total,

View File

@ -465,7 +465,7 @@ impl<'a> ShapedText<'a> {
None None
}; };
let mut chain = families(self.styles) let mut chain = families(self.styles)
.filter(|family| family.covers().map_or(true, |c| c.is_match("-"))) .filter(|family| family.covers().is_none_or(|c| c.is_match("-")))
.map(|family| book.select(family.as_str(), self.variant)) .map(|family| book.select(family.as_str(), self.variant))
.chain(fallback_func.iter().map(|f| f())) .chain(fallback_func.iter().map(|f| f()))
.flatten(); .flatten();
@ -570,7 +570,7 @@ impl<'a> ShapedText<'a> {
// for the next line. // for the next line.
let dec = if ltr { usize::checked_sub } else { usize::checked_add }; let dec = if ltr { usize::checked_sub } else { usize::checked_add };
while let Some(next) = dec(idx, 1) { while let Some(next) = dec(idx, 1) {
if self.glyphs.get(next).map_or(true, |g| g.range.start != text_index) { if self.glyphs.get(next).is_none_or(|g| g.range.start != text_index) {
break; break;
} }
idx = next; idx = next;
@ -812,7 +812,7 @@ fn shape_segment<'a>(
.nth(1) .nth(1)
.map(|(i, _)| offset + i) .map(|(i, _)| offset + i)
.unwrap_or(text.len()); .unwrap_or(text.len());
covers.map_or(true, |cov| cov.is_match(&text[offset..end])) covers.is_none_or(|cov| cov.is_match(&text[offset..end]))
}; };
// Collect the shaped glyphs, doing fallback and shaping parts again with // Collect the shaped glyphs, doing fallback and shaping parts again with

View File

@ -34,7 +34,7 @@ pub fn layout_accent(
// Try to replace accent glyph with flattened variant. // Try to replace accent glyph with flattened variant.
let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height);
if base.height() > flattened_base_height { if base.ascent() > flattened_base_height {
glyph.make_flattened_accent_form(ctx); glyph.make_flattened_accent_form(ctx);
} }
@ -50,7 +50,7 @@ pub fn layout_accent(
// minus the accent base height. Only if the base is very small, we need // minus the accent base height. Only if the base is very small, we need
// a larger gap so that the accent doesn't move too low. // a larger gap so that the accent doesn't move too low.
let accent_base_height = scaled!(ctx, styles, accent_base_height); let accent_base_height = scaled!(ctx, styles, accent_base_height);
let gap = -accent.descent() - base.height().min(accent_base_height); let gap = -accent.descent() - base.ascent().min(accent_base_height);
let size = Size::new(base.width(), accent.height() + gap + base.height()); let size = Size::new(base.width(), accent.height() + gap + base.height());
let accent_pos = Point::with_x(base_attach - accent_attach); let accent_pos = Point::with_x(base_attach - accent_attach);
let base_pos = Point::with_y(accent.height() + gap); let base_pos = Point::with_y(accent.height() + gap);

View File

@ -437,10 +437,10 @@ impl PartialEq for Func {
} }
} }
impl PartialEq<&NativeFuncData> for Func { impl PartialEq<&'static NativeFuncData> for Func {
fn eq(&self, other: &&NativeFuncData) -> bool { fn eq(&self, other: &&'static NativeFuncData) -> bool {
match &self.repr { match &self.repr {
Repr::Native(native) => native.function == other.function, Repr::Native(native) => *native == Static(*other),
_ => false, _ => false,
} }
} }

View File

@ -21,6 +21,7 @@ use crate::foundations::{
/// be accessed using [field access notation]($scripting/#fields): /// be accessed using [field access notation]($scripting/#fields):
/// ///
/// - General symbols are defined in the [`sym` module]($category/symbols/sym) /// - General symbols are defined in the [`sym` module]($category/symbols/sym)
/// and are accessible without the `sym.` prefix in math mode.
/// - Emoji are defined in the [`emoji` module]($category/symbols/emoji) /// - Emoji are defined in the [`emoji` module]($category/symbols/emoji)
/// ///
/// Moreover, you can define custom symbols with this type's constructor /// Moreover, you can define custom symbols with this type's constructor
@ -410,7 +411,7 @@ fn find<'a>(
} }
let score = (matching, Reverse(total)); let score = (matching, Reverse(total));
if best_score.map_or(true, |b| score > b) { if best_score.is_none_or(|b| score > b) {
best = Some(candidate.1); best = Some(candidate.1);
best_score = Some(score); best_score = Some(score);
} }

View File

@ -10,7 +10,7 @@ use typst_utils::NonZeroExt;
use crate::diag::{bail, StrResult}; use crate::diag::{bail, StrResult};
use crate::foundations::{Content, Label, Repr, Selector}; use crate::foundations::{Content, Label, Repr, Selector};
use crate::html::{HtmlElement, HtmlNode}; use crate::html::HtmlNode;
use crate::introspection::{Location, Tag}; use crate::introspection::{Location, Tag};
use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform}; use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform};
use crate::model::Numbering; use crate::model::Numbering;
@ -55,8 +55,8 @@ impl Introspector {
/// Creates an introspector for HTML. /// Creates an introspector for HTML.
#[typst_macros::time(name = "introspect html")] #[typst_macros::time(name = "introspect html")]
pub fn html(root: &HtmlElement) -> Self { pub fn html(output: &[HtmlNode]) -> Self {
IntrospectorBuilder::new().build_html(root) IntrospectorBuilder::new().build_html(output)
} }
/// Iterates over all locatable elements. /// Iterates over all locatable elements.
@ -392,9 +392,9 @@ impl IntrospectorBuilder {
} }
/// Build an introspector for an HTML document. /// Build an introspector for an HTML document.
fn build_html(mut self, root: &HtmlElement) -> Introspector { fn build_html(mut self, output: &[HtmlNode]) -> Introspector {
let mut elems = Vec::new(); let mut elems = Vec::new();
self.discover_in_html(&mut elems, root); self.discover_in_html(&mut elems, output);
self.finalize(elems) self.finalize(elems)
} }
@ -434,16 +434,16 @@ impl IntrospectorBuilder {
} }
/// Processes the tags in the HTML element. /// Processes the tags in the HTML element.
fn discover_in_html(&mut self, sink: &mut Vec<Pair>, elem: &HtmlElement) { fn discover_in_html(&mut self, sink: &mut Vec<Pair>, nodes: &[HtmlNode]) {
for child in &elem.children { for node in nodes {
match child { match node {
HtmlNode::Tag(tag) => self.discover_in_tag( HtmlNode::Tag(tag) => self.discover_in_tag(
sink, sink,
tag, tag,
Position { page: NonZeroUsize::ONE, point: Point::zero() }, Position { page: NonZeroUsize::ONE, point: Point::zero() },
), ),
HtmlNode::Text(_, _) => {} HtmlNode::Text(_, _) => {}
HtmlNode::Element(elem) => self.discover_in_html(sink, elem), HtmlNode::Element(elem) => self.discover_in_html(sink, &elem.children),
HtmlNode::Frame(frame) => self.discover_in_frame( HtmlNode::Frame(frame) => self.discover_in_frame(
sink, sink,
frame, frame,

View File

@ -1387,7 +1387,7 @@ impl<'a> CellGrid<'a> {
// Include the gutter right before the footer, unless there is // Include the gutter right before the footer, unless there is
// none, or the gutter is already included in the header (no // none, or the gutter is already included in the header (no
// rows between the header and the footer). // rows between the header and the footer).
if header_end.map_or(true, |header_end| header_end != footer.start) { if header_end != Some(footer.start) {
footer.start = footer.start.saturating_sub(1); footer.start = footer.start.saturating_sub(1);
} }
} }
@ -1526,11 +1526,7 @@ impl<'a> CellGrid<'a> {
self.entry(x, y).map(|entry| match entry { self.entry(x, y).map(|entry| match entry {
Entry::Cell(_) => Axes::new(x, y), Entry::Cell(_) => Axes::new(x, y),
Entry::Merged { parent } => { Entry::Merged { parent } => {
let c = if self.has_gutter { let c = self.non_gutter_column_count();
1 + self.cols.len() / 2
} else {
self.cols.len()
};
let factor = if self.has_gutter { 2 } else { 1 }; let factor = if self.has_gutter { 2 } else { 1 };
Axes::new(factor * (*parent % c), factor * (*parent / c)) Axes::new(factor * (*parent % c), factor * (*parent / c))
} }
@ -1602,6 +1598,21 @@ impl<'a> CellGrid<'a> {
cell.rowspan.get() cell.rowspan.get()
} }
} }
#[inline]
pub fn non_gutter_column_count(&self) -> usize {
if self.has_gutter {
// Calculation: With gutters, we have
// 'cols = 2 * (non-gutter cols) - 1', since there is a gutter
// column between each regular column. Therefore,
// 'floor(cols / 2)' will be equal to
// 'floor(non-gutter cols - 1/2) = non-gutter-cols - 1',
// so 'non-gutter cols = 1 + floor(cols / 2)'.
1 + self.cols.len() / 2
} else {
self.cols.len()
}
}
} }
/// Given a cell's requested x and y, the vector with the resolved cell /// Given a cell's requested x and y, the vector with the resolved cell

View File

@ -282,7 +282,7 @@ fn show_cell_html(tag: HtmlTag, cell: &Cell, styles: StyleChain) -> Content {
fn show_cellgrid_html(grid: CellGrid, styles: StyleChain) -> Content { fn show_cellgrid_html(grid: CellGrid, styles: StyleChain) -> Content {
let elem = |tag, body| HtmlElem::new(tag).with_body(Some(body)).pack(); let elem = |tag, body| HtmlElem::new(tag).with_body(Some(body)).pack();
let mut rows: Vec<_> = grid.entries.chunks(grid.cols.len()).collect(); let mut rows: Vec<_> = grid.entries.chunks(grid.non_gutter_column_count()).collect();
let tr = |tag, row: &[Entry]| { let tr = |tag, row: &[Entry]| {
let row = row let row = row

View File

@ -160,7 +160,7 @@ impl FontBook {
current.variant.weight.distance(variant.weight), current.variant.weight.distance(variant.weight),
); );
if best_key.map_or(true, |b| key < b) { if best_key.is_none_or(|b| key < b) {
best = Some(id); best = Some(id);
best_key = Some(key); best_key = Some(key);
} }

View File

@ -159,7 +159,7 @@ fn is_shapable(engine: &Engine, text: &str, styles: StyleChain) -> bool {
{ {
let covers = family.covers(); let covers = family.covers();
return text.chars().all(|c| { return text.chars().all(|c| {
covers.map_or(true, |cov| cov.is_match(c.encode_utf8(&mut [0; 4]))) covers.is_none_or(|cov| cov.is_match(c.encode_utf8(&mut [0; 4])))
&& font.ttf().glyph_index(c).is_some() && font.ttf().glyph_index(c).is_some()
}); });
} }

View File

@ -130,7 +130,7 @@ static TO_SRGB: LazyLock<qcms::Transform> = LazyLock::new(|| {
/// ///
/// # Predefined color maps /// # Predefined color maps
/// Typst also includes a number of preset color maps that can be used for /// Typst also includes a number of preset color maps that can be used for
/// [gradients]($gradient.linear). These are simply arrays of colors defined in /// [gradients]($gradient/#stops). These are simply arrays of colors defined in
/// the module `color.map`. /// the module `color.map`.
/// ///
/// ```example /// ```example

View File

@ -70,6 +70,9 @@ use crate::visualize::{Color, ColorSpace, WeightedColor};
/// the offsets when defining a gradient. In this case, Typst will space all /// the offsets when defining a gradient. In this case, Typst will space all
/// stops evenly. /// stops evenly.
/// ///
/// Typst predefines color maps that you can use as stops. See the
/// [`color`]($color/#predefined-color-maps) documentation for more details.
///
/// # Relativeness /// # Relativeness
/// The location of the `{0%}` and `{100%}` stops depends on the dimensions /// The location of the `{0%}` and `{100%}` stops depends on the dimensions
/// of a container. This container can either be the shape that it is being /// of a container. This container can either be the shape that it is being
@ -157,10 +160,6 @@ use crate::visualize::{Color, ColorSpace, WeightedColor};
/// ) /// )
/// ``` /// ```
/// ///
/// # Presets
/// Typst predefines color maps that you can use with your gradients. See the
/// [`color`]($color/#predefined-color-maps) documentation for more details.
///
/// # Note on file sizes /// # Note on file sizes
/// ///
/// Gradients can be quite large, especially if they have many stops. This is /// Gradients can be quite large, especially if they have many stops. This is
@ -288,7 +287,7 @@ impl Gradient {
/// )), /// )),
/// ) /// )
/// ``` /// ```
#[func] #[func(title = "Radial Gradient")]
fn radial( fn radial(
span: Span, span: Span,
/// The color [stops](#stops) of the gradient. /// The color [stops](#stops) of the gradient.
@ -402,7 +401,7 @@ impl Gradient {
/// )), /// )),
/// ) /// )
/// ``` /// ```
#[func] #[func(title = "Conic Gradient")]
pub fn conic( pub fn conic(
span: Span, span: Span,
/// The color [stops](#stops) of the gradient. /// The color [stops](#stops) of the gradient.

View File

@ -70,7 +70,7 @@ pub(crate) fn write_outline(
// (not exceeding whichever is the most restrictive depth limit // (not exceeding whichever is the most restrictive depth limit
// of those two). // of those two).
while children.last().is_some_and(|last| { while children.last().is_some_and(|last| {
last_skipped_level.map_or(true, |l| last.level < l) last_skipped_level.is_none_or(|l| last.level < l)
&& last.level < leaf.level && last.level < leaf.level
}) { }) {
children = &mut children.last_mut().unwrap().children; children = &mut children.last_mut().unwrap().children;
@ -83,7 +83,7 @@ pub(crate) fn write_outline(
// needed, following the usual rules listed above. // needed, following the usual rules listed above.
last_skipped_level = None; last_skipped_level = None;
children.push(leaf); children.push(leaf);
} else if last_skipped_level.map_or(true, |l| leaf.level < l) { } else if last_skipped_level.is_none_or(|l| leaf.level < l) {
// Only the topmost / lowest-level skipped heading matters when you // Only the topmost / lowest-level skipped heading matters when you
// have consecutive skipped headings (since none of them are being // have consecutive skipped headings (since none of them are being
// added to the bookmark tree), hence the condition above. // added to the bookmark tree), hence the condition above.

View File

@ -753,7 +753,7 @@ impl<'a> LinkedNode<'a> {
// sibling's span number is larger than the target span's number. // sibling's span number is larger than the target span's number.
if children if children
.peek() .peek()
.map_or(true, |next| next.span().number() > span.number()) .is_none_or(|next| next.span().number() > span.number())
{ {
if let Some(found) = child.find(span) { if let Some(found) = child.find(span) {
return Some(found); return Some(found);

View File

@ -327,8 +327,8 @@ impl PackageVersion {
/// missing in the bound are ignored. /// missing in the bound are ignored.
pub fn matches_eq(&self, bound: &VersionBound) -> bool { pub fn matches_eq(&self, bound: &VersionBound) -> bool {
self.major == bound.major self.major == bound.major
&& bound.minor.map_or(true, |minor| self.minor == minor) && bound.minor.is_none_or(|minor| self.minor == minor)
&& bound.patch.map_or(true, |patch| self.patch == patch) && bound.patch.is_none_or(|patch| self.patch == patch)
} }
/// Performs a `>` match with the given version bound. The match only /// Performs a `>` match with the given version bound. The match only

View File

@ -271,7 +271,8 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) {
} }
SyntaxKind::Text | SyntaxKind::MathText | SyntaxKind::MathShorthand => { SyntaxKind::Text | SyntaxKind::MathText | SyntaxKind::MathShorthand => {
continuable = matches!( continuable = !p.at(SyntaxKind::MathShorthand)
&& matches!(
math_class(p.current_text()), math_class(p.current_text()),
None | Some(MathClass::Alphabetic) None | Some(MathClass::Alphabetic)
); );

View File

@ -360,6 +360,21 @@ pub fn default_math_class(c: char) -> Option<MathClass> {
// https://github.com/typst/typst/pull/5714 // https://github.com/typst/typst/pull/5714
'\u{22A5}' => Some(MathClass::Normal), '\u{22A5}' => Some(MathClass::Normal),
// Used as a binary connector in linear logic, where it is referred to
// as "par".
// https://github.com/typst/typst/issues/5764
'⅋' => Some(MathClass::Binary),
// Those overrides should become the default in the next revision of
// MathClass.txt.
// https://github.com/typst/typst/issues/5764#issuecomment-2632435247
'⎰' | '⟅' => Some(MathClass::Opening),
'⎱' | '⟆' => Some(MathClass::Closing),
// Both and ⟑ are classified as Binary.
// https://github.com/typst/typst/issues/5764
'⟇' => Some(MathClass::Binary),
c => unicode_math_class::class(c), c => unicode_math_class::class(c),
} }
} }

View File

@ -28,7 +28,7 @@ impl Scalar {
/// ///
/// If the value is NaN, then it is set to `0.0` in the result. /// If the value is NaN, then it is set to `0.0` in the result.
pub const fn new(x: f64) -> Self { pub const fn new(x: f64) -> Self {
Self(if is_nan(x) { 0.0 } else { x }) Self(if x.is_nan() { 0.0 } else { x })
} }
/// Gets the value of this [`Scalar`]. /// Gets the value of this [`Scalar`].
@ -37,17 +37,6 @@ impl Scalar {
} }
} }
// We have to detect NaNs this way since `f64::is_nan` isnt const
// on stable yet:
// ([tracking issue](https://github.com/rust-lang/rust/issues/57241))
#[allow(clippy::unusual_byte_groupings)]
const fn is_nan(x: f64) -> bool {
// Safety: all bit patterns are valid for u64, and f64 has no padding bits.
// We cannot use `f64::to_bits` because it is not const.
let x_bits = unsafe { std::mem::transmute::<f64, u64>(x) };
(x_bits << 1 >> (64 - 12 + 1)) == 0b0_111_1111_1111 && (x_bits << 12) != 0
}
impl Numeric for Scalar { impl Numeric for Scalar {
fn zero() -> Self { fn zero() -> Self {
Self(0.0) Self(0.0)

View File

@ -56,7 +56,7 @@ requirements with examples.
Typst's default page size is A4 paper. Depending on your region and your use Typst's default page size is A4 paper. Depending on your region and your use
case, you will want to change this. You can do this by using the case, you will want to change this. You can do this by using the
[`{page}`]($page) set rule and passing it a string argument to use a common page [`{page}`]($page) set rule and passing it a string argument to use a common page
size. Options include the complete ISO 216 series (e.g. `"iso-a4"`, `"iso-c2"`), size. Options include the complete ISO 216 series (e.g. `"a4"` and `"iso-c2"`),
customary US formats like `"us-legal"` or `"us-letter"`, and more. Check out the customary US formats like `"us-legal"` or `"us-letter"`, and more. Check out the
reference for the [page's paper argument]($page.paper) to learn about all reference for the [page's paper argument]($page.paper) to learn about all
available options. available options.

View File

@ -170,8 +170,8 @@
category: symbols category: symbols
path: ["emoji"] path: ["emoji"]
details: | details: |
Named emoji. Named emojis.
For example, `#emoji.face` produces the 😀 emoji. If you frequently use For example, `#emoji.face` produces the 😀 emoji. If you frequently use
certain emojis, you can also import them from the `emoji` module (`[#import certain emojis, you can also import them from the `emoji` module (`[#import
emoji: face]`) to use them without the `#emoji.` prefix. emoji: face]`) to use them without the `emoji.` prefix.

6
flake.lock generated
View File

@ -112,13 +112,13 @@
"rust-manifest": { "rust-manifest": {
"flake": false, "flake": false,
"locked": { "locked": {
"narHash": "sha256-Yqu2/i9170R7pQhvOCR1f5SyFr7PcFbO6xcMr9KWruQ=", "narHash": "sha256-irgHsBXecwlFSdmP9MfGP06Cbpca2QALJdbN4cymcko=",
"type": "file", "type": "file",
"url": "https://static.rust-lang.org/dist/channel-rust-1.83.0.toml" "url": "https://static.rust-lang.org/dist/channel-rust-1.85.0.toml"
}, },
"original": { "original": {
"type": "file", "type": "file",
"url": "https://static.rust-lang.org/dist/channel-rust-1.83.0.toml" "url": "https://static.rust-lang.org/dist/channel-rust-1.85.0.toml"
} }
}, },
"systems": { "systems": {

View File

@ -10,7 +10,7 @@
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
rust-manifest = { rust-manifest = {
url = "https://static.rust-lang.org/dist/channel-rust-1.83.0.toml"; url = "https://static.rust-lang.org/dist/channel-rust-1.85.0.toml";
flake = false; flake = false;
}; };
}; };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<table>
<tr>
<td>a</td>
<td>b</td>
<td>c</td>
</tr>
<tr>
<td>d</td>
<td>e</td>
<td>f</td>
</tr>
<tr>
<td>g</td>
<td>h</td>
<td>i</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<table>
<tr>
<td>a</td>
<td>b</td>
<td>c</td>
</tr>
<tr>
<td>d</td>
<td>e</td>
<td>f</td>
</tr>
<tr>
<td>g</td>
<td>h</td>
<td>i</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,2 @@
<!DOCTYPE html>
<html></html>

View File

@ -0,0 +1,2 @@
<!DOCTYPE html>
<html>Hi</html>

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<table>
<tr>
<td>a</td>
<td>b</td>
<td>c</td>
</tr>
<tr>
<td>d</td>
<td>e</td>
<td>f</td>
</tr>
<tr>
<td>g</td>
<td>h</td>
<td>i</td>
</tr>
</table>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 614 B

After

Width:  |  Height:  |  Size: 625 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 327 B

After

Width:  |  Height:  |  Size: 327 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 B

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 926 B

After

Width:  |  Height:  |  Size: 930 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -149,7 +149,7 @@ impl Collector {
for entry in walkdir::WalkDir::new(crate::SUITE_PATH).sort_by_file_name() { for entry in walkdir::WalkDir::new(crate::SUITE_PATH).sort_by_file_name() {
let entry = entry.unwrap(); let entry = entry.unwrap();
let path = entry.path(); let path = entry.path();
if !path.extension().is_some_and(|ext| ext == "typ") { if path.extension().is_none_or(|ext| ext != "typ") {
continue; continue;
} }
@ -168,7 +168,7 @@ impl Collector {
for entry in walkdir::WalkDir::new(crate::REF_PATH).sort_by_file_name() { for entry in walkdir::WalkDir::new(crate::REF_PATH).sort_by_file_name() {
let entry = entry.unwrap(); let entry = entry.unwrap();
let path = entry.path(); let path = entry.path();
if !path.extension().is_some_and(|ext| ext == "png") { if path.extension().is_none_or(|ext| ext != "png") {
continue; continue;
} }

View File

@ -161,7 +161,7 @@ impl<'a> Runner<'a> {
// Compare against reference output if available. // Compare against reference output if available.
// Test that is ok doesn't need to be updated. // Test that is ok doesn't need to be updated.
if ref_data.as_ref().map_or(false, |r| D::matches(&live, r)) { if ref_data.as_ref().is_ok_and(|r| D::matches(&live, r)) {
return; return;
} }

15
tests/suite/html/elem.typ Normal file
View File

@ -0,0 +1,15 @@
--- html-elem-alone-context html ---
#context html.elem("html")
--- html-elem-not-alone html ---
// Error: 2-19 `<html>` element must be the only element in the document
#html.elem("html")
Text
--- html-elem-metadata html ---
#html.elem("html", context {
let val = query(<l>).first().value
test(val, "Hi")
val
})
#metadata("Hi") <l>

View File

@ -30,3 +30,30 @@
[row], [row],
), ),
) )
--- col-gutter-table html ---
#table(
columns: 3,
column-gutter: 3pt,
[a], [b], [c],
[d], [e], [f],
[g], [h], [i]
)
--- row-gutter-table html ---
#table(
columns: 3,
row-gutter: 3pt,
[a], [b], [c],
[d], [e], [f],
[g], [h], [i]
)
--- col-row-gutter-table html ---
#table(
columns: 3,
gutter: 3pt,
[a], [b], [c],
[d], [e], [f],
[g], [h], [i]
)

View File

@ -13,6 +13,11 @@ $ underline(f' : NN -> RR) \
1 - 0 thick &..., 1 - 0 thick &...,
) $ ) $
--- math-shorthands-noncontinuable ---
// Test that shorthands are not continuable.
$ x >=(y) / z \
x >= (y) / z $
--- math-common-symbols --- --- math-common-symbols ---
// Test common symbols. // Test common symbols.
$ dot \ dots \ ast \ tilde \ star $ $ dot \ dots \ ast \ tilde \ star $