diff --git a/Cargo.lock b/Cargo.lock index a92c0d228..f238f4f56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2790,7 +2790,6 @@ dependencies = [ "typst-assets", "typst-macros", "typst-timing", - "unicode-properties", "unscanny", "xmp-writer", ] diff --git a/Cargo.toml b/Cargo.toml index e26f058ea..098900620 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -114,7 +114,6 @@ typed-arena = "2" unicode-bidi = "0.3.13" unicode-ident = "1.0" unicode-math-class = "0.1" -unicode-properties = "0.1" unicode-script = "0.5" unicode-segmentation = "1" unscanny = "0.1" diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index d15264255..cc85d9209 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -10,10 +10,9 @@ use parking_lot::RwLock; use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use typst::diag::{bail, Severity, SourceDiagnostic, StrResult, Warned}; use typst::foundations::{Datetime, Smart}; -use typst::layout::{Frame, PageRanges}; +use typst::layout::{Frame, Page, PageRanges}; use typst::model::Document; use typst::syntax::{FileId, Source, Span}; -use typst::visualize::Color; use typst::WorldExt; use crate::args::{ @@ -269,7 +268,7 @@ fn export_image( Output::Stdout => Output::Stdout, }; - export_image_page(command, &page.frame, &output, fmt)?; + export_image_page(command, page, &output, fmt)?; Ok(()) }) .collect::, EcoString>>()?; @@ -309,13 +308,13 @@ mod output_template { /// Export single image. fn export_image_page( command: &CompileCommand, - frame: &Frame, + page: &Page, output: &Output, fmt: ImageExportFormat, ) -> StrResult<()> { match fmt { ImageExportFormat::Png => { - let pixmap = typst_render::render(frame, command.ppi / 72.0, Color::WHITE); + let pixmap = typst_render::render(page, command.ppi / 72.0); let buf = pixmap .encode_png() .map_err(|err| eco_format!("failed to encode PNG file ({err})"))?; @@ -324,7 +323,7 @@ fn export_image_page( .map_err(|err| eco_format!("failed to write PNG file ({err})"))?; } ImageExportFormat::Svg => { - let svg = typst_svg::svg(frame); + let svg = typst_svg::svg(page); output .write(svg.as_bytes()) .map_err(|err| eco_format!("failed to write SVG file ({err})"))?; diff --git a/crates/typst-cli/src/download.rs b/crates/typst-cli/src/download.rs index 38b160081..63a2e4163 100644 --- a/crates/typst-cli/src/download.rs +++ b/crates/typst-cli/src/download.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use native_tls::{Certificate, TlsConnector}; -use once_cell::sync::Lazy; +use once_cell::sync::OnceCell; use ureq::Response; use crate::terminal; @@ -16,13 +16,22 @@ use crate::terminal; /// Keep track of this many download speed samples. const SPEED_SAMPLES: usize = 5; -/// Lazily loads a custom CA certificate if present, but if there's an error -/// loading certificate, it just uses the default configuration. -static CERT: Lazy> = Lazy::new(|| { - let path = crate::ARGS.cert.as_ref()?; - let pem = std::fs::read(path).ok()?; - Certificate::from_pem(&pem).ok() -}); +/// Load a certificate from the file system if the `--cert` argument or +/// `TYPST_CERT` environment variable is present. The certificate is cached for +/// efficiency. +/// +/// - Returns `None` if `--cert` and `TYPST_CERT` are not set. +/// - Returns `Some(Ok(cert))` if the certificate was loaded successfully. +/// - Returns `Some(Err(err))` if an error occurred while loading the certificate. +fn cert() -> Option> { + static CERT: OnceCell = OnceCell::new(); + crate::ARGS.cert.as_ref().map(|path| { + CERT.get_or_try_init(|| { + let pem = std::fs::read(path)?; + Certificate::from_pem(&pem).map_err(io::Error::other) + }) + }) +} /// Download binary data and display its progress. #[allow(clippy::result_large_err)] @@ -49,8 +58,8 @@ pub fn download(url: &str) -> Result { } // Apply a custom CA certificate if present. - if let Some(cert) = &*CERT { - tls.add_root_certificate(cert.clone()); + if let Some(cert) = cert() { + tls.add_root_certificate(cert?.clone()); } // Configure native TLS. diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index f6c96d001..d534f55cc 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -5,8 +5,8 @@ use ecow::{eco_format, EcoString}; use if_chain::if_chain; use serde::{Deserialize, Serialize}; use typst::foundations::{ - fields_on, format_str, mutable_methods_on, repr, AutoValue, CastInfo, Func, Label, - NoneValue, Repr, Scope, StyleChain, Styles, Type, Value, + fields_on, format_str, repr, AutoValue, CastInfo, Func, Label, NoneValue, Repr, + Scope, StyleChain, Styles, Type, Value, }; use typst::model::Document; use typst::syntax::{ @@ -396,19 +396,6 @@ fn field_access_completions( } } - for &(method, args) in mutable_methods_on(value.ty()) { - ctx.completions.push(Completion { - kind: CompletionKind::Func, - label: method.into(), - apply: Some(if args { - eco_format!("{method}(${{}})") - } else { - eco_format!("{method}()${{}}") - }), - detail: None, - }) - } - for &field in fields_on(value.ty()) { // Complete the field name along with its value. Notes: // 1. No parentheses since function fields cannot currently be called @@ -1394,7 +1381,7 @@ mod tests { } #[test] - fn test_whitespace_in_autocomplete() { + fn test_autocomplete_whitespace() { //Check that extra space before '.' is handled correctly. test("#() .", 5, &[], &["insert", "remove", "len", "all"]); test("#{() .}", 6, &["insert", "remove", "len", "all"], &["foo"]); @@ -1404,10 +1391,16 @@ mod tests { } #[test] - fn test_before_window_char_boundary() { + fn test_autocomplete_before_window_char_boundary() { // Check that the `before_window` doesn't slice into invalid byte // boundaries. let s = "πŸ˜€πŸ˜€ #text(font: \"\")"; test(s, s.len() - 2, &[], &[]); } + + #[test] + fn test_autocomplete_mutable_method() { + let s = "#{ let x = (1, 2, 3); x. }"; + test(s, s.len() - 2, &["at", "push", "pop"], &[]); + } } diff --git a/crates/typst-pdf/Cargo.toml b/crates/typst-pdf/Cargo.toml index d2dcd5f5c..a3a693f38 100644 --- a/crates/typst-pdf/Cargo.toml +++ b/crates/typst-pdf/Cargo.toml @@ -29,7 +29,6 @@ pdf-writer = { workspace = true } subsetter = { workspace = true } svg2pdf = { workspace = true } ttf-parser = { workspace = true } -unicode-properties = { workspace = true } unscanny = { workspace = true } xmp-writer = { workspace = true } diff --git a/crates/typst-pdf/src/color_font.rs b/crates/typst-pdf/src/color_font.rs index 201915b19..4889d9151 100644 --- a/crates/typst-pdf/src/color_font.rs +++ b/crates/typst-pdf/src/color_font.rs @@ -243,7 +243,7 @@ impl ColorFontMap<()> { let width = font.advance(gid).unwrap_or(Em::new(0.0)).get() * font.units_per_em(); let instructions = - content::build(&mut self.resources, &frame, Some(width as f32)); + content::build(&mut self.resources, &frame, None, Some(width as f32)); color_font.glyphs.push(ColorGlyph { gid, instructions }); color_font.glyph_indices.insert(gid, index); diff --git a/crates/typst-pdf/src/content.rs b/crates/typst-pdf/src/content.rs index da9e4ed44..e88769449 100644 --- a/crates/typst-pdf/src/content.rs +++ b/crates/typst-pdf/src/content.rs @@ -16,7 +16,8 @@ use typst::model::Destination; use typst::text::{color::is_color_glyph, Font, TextItem, TextItemView}; use typst::utils::{Deferred, Numeric, SliceExt}; use typst::visualize::{ - FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, Shape, + FillRule, FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, + Shape, }; use crate::color_font::ColorFontMap; @@ -36,6 +37,7 @@ use crate::{deflate_deferred, AbsExt, EmExt}; pub fn build( resources: &mut Resources<()>, frame: &Frame, + fill: Option, color_glyph_width: Option, ) -> Encoded { let size = frame.size(); @@ -53,6 +55,11 @@ pub fn build( .post_concat(Transform::translate(Abs::zero(), size.y)), ); + if let Some(fill) = fill { + let shape = Geometry::Rect(frame.size()).filled(fill); + write_shape(&mut ctx, Point::zero(), &shape); + } + // Encode the frame into the content stream. write_frame(&mut ctx, frame); @@ -630,11 +637,13 @@ fn write_shape(ctx: &mut Builder, pos: Point, shape: &Shape) { } } - match (&shape.fill, stroke) { - (None, None) => unreachable!(), - (Some(_), None) => ctx.content.fill_nonzero(), - (None, Some(_)) => ctx.content.stroke(), - (Some(_), Some(_)) => ctx.content.fill_nonzero_and_stroke(), + match (&shape.fill, &shape.fill_rule, stroke) { + (None, _, None) => unreachable!(), + (Some(_), FillRule::NonZero, None) => ctx.content.fill_nonzero(), + (Some(_), FillRule::EvenOdd, None) => ctx.content.fill_even_odd(), + (None, _, Some(_)) => ctx.content.stroke(), + (Some(_), FillRule::NonZero, Some(_)) => ctx.content.fill_nonzero_and_stroke(), + (Some(_), FillRule::EvenOdd, Some(_)) => ctx.content.fill_even_odd_and_stroke(), }; } diff --git a/crates/typst-pdf/src/font.rs b/crates/typst-pdf/src/font.rs index fd719799d..c88c2bfde 100644 --- a/crates/typst-pdf/src/font.rs +++ b/crates/typst-pdf/src/font.rs @@ -12,7 +12,6 @@ use subsetter::GlyphRemapper; use ttf_parser::{name_id, GlyphId, Tag}; use typst::text::Font; use typst::utils::SliceExt; -use unicode_properties::{GeneralCategory, UnicodeGeneralCategory}; use crate::{deflate, EmExt, PdfChunk, WithGlobalRefs}; @@ -226,38 +225,6 @@ pub(crate) fn subset_tag(glyphs: &T) -> EcoString { std::str::from_utf8(&letter).unwrap().into() } -/// For glyphs that have codepoints mapping to them in the font's cmap table, we -/// prefer them over pre-existing text mappings from the document. Only things -/// that don't have a corresponding codepoint (or only a private-use one) like -/// the "Th" in Linux Libertine get the text of their first occurrences in the -/// document instead. -/// -/// This function replaces as much copepoints from the document with ones from -/// the cmap table as possible. -pub fn improve_glyph_sets(glyph_sets: &mut HashMap>) { - for (font, glyph_set) in glyph_sets { - let ttf = font.ttf(); - - for subtable in ttf.tables().cmap.into_iter().flat_map(|table| table.subtables) { - if !subtable.is_unicode() { - continue; - } - - subtable.codepoints(|n| { - let Some(c) = std::char::from_u32(n) else { return }; - if c.general_category() == GeneralCategory::PrivateUse { - return; - } - - let Some(GlyphId(g)) = ttf.glyph_index(c) else { return }; - if glyph_set.contains_key(&g) { - glyph_set.insert(g, c.into()); - } - }); - } - } -} - /// Create a compressed `/ToUnicode` CMap. #[comemo::memoize] #[typst_macros::time(name = "create cmap")] diff --git a/crates/typst-pdf/src/page.rs b/crates/typst-pdf/src/page.rs index 2983f504f..1001d8992 100644 --- a/crates/typst-pdf/src/page.rs +++ b/crates/typst-pdf/src/page.rs @@ -8,12 +8,12 @@ use pdf_writer::{ }; use typst::foundations::Label; use typst::introspection::Location; -use typst::layout::{Abs, Frame}; +use typst::layout::{Abs, Page}; use typst::model::{Destination, Numbering}; use typst::text::Case; +use crate::Resources; use crate::{content, AbsExt, PdfChunk, WithDocument, WithRefs, WithResources}; -use crate::{font::improve_glyph_sets, Resources}; /// Construct page objects. #[typst_macros::time(name = "construct pages")] @@ -33,7 +33,7 @@ pub fn traverse_pages( pages.push(None); skipped_pages += 1; } else { - let mut encoded = construct_page(&mut resources, &page.frame); + let mut encoded = construct_page(&mut resources, page); encoded.label = page .numbering .as_ref() @@ -52,17 +52,13 @@ pub fn traverse_pages( } } - improve_glyph_sets(&mut resources.glyph_sets); - improve_glyph_sets(&mut resources.color_glyph_sets); - (PdfChunk::new(), (pages, resources)) } /// Construct a page object. #[typst_macros::time(name = "construct page")] -fn construct_page(out: &mut Resources<()>, frame: &Frame) -> EncodedPage { - let content = content::build(out, frame, None); - +fn construct_page(out: &mut Resources<()>, page: &Page) -> EncodedPage { + let content = content::build(out, &page.frame, page.fill_or_transparent(), None); EncodedPage { content, label: None } } diff --git a/crates/typst-pdf/src/pattern.rs b/crates/typst-pdf/src/pattern.rs index e06c04f87..d4f5a6e08 100644 --- a/crates/typst-pdf/src/pattern.rs +++ b/crates/typst-pdf/src/pattern.rs @@ -103,7 +103,7 @@ fn register_pattern( }; // Render the body. - let content = content::build(&mut patterns.resources, pattern.frame(), None); + let content = content::build(&mut patterns.resources, pattern.frame(), None, None); let pdf_pattern = PdfPattern { transform, diff --git a/crates/typst-pdf/src/resources.rs b/crates/typst-pdf/src/resources.rs index a2cf56878..32b6612ff 100644 --- a/crates/typst-pdf/src/resources.rs +++ b/crates/typst-pdf/src/resources.rs @@ -77,11 +77,16 @@ pub struct Resources { pub languages: BTreeMap, /// For each font a mapping from used glyphs to their text representation. - /// May contain multiple chars in case of ligatures or similar things. The - /// same glyph can have a different text representation within one document, - /// then we just save the first one. The resulting strings are used for the - /// PDF's /ToUnicode map for glyphs that don't have an entry in the font's - /// cmap. This is important for copy-paste and searching. + /// This is used for the PDF's /ToUnicode map, and important for copy-paste + /// and searching. + /// + /// Note that the text representation may contain multiple chars in case of + /// ligatures or similar things, and it may have no entry in the font's cmap + /// (or only a private-use codepoint), like the β€œTh” in Linux Libertine. + /// + /// A glyph may have multiple entries in the font's cmap, and even the same + /// glyph can have a different text representation within one document. + /// But /ToUnicode does not support that, so we just save the first occurrence. pub glyph_sets: HashMap>, /// Same as `glyph_sets`, but for color fonts. pub color_glyph_sets: HashMap>, diff --git a/crates/typst-render/src/lib.rs b/crates/typst-render/src/lib.rs index 305dcd1fc..d5eeacce8 100644 --- a/crates/typst-render/src/lib.rs +++ b/crates/typst-render/src/lib.rs @@ -7,45 +7,49 @@ mod text; use tiny_skia as sk; use typst::layout::{ - Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Size, Transform, + Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Page, Point, Size, Transform, }; use typst::model::Document; -use typst::visualize::Color; +use typst::visualize::{Color, Geometry, Paint}; -/// Export a frame into a raster image. +/// Export a page into a raster image. /// -/// This renders the frame at the given number of pixels per point and returns +/// This renders the page at the given number of pixels per point and returns /// the resulting `tiny-skia` pixel buffer. #[typst_macros::time(name = "render")] -pub fn render(frame: &Frame, pixel_per_pt: f32, fill: Color) -> sk::Pixmap { - let size = frame.size(); +pub fn render(page: &Page, pixel_per_pt: f32) -> sk::Pixmap { + let size = page.frame.size(); let pxw = (pixel_per_pt * size.x.to_f32()).round().max(1.0) as u32; let pxh = (pixel_per_pt * size.y.to_f32()).round().max(1.0) as u32; - let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap(); - canvas.fill(paint::to_sk_color(fill)); - let ts = sk::Transform::from_scale(pixel_per_pt, pixel_per_pt); - render_frame(&mut canvas, State::new(size, ts, pixel_per_pt), frame); + let state = State::new(size, ts, pixel_per_pt); + + let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap(); + + if let Some(fill) = page.fill_or_white() { + if let Paint::Solid(color) = fill { + canvas.fill(paint::to_sk_color(color)); + } else { + let rect = Geometry::Rect(page.frame.size()).filled(fill); + shape::render_shape(&mut canvas, state, &rect); + } + } + + render_frame(&mut canvas, state, &page.frame); canvas } /// Export a document with potentially multiple pages into a single raster image. -/// -/// The gap will be added between the individual frames. pub fn render_merged( document: &Document, pixel_per_pt: f32, - frame_fill: Color, gap: Abs, - gap_fill: Color, + fill: Option, ) -> sk::Pixmap { - let pixmaps: Vec<_> = document - .pages - .iter() - .map(|page| render(&page.frame, pixel_per_pt, frame_fill)) - .collect(); + let pixmaps: Vec<_> = + document.pages.iter().map(|page| render(page, pixel_per_pt)).collect(); let gap = (pixel_per_pt * gap.to_f32()).round() as u32; let pxw = pixmaps.iter().map(sk::Pixmap::width).max().unwrap_or_default(); @@ -53,7 +57,9 @@ pub fn render_merged( + gap * pixmaps.len().saturating_sub(1) as u32; let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap(); - canvas.fill(paint::to_sk_color(gap_fill)); + if let Some(fill) = fill { + canvas.fill(paint::to_sk_color(fill)); + } let mut y = 0; for pixmap in pixmaps { diff --git a/crates/typst-render/src/shape.rs b/crates/typst-render/src/shape.rs index 360c2a4f8..f31262eff 100644 --- a/crates/typst-render/src/shape.rs +++ b/crates/typst-render/src/shape.rs @@ -1,7 +1,8 @@ use tiny_skia as sk; use typst::layout::{Abs, Axes, Point, Ratio, Size}; use typst::visualize::{ - DashPattern, FixedStroke, Geometry, LineCap, LineJoin, Path, PathItem, Shape, + DashPattern, FillRule, FixedStroke, Geometry, LineCap, LineJoin, Path, PathItem, + Shape, }; use crate::{paint, AbsExt, State}; @@ -51,7 +52,10 @@ pub fn render_shape(canvas: &mut sk::Pixmap, state: State, shape: &Shape) -> Opt paint.anti_alias = false; } - let rule = sk::FillRule::default(); + let rule = match shape.fill_rule { + FillRule::NonZero => sk::FillRule::Winding, + FillRule::EvenOdd => sk::FillRule::EvenOdd, + }; canvas.fill_path(&path, &paint, rule, ts, state.mask); } diff --git a/crates/typst-svg/src/lib.rs b/crates/typst-svg/src/lib.rs index 01ed3faee..145e23f86 100644 --- a/crates/typst-svg/src/lib.rs +++ b/crates/typst-svg/src/lib.rs @@ -11,11 +11,11 @@ use std::fmt::{self, Display, Formatter, Write}; use ecow::EcoString; use ttf_parser::OutlineBuilder; use typst::layout::{ - Abs, Frame, FrameItem, FrameKind, GroupItem, Point, Ratio, Size, Transform, + Abs, Frame, FrameItem, FrameKind, GroupItem, Page, Point, Ratio, Size, Transform, }; use typst::model::Document; use typst::utils::hash128; -use typst::visualize::{Gradient, Pattern}; +use typst::visualize::{Geometry, Gradient, Pattern}; use xmlwriter::XmlWriter; use crate::paint::{GradientRef, PatternRef, SVGSubGradient}; @@ -23,12 +23,12 @@ use crate::text::RenderedGlyph; /// Export a frame into a SVG file. #[typst_macros::time(name = "svg")] -pub fn svg(frame: &Frame) -> String { +pub fn svg(page: &Page) -> String { let mut renderer = SVGRenderer::new(); - renderer.write_header(frame.size()); + renderer.write_header(page.frame.size()); - let state = State::new(frame.size(), Transform::identity()); - renderer.render_frame(state, Transform::identity(), frame); + let state = State::new(page.frame.size(), Transform::identity()); + renderer.render_page(state, Transform::identity(), page); renderer.finalize() } @@ -57,7 +57,7 @@ pub fn svg_merged(document: &Document, padding: Abs) -> String { for page in &document.pages { let ts = Transform::translate(x, y); let state = State::new(page.frame.size(), Transform::identity()); - renderer.render_frame(state, ts, &page.frame); + renderer.render_page(state, ts, page); y += page.frame.height() + padding; } @@ -176,6 +176,16 @@ impl SVGRenderer { self.xml.write_attribute("xmlns:h5", "http://www.w3.org/1999/xhtml"); } + /// Render a page with the given transform. + fn render_page(&mut self, state: State, ts: Transform, page: &Page) { + if let Some(fill) = page.fill_or_white() { + let shape = Geometry::Rect(page.frame.size()).filled(fill); + self.render_shape(state, &shape); + } + + self.render_frame(state, ts, &page.frame); + } + /// Render a frame with the given transform. fn render_frame(&mut self, state: State, ts: Transform, frame: &Frame) { self.xml.start_element("g"); diff --git a/crates/typst-svg/src/paint.rs b/crates/typst-svg/src/paint.rs index a382bd9d0..364cdd234 100644 --- a/crates/typst-svg/src/paint.rs +++ b/crates/typst-svg/src/paint.rs @@ -5,7 +5,7 @@ use ttf_parser::OutlineBuilder; use typst::foundations::Repr; use typst::layout::{Angle, Axes, Frame, Quadrant, Ratio, Size, Transform}; use typst::utils::hash128; -use typst::visualize::{Color, Gradient, Paint, Pattern, RatioOrAngle}; +use typst::visualize::{Color, FillRule, Gradient, Paint, Pattern, RatioOrAngle}; use xmlwriter::XmlWriter; use crate::{Id, SVGRenderer, State, SvgMatrix, SvgPathBuilder}; @@ -31,7 +31,13 @@ impl SVGRenderer { } /// Write a fill attribute. - pub(super) fn write_fill(&mut self, fill: &Paint, size: Size, ts: Transform) { + pub(super) fn write_fill( + &mut self, + fill: &Paint, + fill_rule: FillRule, + size: Size, + ts: Transform, + ) { match fill { Paint::Solid(color) => self.xml.write_attribute("fill", &color.encode()), Paint::Gradient(gradient) => { @@ -43,6 +49,10 @@ impl SVGRenderer { self.xml.write_attribute_fmt("fill", format_args!("url(#{id})")); } } + match fill_rule { + FillRule::NonZero => self.xml.write_attribute("fill-rule", "nonzero"), + FillRule::EvenOdd => self.xml.write_attribute("fill-rule", "evenodd"), + } } /// Pushes a gradient to the list of gradients to write SVG file. diff --git a/crates/typst-svg/src/shape.rs b/crates/typst-svg/src/shape.rs index 4caae2fdc..12be2e22d 100644 --- a/crates/typst-svg/src/shape.rs +++ b/crates/typst-svg/src/shape.rs @@ -17,6 +17,7 @@ impl SVGRenderer { if let Some(paint) = &shape.fill { self.write_fill( paint, + shape.fill_rule, self.shape_fill_size(state, paint, shape), self.shape_paint_transform(state, paint, shape), ); diff --git a/crates/typst-svg/src/text.rs b/crates/typst-svg/src/text.rs index 04b75123b..6af933988 100644 --- a/crates/typst-svg/src/text.rs +++ b/crates/typst-svg/src/text.rs @@ -6,7 +6,7 @@ use ttf_parser::GlyphId; use typst::layout::{Abs, Point, Ratio, Size, Transform}; use typst::text::{Font, TextItem}; use typst::utils::hash128; -use typst::visualize::{Image, Paint, RasterFormat, RelativeTo}; +use typst::visualize::{FillRule, Image, Paint, RasterFormat, RelativeTo}; use crate::{SVGRenderer, State, SvgMatrix, SvgPathBuilder}; @@ -138,6 +138,7 @@ impl SVGRenderer { self.xml.write_attribute_fmt("x", format_args!("{x_offset}")); self.write_fill( &text.fill, + FillRule::default(), Size::new(Abs::pt(width), Abs::pt(height)), self.text_paint_transform(state, &text.fill), ); diff --git a/crates/typst/src/foundations/func.rs b/crates/typst/src/foundations/func.rs index dc5ea8dfc..4cc4a6bab 100644 --- a/crates/typst/src/foundations/func.rs +++ b/crates/typst/src/foundations/func.rs @@ -81,6 +81,10 @@ pub use typst_macros::func; /// body evaluates to the result of joining all expressions preceding the /// `return`. /// +/// Functions that don't return any meaningful value return [`none`] instead. +/// The return type of such functions is not explicitly specified in the +/// documentation. (An example of this is [`array.push`]). +/// /// ```example /// #let alert(body, fill: red) = { /// set text(white) diff --git a/crates/typst/src/foundations/int.rs b/crates/typst/src/foundations/int.rs index 40f896188..eb9246494 100644 --- a/crates/typst/src/foundations/int.rs +++ b/crates/typst/src/foundations/int.rs @@ -3,7 +3,9 @@ use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError use ecow::{eco_format, EcoString}; use crate::diag::StrResult; -use crate::foundations::{cast, func, repr, scope, ty, Repr, Str, Value}; +use crate::foundations::{ + bail, cast, func, repr, scope, ty, Bytes, Cast, Repr, Str, Value, +}; /// A whole number. /// @@ -145,7 +147,6 @@ impl i64 { #[func(title = "Bitwise Left Shift")] pub fn bit_lshift( self, - /// The amount of bits to shift. Must not be negative. shift: u32, ) -> StrResult { @@ -168,7 +169,6 @@ impl i64 { #[func(title = "Bitwise Right Shift")] pub fn bit_rshift( self, - /// The amount of bits to shift. Must not be negative. /// /// Shifts larger than 63 are allowed and will cause the return value to @@ -178,7 +178,6 @@ impl i64 { /// just applying this operation multiple times. Therefore, the shift will /// always succeed. shift: u32, - /// Toggles whether a logical (unsigned) right shift should be performed /// instead of arithmetic right shift. /// If this is `true`, negative operands will not preserve their sign bit, @@ -214,6 +213,126 @@ impl i64 { self >> shift } } + + /// Converts bytes to an integer. + /// + /// ```example + /// #int.from-bytes(bytes((0, 0, 0, 0, 0, 0, 0, 1))) \ + /// #int.from-bytes(bytes((1, 0, 0, 0, 0, 0, 0, 0)), endian: "big") + /// ``` + #[func] + pub fn from_bytes( + /// The bytes that should be converted to an integer. + /// + /// Must be of length at most 8 so that the result fits into a 64-bit + /// signed integer. + bytes: Bytes, + /// The endianness of the conversion. + #[named] + #[default(Endianness::Little)] + endian: Endianness, + /// Whether the bytes should be treated as a signed integer. If this is + /// `{true}` and the most significant bit is set, the resulting number + /// will negative. + #[named] + #[default(true)] + signed: bool, + ) -> StrResult { + let len = bytes.len(); + if len == 0 { + return Ok(0); + } else if len > 8 { + bail!("too many bytes to convert to a 64 bit number"); + } + + // `decimal` will hold the part of the buffer that should be filled with + // the input bytes, `rest` will remain as is or be filled with 0xFF for + // negative numbers if signed is true. + // + // – big-endian: `decimal` will be the rightmost bytes of the buffer. + // - little-endian: `decimal` will be the leftmost bytes of the buffer. + let mut buf = [0u8; 8]; + let (rest, decimal) = match endian { + Endianness::Big => buf.split_at_mut(8 - len), + Endianness::Little => { + let (first, second) = buf.split_at_mut(len); + (second, first) + } + }; + + decimal.copy_from_slice(bytes.as_ref()); + + // Perform sign-extension if necessary. + if signed { + let most_significant_byte = match endian { + Endianness::Big => decimal[0], + Endianness::Little => decimal[len - 1], + }; + + if most_significant_byte & 0b1000_0000 != 0 { + rest.fill(0xFF); + } + } + + Ok(match endian { + Endianness::Big => i64::from_be_bytes(buf), + Endianness::Little => i64::from_le_bytes(buf), + }) + } + + /// Converts an integer to bytes. + /// + /// ```example + /// #array(10000.to-bytes(endian: "big")) \ + /// #array(10000.to-bytes(size: 4)) + /// ``` + #[func] + pub fn to_bytes( + self, + /// The endianness of the conversion. + #[named] + #[default(Endianness::Little)] + endian: Endianness, + /// The size in bytes of the resulting bytes (must be at least zero). If + /// the integer is too large to fit in the specified size, the + /// conversion will truncate the remaining bytes based on the + /// endianness. To keep the same resulting value, if the endianness is + /// big-endian, the truncation will happen at the rightmost bytes. + /// Otherwise, if the endianness is little-endian, the truncation will + /// happen at the leftmost bytes. + /// + /// Be aware that if the integer is negative and the size is not enough + /// to make the number fit, when passing the resulting bytes to + /// `int.from-bytes`, the resulting number might be positive, as the + /// most significant bit might not be set to 1. + #[named] + #[default(8)] + size: usize, + ) -> Bytes { + let array = match endian { + Endianness::Big => self.to_be_bytes(), + Endianness::Little => self.to_le_bytes(), + }; + + let mut buf = vec![0u8; size]; + match endian { + Endianness::Big => { + // Copy the bytes from the array to the buffer, starting from + // the end of the buffer. + let buf_start = size.saturating_sub(8); + let array_start = 8usize.saturating_sub(size); + buf[buf_start..].copy_from_slice(&array[array_start..]) + } + Endianness::Little => { + // Copy the bytes from the array to the buffer, starting from + // the beginning of the buffer. + let end = size.min(8); + buf[..end].copy_from_slice(&array[..end]) + } + } + + Bytes::from(buf) + } } impl Repr for i64 { @@ -222,6 +341,15 @@ impl Repr for i64 { } } +/// Represents the byte order used for converting integers to bytes and vice versa. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum Endianness { + /// Big-endian byte order: the highest-value byte is at the beginning of the bytes. + Big, + /// Little-endian byte order: the lowest-value byte is at the beginning of the bytes. + Little, +} + /// A value that can be cast to an integer. pub struct ToInt(i64); diff --git a/crates/typst/src/foundations/methods.rs b/crates/typst/src/foundations/methods.rs index 287a49c69..945b7c507 100644 --- a/crates/typst/src/foundations/methods.rs +++ b/crates/typst/src/foundations/methods.rs @@ -1,28 +1,9 @@ //! Handles special built-in methods on values. use crate::diag::{At, SourceResult}; -use crate::foundations::{Args, Array, Dict, Str, Type, Value}; +use crate::foundations::{Args, Str, Type, Value}; use crate::syntax::Span; -/// List the available methods for a type and whether they take arguments. -pub fn mutable_methods_on(ty: Type) -> &'static [(&'static str, bool)] { - if ty == Type::of::() { - &[ - ("first", false), - ("last", false), - ("at", true), - ("pop", false), - ("push", true), - ("insert", true), - ("remove", true), - ] - } else if ty == Type::of::() { - &[("at", true), ("insert", true), ("remove", true)] - } else { - &[] - } -} - /// Whether a specific method is mutating. pub(crate) fn is_mutating_method(method: &str) -> bool { matches!(method, "push" | "pop" | "insert" | "remove") diff --git a/crates/typst/src/foundations/mod.rs b/crates/typst/src/foundations/mod.rs index b7783dda9..f9dfff4f5 100644 --- a/crates/typst/src/foundations/mod.rs +++ b/crates/typst/src/foundations/mod.rs @@ -49,7 +49,7 @@ pub use self::float::*; pub use self::func::*; pub use self::int::*; pub use self::label::*; -pub use self::methods::*; +pub(crate) use self::methods::*; pub use self::module::*; pub use self::none::*; pub use self::plugin::*; diff --git a/crates/typst/src/foundations/styles.rs b/crates/typst/src/foundations/styles.rs index 42be89229..48009c8c3 100644 --- a/crates/typst/src/foundations/styles.rs +++ b/crates/typst/src/foundations/styles.rs @@ -7,7 +7,7 @@ use comemo::{Track, Tracked}; use ecow::{eco_vec, EcoString, EcoVec}; use smallvec::SmallVec; -use crate::diag::{SourceResult, Trace, Tracepoint}; +use crate::diag::{warning, SourceResult, Trace, Tracepoint}; use crate::engine::Engine; use crate::foundations::{ cast, elem, func, ty, Content, Context, Element, Func, NativeElement, Packed, Repr, @@ -33,6 +33,8 @@ use crate::utils::LazyHash; /// ``` #[func] pub fn style( + /// The engine. + engine: &mut Engine, /// The call site span. span: Span, /// A function to call with the styles. Its return value is displayed @@ -43,6 +45,11 @@ pub fn style( /// content that depends on the style context it appears in. func: Func, ) -> Content { + engine.sink.warn(warning!( + span, "`style` is deprecated"; + hint: "use a `context` expression instead" + )); + StyleElem::new(func).pack().spanned(span) } diff --git a/crates/typst/src/introspection/counter.rs b/crates/typst/src/introspection/counter.rs index 13ea4d142..2aefb68ec 100644 --- a/crates/typst/src/introspection/counter.rs +++ b/crates/typst/src/introspection/counter.rs @@ -5,7 +5,7 @@ use comemo::{Track, Tracked, TrackedMut}; use ecow::{eco_format, eco_vec, EcoString, EcoVec}; use smallvec::{smallvec, SmallVec}; -use crate::diag::{bail, At, HintedStrResult, SourceResult}; +use crate::diag::{bail, warning, At, HintedStrResult, SourceResult}; use crate::engine::{Engine, Route, Sink, Traced}; use crate::foundations::{ cast, elem, func, scope, select_where, ty, Args, Array, Construct, Content, Context, @@ -464,6 +464,11 @@ impl Counter { if let Ok(loc) = context.location() { self.display_impl(engine, loc, numbering, both, context.styles().ok()) } else { + engine.sink.warn(warning!( + span, "`counter.display` without context is deprecated"; + hint: "use it in a `context` expression instead" + )); + Ok(CounterDisplayElem::new(self, numbering, both) .pack() .spanned(span) @@ -508,13 +513,19 @@ impl Counter { context: Tracked, /// The callsite span. span: Span, - /// _Compatibility:_ This argument only exists for compatibility with - /// Typst 0.10 and lower and shouldn't be used anymore. + /// _Compatibility:_ This argument is deprecated. It only exists for + /// compatibility with Typst 0.10 and lower and shouldn't be used + /// anymore. #[default] location: Option, ) -> SourceResult { if location.is_none() { context.location().at(span)?; + } else { + engine.sink.warn(warning!( + span, "calling `counter.final` with a location is deprecated"; + hint: "try removing the location argument" + )); } let sequence = self.sequence(engine)?; diff --git a/crates/typst/src/introspection/locate.rs b/crates/typst/src/introspection/locate.rs index 373b1fe27..8991ae9be 100644 --- a/crates/typst/src/introspection/locate.rs +++ b/crates/typst/src/introspection/locate.rs @@ -1,6 +1,6 @@ use comemo::{Track, Tracked}; -use crate::diag::{HintedStrResult, SourceResult}; +use crate::diag::{warning, HintedStrResult, SourceResult}; use crate::engine::Engine; use crate::foundations::{ cast, elem, func, Content, Context, Func, LocatableSelector, NativeElement, Packed, @@ -29,11 +29,12 @@ use crate::syntax::Span; /// /// # Compatibility /// In Typst 0.10 and lower, the `locate` function took a closure that made the -/// current location in the document available (like [`here`] does now). -/// Compatibility with the old way will remain for a while to give package -/// authors time to upgrade. To that effect, `locate` detects whether it -/// received a selector or a user-defined function and adjusts its semantics -/// accordingly. This behaviour will be removed in the future. +/// current location in the document available (like [`here`] does now). This +/// usage pattern is deprecated. Compatibility with the old way will remain for +/// a while to give package authors time to upgrade. To that effect, `locate` +/// detects whether it received a selector or a user-defined function and +/// adjusts its semantics accordingly. This behaviour will be removed in the +/// future. #[func(contextual)] pub fn locate( /// The engine. @@ -56,6 +57,11 @@ pub fn locate( LocateOutput::Location(selector.resolve_unique(engine.introspector, context)?) } LocateInput::Func(func) => { + engine.sink.warn(warning!( + span, "`locate` with callback function is deprecated"; + hint: "use a `context` expression instead" + )); + LocateOutput::Content(LocateElem::new(func).pack().spanned(span)) } }) diff --git a/crates/typst/src/introspection/query.rs b/crates/typst/src/introspection/query.rs index e416bfc9d..07f761a8d 100644 --- a/crates/typst/src/introspection/query.rs +++ b/crates/typst/src/introspection/query.rs @@ -1,9 +1,10 @@ use comemo::Tracked; -use crate::diag::HintedStrResult; +use crate::diag::{warning, HintedStrResult}; use crate::engine::Engine; use crate::foundations::{func, Array, Context, LocatableSelector, Value}; use crate::introspection::Location; +use crate::syntax::Span; /// Finds elements in the document. /// @@ -141,6 +142,8 @@ pub fn query( engine: &mut Engine, /// The callsite context. context: Tracked, + /// The span of the `query` call. + span: Span, /// Can be /// - an element function like a `heading` or `figure`, /// - a `{