From 0a374d238016c0101d11cbc3f4bc621f3895ad36 Mon Sep 17 00:00:00 2001 From: Niklas Eicker Date: Wed, 8 Jan 2025 10:38:34 +0100 Subject: [PATCH 01/10] Embed files associated with the document as a whole (#5221) Co-authored-by: Laurenz --- crates/typst-cli/src/args.rs | 3 + crates/typst-cli/src/compile.rs | 1 + crates/typst-library/src/lib.rs | 2 + crates/typst-library/src/pdf/embed.rs | 131 ++++++++++++++++++++++++++ crates/typst-library/src/pdf/mod.rs | 24 +++++ crates/typst-pdf/src/catalog.rs | 84 +++++++++++------ crates/typst-pdf/src/embed.rs | 122 ++++++++++++++++++++++++ crates/typst-pdf/src/lib.rs | 46 +++++++-- docs/src/lib.rs | 2 + tests/suite/pdf/embed.typ | 30 ++++++ 10 files changed, 411 insertions(+), 34 deletions(-) create mode 100644 crates/typst-library/src/pdf/embed.rs create mode 100644 crates/typst-library/src/pdf/mod.rs create mode 100644 crates/typst-pdf/src/embed.rs create mode 100644 tests/suite/pdf/embed.typ diff --git a/crates/typst-cli/src/args.rs b/crates/typst-cli/src/args.rs index 83c4c8f9e..d6855d100 100644 --- a/crates/typst-cli/src/args.rs +++ b/crates/typst-cli/src/args.rs @@ -473,6 +473,9 @@ pub enum PdfStandard { /// PDF/A-2b. #[value(name = "a-2b")] A_2b, + /// PDF/A-3b. + #[value(name = "a-3b")] + A_3b, } display_possible_values!(PdfStandard); diff --git a/crates/typst-cli/src/compile.rs b/crates/typst-cli/src/compile.rs index adeef0f2d..515a777a2 100644 --- a/crates/typst-cli/src/compile.rs +++ b/crates/typst-cli/src/compile.rs @@ -136,6 +136,7 @@ impl CompileConfig { .map(|standard| match standard { PdfStandard::V_1_7 => typst_pdf::PdfStandard::V_1_7, PdfStandard::A_2b => typst_pdf::PdfStandard::A_2b, + PdfStandard::A_3b => typst_pdf::PdfStandard::A_3b, }) .collect::>(); PdfStandards::new(&list)? diff --git a/crates/typst-library/src/lib.rs b/crates/typst-library/src/lib.rs index 87b2fcb44..2ea77eaa5 100644 --- a/crates/typst-library/src/lib.rs +++ b/crates/typst-library/src/lib.rs @@ -21,6 +21,7 @@ pub mod layout; pub mod loading; pub mod math; pub mod model; +pub mod pdf; pub mod routines; pub mod symbols; pub mod text; @@ -249,6 +250,7 @@ fn global(math: Module, inputs: Dict, features: &Features) -> Module { self::introspection::define(&mut global); self::loading::define(&mut global); self::symbols::define(&mut global); + self::pdf::define(&mut global); global.reset_category(); if features.is_enabled(Feature::Html) { global.define_module(self::html::module()); diff --git a/crates/typst-library/src/pdf/embed.rs b/crates/typst-library/src/pdf/embed.rs new file mode 100644 index 000000000..db4986225 --- /dev/null +++ b/crates/typst-library/src/pdf/embed.rs @@ -0,0 +1,131 @@ +use ecow::EcoString; +use typst_syntax::{Span, Spanned}; + +use crate::diag::{At, SourceResult, StrResult}; +use crate::engine::Engine; +use crate::foundations::{ + elem, func, scope, Cast, Content, NativeElement, Packed, Show, StyleChain, +}; +use crate::introspection::Locatable; +use crate::loading::Readable; +use crate::World; + +/// A file that will be embedded into the output PDF. +/// +/// This can be used to distribute additional files that are related to the PDF +/// within it. PDF readers will display the files in a file listing. +/// +/// Some international standards use this mechanism to embed machine-readable +/// data (e.g., ZUGFeRD/Factur-X for invoices) that mirrors the visual content +/// of the PDF. +/// +/// # Example +/// ```typ +/// #pdf.embed( +/// "experiment.csv", +/// relationship: "supplement", +/// mime-type: "text/csv", +/// description: "Raw Oxygen readings from the Arctic experiment", +/// ) +/// ``` +/// +/// # Notes +/// - This element is ignored if exporting to a format other than PDF. +/// - File embeddings are not currently supported for PDF/A-2, even if the +/// embedded file conforms to PDF/A-1 or PDF/A-2. +#[elem(scope, Show, Locatable)] +pub struct EmbedElem { + /// Path to a file to be embedded. + /// + /// For more details, see the [Paths section]($syntax/#paths). + #[required] + #[parse( + let Spanned { v: path, span } = + args.expect::>("path to the file to be embedded")?; + let id = span.resolve_path(&path).at(span)?; + let data = engine.world.file(id).at(span)?; + path + )] + #[borrowed] + pub path: EcoString, + + /// The resolved project-relative path. + #[internal] + #[required] + #[parse(id.vpath().as_rootless_path().to_string_lossy().replace("\\", "/").into())] + pub resolved_path: EcoString, + + /// The raw file data. + #[internal] + #[required] + #[parse(Readable::Bytes(data))] + pub data: Readable, + + /// The relationship of the embedded file to the document. + /// + /// Ignored if export doesn't target PDF/A-3. + pub relationship: Option, + + /// The MIME type of the embedded file. + #[borrowed] + pub mime_type: Option, + + /// A description for the embedded file. + #[borrowed] + pub description: Option, +} + +#[scope] +impl EmbedElem { + /// Decode a file embedding from bytes or a string. + #[func(title = "Embed Data")] + fn decode( + /// The call span of this function. + span: Span, + /// The path that will be written into the PDF. Typst will not read from + /// this path since the data is provided in the following argument. + path: EcoString, + /// The data to embed as a file. + data: Readable, + /// The relationship of the embedded file to the document. + #[named] + relationship: Option>, + /// The MIME type of the embedded file. + #[named] + mime_type: Option>, + /// A description for the embedded file. + #[named] + description: Option>, + ) -> StrResult { + let mut elem = EmbedElem::new(path.clone(), path, data); + if let Some(description) = description { + elem.push_description(description); + } + if let Some(mime_type) = mime_type { + elem.push_mime_type(mime_type); + } + if let Some(relationship) = relationship { + elem.push_relationship(relationship); + } + Ok(elem.pack().spanned(span)) + } +} + +impl Show for Packed { + fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { + Ok(Content::empty()) + } +} + +/// The relationship of an embedded file with the document. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)] +pub enum EmbeddedFileRelationship { + /// The PDF document was created from the source file. + Source, + /// The file was used to derive a visual presentation in the PDF. + Data, + /// An alternative representation of the document. + Alternative, + /// Additional resources for the document. + Supplement, +} diff --git a/crates/typst-library/src/pdf/mod.rs b/crates/typst-library/src/pdf/mod.rs new file mode 100644 index 000000000..669835d4c --- /dev/null +++ b/crates/typst-library/src/pdf/mod.rs @@ -0,0 +1,24 @@ +//! PDF-specific functionality. + +mod embed; + +pub use self::embed::*; + +use crate::foundations::{category, Category, Module, Scope}; + +/// PDF-specific functionality. +#[category] +pub static PDF: Category; + +/// Hook up the `pdf` module. +pub(super) fn define(global: &mut Scope) { + global.category(PDF); + global.define_module(module()); +} + +/// Hook up all `pdf` definitions. +pub fn module() -> Module { + let mut scope = Scope::deduplicating(); + scope.define_elem::(); + Module::new("pdf", scope) +} diff --git a/crates/typst-pdf/src/catalog.rs b/crates/typst-pdf/src/catalog.rs index c4b0e2e83..709b01553 100644 --- a/crates/typst-pdf/src/catalog.rs +++ b/crates/typst-pdf/src/catalog.rs @@ -12,7 +12,7 @@ use typst_syntax::Span; use xmp_writer::{DateTime, LangId, RenditionClass, XmpWriter}; use crate::page::PdfPageLabel; -use crate::{hash_base64, outline, TextStrExt, Timezone, WithEverything}; +use crate::{hash_base64, outline, TextStrExt, Timestamp, Timezone, WithEverything}; /// Write the document catalog. pub fn write_catalog( @@ -86,23 +86,10 @@ pub fn write_catalog( info.keywords(TextStr::trimmed(&joined)); xmp.pdf_keywords(&joined); } - - // (1) If the `document.date` is set to specific `datetime` or `none`, use it. - // (2) If the `document.date` is set to `auto` or not set, try to use the - // date from the options. - // (3) Otherwise, we don't write date metadata. - let (date, tz) = match (ctx.document.info.date, ctx.options.timestamp) { - (Smart::Custom(date), _) => (date, None), - (Smart::Auto, Some(timestamp)) => { - (Some(timestamp.datetime), Some(timestamp.timezone)) - } - _ => (None, None), - }; - if let Some(date) = date { - if let Some(pdf_date) = pdf_date(date, tz) { - info.creation_date(pdf_date); - info.modified_date(pdf_date); - } + let (date, tz) = document_date(ctx.document.info.date, ctx.options.timestamp); + if let Some(pdf_date) = date.and_then(|date| pdf_date(date, tz)) { + info.creation_date(pdf_date); + info.modified_date(pdf_date); } info.finish(); @@ -154,7 +141,7 @@ pub fn write_catalog( } // Assert dominance. - if ctx.options.standards.pdfa { + if let Some((part, conformance)) = ctx.options.standards.pdfa_part { let mut extension_schemas = xmp.extension_schemas(); extension_schemas .xmp_media_management() @@ -162,8 +149,8 @@ pub fn write_catalog( .describe_instance_id(); extension_schemas.pdf().properties().describe_all(); extension_schemas.finish(); - xmp.pdfa_part(2); - xmp.pdfa_conformance("B"); + xmp.pdfa_part(part); + xmp.pdfa_conformance(conformance); } let xmp_buf = xmp.finish(None); @@ -182,13 +169,35 @@ pub fn write_catalog( catalog.viewer_preferences().direction(dir); catalog.metadata(meta_ref); - // Write the named destination tree if there are any entries. - if !ctx.references.named_destinations.dests.is_empty() { + let has_dests = !ctx.references.named_destinations.dests.is_empty(); + let has_embeddings = !ctx.references.embedded_files.is_empty(); + + // Write the `/Names` dictionary. + if has_dests || has_embeddings { + // Write the named destination tree if there are any entries. let mut name_dict = catalog.names(); - let mut dests_name_tree = name_dict.destinations(); - let mut names = dests_name_tree.names(); - for &(name, dest_ref, ..) in &ctx.references.named_destinations.dests { - names.insert(Str(name.resolve().as_bytes()), dest_ref); + if has_dests { + let mut dests_name_tree = name_dict.destinations(); + let mut names = dests_name_tree.names(); + for &(name, dest_ref, ..) in &ctx.references.named_destinations.dests { + names.insert(Str(name.resolve().as_bytes()), dest_ref); + } + } + + if has_embeddings { + let mut embedded_files = name_dict.embedded_files(); + let mut names = embedded_files.names(); + for (name, file_ref) in &ctx.references.embedded_files { + names.insert(Str(name.as_bytes()), *file_ref); + } + } + } + + if has_embeddings && ctx.options.standards.pdfa { + // PDF 2.0, but ISO 19005-3 (PDF/A-3) Annex E allows it for PDF/A-3. + let mut associated_files = catalog.insert(Name(b"AF")).array().typed(); + for (_, file_ref) in ctx.references.embedded_files { + associated_files.item(file_ref).finish(); } } @@ -289,8 +298,27 @@ pub(crate) fn write_page_labels( result } +/// Resolve the document date. +/// +/// (1) If the `document.date` is set to specific `datetime` or `none`, use it. +/// (2) If the `document.date` is set to `auto` or not set, try to use the +/// date from the options. +/// (3) Otherwise, we don't write date metadata. +pub fn document_date( + document_date: Smart>, + timestamp: Option, +) -> (Option, Option) { + match (document_date, timestamp) { + (Smart::Custom(date), _) => (date, None), + (Smart::Auto, Some(timestamp)) => { + (Some(timestamp.datetime), Some(timestamp.timezone)) + } + _ => (None, None), + } +} + /// Converts a datetime to a pdf-writer date. -fn pdf_date(datetime: Datetime, tz: Option) -> Option { +pub fn pdf_date(datetime: Datetime, tz: Option) -> Option { let year = datetime.year().filter(|&y| y >= 0)? as u16; let mut pdf_date = pdf_writer::Date::new(year); diff --git a/crates/typst-pdf/src/embed.rs b/crates/typst-pdf/src/embed.rs new file mode 100644 index 000000000..b32f6e45d --- /dev/null +++ b/crates/typst-pdf/src/embed.rs @@ -0,0 +1,122 @@ +use std::collections::BTreeMap; + +use ecow::EcoString; +use pdf_writer::types::AssociationKind; +use pdf_writer::{Filter, Finish, Name, Ref, Str, TextStr}; +use typst_library::diag::{bail, SourceResult}; +use typst_library::foundations::{NativeElement, Packed, StyleChain}; +use typst_library::pdf::{EmbedElem, EmbeddedFileRelationship}; + +use crate::catalog::{document_date, pdf_date}; +use crate::{deflate, NameExt, PdfChunk, StrExt, WithGlobalRefs}; + +/// Query for all [`EmbedElem`] and write them and their file specifications. +/// +/// This returns a map of embedding names and references so that we can later +/// add them to the catalog's `/Names` dictionary. +pub fn write_embedded_files( + ctx: &WithGlobalRefs, +) -> SourceResult<(PdfChunk, BTreeMap)> { + let mut chunk = PdfChunk::new(); + let mut embedded_files = BTreeMap::default(); + + let elements = ctx.document.introspector.query(&EmbedElem::elem().select()); + for elem in &elements { + if !ctx.options.standards.embedded_files { + // PDF/A-2 requires embedded files to be PDF/A-1 or PDF/A-2, + // which we don't currently check. + bail!( + elem.span(), + "file embeddings are not currently supported for PDF/A-2"; + hint: "PDF/A-3 supports arbitrary embedded files" + ); + } + + let embed = elem.to_packed::().unwrap(); + if embed.resolved_path.len() > Str::PDFA_LIMIT { + bail!(embed.span(), "embedded file path is too long"); + } + + let id = embed_file(ctx, &mut chunk, embed)?; + if embedded_files.insert(embed.resolved_path.clone(), id).is_some() { + bail!( + elem.span(), + "duplicate embedded file for path `{}`", embed.resolved_path; + hint: "embedded file paths must be unique", + ); + } + } + + Ok((chunk, embedded_files)) +} + +/// Write the embedded file stream and its file specification. +fn embed_file( + ctx: &WithGlobalRefs, + chunk: &mut PdfChunk, + embed: &Packed, +) -> SourceResult { + let embedded_file_stream_ref = chunk.alloc.bump(); + let file_spec_dict_ref = chunk.alloc.bump(); + + let data = embed.data().as_slice(); + let compressed = deflate(data); + + let mut embedded_file = chunk.embedded_file(embedded_file_stream_ref, &compressed); + embedded_file.filter(Filter::FlateDecode); + + if let Some(mime_type) = embed.mime_type(StyleChain::default()) { + if mime_type.len() > Name::PDFA_LIMIT { + bail!(embed.span(), "embedded file MIME type is too long"); + } + embedded_file.subtype(Name(mime_type.as_bytes())); + } else if ctx.options.standards.pdfa { + bail!(embed.span(), "embedded files must have a MIME type in PDF/A-3"); + } + + let mut params = embedded_file.params(); + params.size(data.len() as i32); + + let (date, tz) = document_date(ctx.document.info.date, ctx.options.timestamp); + if let Some(pdf_date) = date.and_then(|date| pdf_date(date, tz)) { + params.modification_date(pdf_date); + } else if ctx.options.standards.pdfa { + bail!( + embed.span(), + "the document must have a date when embedding files in PDF/A-3"; + hint: "`set document(date: none)` must not be used in this case" + ); + } + + params.finish(); + embedded_file.finish(); + + let mut file_spec = chunk.file_spec(file_spec_dict_ref); + file_spec.path(Str(embed.resolved_path.as_bytes())); + file_spec.unic_file(TextStr(&embed.resolved_path)); + file_spec + .insert(Name(b"EF")) + .dict() + .pair(Name(b"F"), embedded_file_stream_ref) + .pair(Name(b"UF"), embedded_file_stream_ref); + + if ctx.options.standards.pdfa { + // PDF 2.0, but ISO 19005-3 (PDF/A-3) Annex E allows it for PDF/A-3. + file_spec.association_kind(match embed.relationship(StyleChain::default()) { + Some(EmbeddedFileRelationship::Source) => AssociationKind::Source, + Some(EmbeddedFileRelationship::Data) => AssociationKind::Data, + Some(EmbeddedFileRelationship::Alternative) => AssociationKind::Alternative, + Some(EmbeddedFileRelationship::Supplement) => AssociationKind::Supplement, + None => AssociationKind::Unspecified, + }); + } + + if let Some(description) = embed.description(StyleChain::default()) { + if description.len() > Str::PDFA_LIMIT { + bail!(embed.span(), "embedded file description is too long"); + } + file_spec.description(TextStr(description)); + } + + Ok(file_spec_dict_ref) +} diff --git a/crates/typst-pdf/src/lib.rs b/crates/typst-pdf/src/lib.rs index f45c62bb5..88e62389c 100644 --- a/crates/typst-pdf/src/lib.rs +++ b/crates/typst-pdf/src/lib.rs @@ -4,6 +4,7 @@ mod catalog; mod color; mod color_font; mod content; +mod embed; mod extg; mod font; mod gradient; @@ -14,12 +15,13 @@ mod page; mod resources; mod tiling; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; use std::fmt::{self, Debug, Formatter}; use std::hash::Hash; use std::ops::{Deref, DerefMut}; use base64::Engine; +use ecow::EcoString; use pdf_writer::{Chunk, Name, Pdf, Ref, Str, TextStr}; use serde::{Deserialize, Serialize}; use typst_library::diag::{bail, SourceResult, StrResult}; @@ -33,6 +35,7 @@ use typst_utils::Deferred; use crate::catalog::write_catalog; use crate::color::{alloc_color_functions_refs, ColorFunctionRefs}; use crate::color_font::{write_color_fonts, ColorFontSlice}; +use crate::embed::write_embedded_files; use crate::extg::{write_graphic_states, ExtGState}; use crate::font::write_fonts; use crate::gradient::{write_gradients, PdfGradient}; @@ -67,6 +70,7 @@ pub fn pdf(document: &PagedDocument, options: &PdfOptions) -> SourceResult, } impl PdfStandards { /// Validates a list of PDF standards for compatibility and returns their /// encapsulated representation. pub fn new(list: &[PdfStandard]) -> StrResult { - Ok(Self { pdfa: list.contains(&PdfStandard::A_2b) }) + let a2b = list.contains(&PdfStandard::A_2b); + let a3b = list.contains(&PdfStandard::A_3b); + + if a2b && a3b { + bail!("PDF cannot conform to A-2B and A-3B at the same time") + } + + let pdfa = a2b || a3b; + Ok(Self { + pdfa, + embedded_files: !a2b, + pdfa_part: pdfa.then_some((if a2b { 2 } else { 3 }, "B")), + }) } } @@ -166,10 +188,9 @@ impl Debug for PdfStandards { } } -#[allow(clippy::derivable_impls)] impl Default for PdfStandards { fn default() -> Self { - Self { pdfa: false } + Self { pdfa: false, embedded_files: true, pdfa_part: None } } } @@ -186,6 +207,9 @@ pub enum PdfStandard { /// PDF/A-2b. #[serde(rename = "a-2b")] A_2b, + /// PDF/A-3b. + #[serde(rename = "a-3b")] + A_3b, } /// A struct to build a PDF following a fixed succession of phases. @@ -316,6 +340,8 @@ struct References { tilings: HashMap, /// The IDs of written external graphics states. ext_gs: HashMap, + /// The names and references for embedded files. + embedded_files: BTreeMap, } /// At this point, the references have been assigned to all resources. The page @@ -481,6 +507,14 @@ impl Renumber for HashMap { } } +impl Renumber for BTreeMap { + fn renumber(&mut self, offset: i32) { + for v in self.values_mut() { + v.renumber(offset); + } + } +} + impl Renumber for Option { fn renumber(&mut self, offset: i32) { if let Some(r) = self { diff --git a/docs/src/lib.rs b/docs/src/lib.rs index 5ca3724ab..e92799718 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -25,6 +25,7 @@ use typst::layout::{Abs, Margin, PageElem, PagedDocument, LAYOUT}; use typst::loading::DATA_LOADING; use typst::math::MATH; use typst::model::MODEL; +use typst::pdf::PDF; use typst::symbols::SYMBOLS; use typst::text::{Font, FontBook, TEXT}; use typst::utils::LazyHash; @@ -163,6 +164,7 @@ fn reference_pages(resolver: &dyn Resolver) -> PageModel { category_page(resolver, VISUALIZE), category_page(resolver, INTROSPECTION), category_page(resolver, DATA_LOADING), + category_page(resolver, PDF), ]; page } diff --git a/tests/suite/pdf/embed.typ b/tests/suite/pdf/embed.typ new file mode 100644 index 000000000..bb5c9316c --- /dev/null +++ b/tests/suite/pdf/embed.typ @@ -0,0 +1,30 @@ +// Test file embeddings. The tests here so far are unsatisfactory because we +// have no PDF testing infrastructure. That should be improved in the future. + +--- pdf-embed --- +#pdf.embed("/assets/text/hello.txt") +#pdf.embed( + "/assets/data/details.toml", + relationship: "supplement", + mime-type: "application/toml", + description: "Information about a secret project", +) + +--- pdf-embed-invalid-relationship --- +#pdf.embed( + "/assets/text/hello.txt", + // Error: 17-23 expected "source", "data", "alternative", "supplement", or none + relationship: "test", + mime-type: "text/plain", + description: "A test file", +) + +--- pdf-embed-decode --- +#pdf.embed.decode("hello.txt", read("/assets/text/hello.txt")) +#pdf.embed.decode( + "a_file_name.txt", + read("/assets/text/hello.txt"), + relationship: "supplement", + mime-type: "text/plain", + description: "A description", +) From dacd6acd5e73d35c6e7a7a3b144f16ae70d03daa Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 8 Jan 2025 11:57:56 +0100 Subject: [PATCH 02/10] More flexible and efficient `Bytes` representation (#5670) --- crates/typst-cli/src/world.rs | 2 +- crates/typst-ide/src/tests.rs | 4 +- crates/typst-kit/src/fonts.rs | 8 +- crates/typst-layout/src/image.rs | 2 +- crates/typst-library/src/foundations/bytes.rs | 146 +++++++++++++----- crates/typst-library/src/foundations/float.rs | 12 +- crates/typst-library/src/foundations/int.rs | 5 +- .../typst-library/src/foundations/plugin.rs | 2 +- crates/typst-library/src/foundations/value.rs | 6 +- crates/typst-library/src/loading/cbor.rs | 2 +- crates/typst-library/src/loading/mod.rs | 24 ++- crates/typst-library/src/text/font/color.rs | 13 +- .../src/visualize/image/raster.rs | 2 +- crates/typst-svg/src/text.rs | 5 +- docs/src/html.rs | 2 +- docs/src/lib.rs | 2 +- tests/fuzz/src/compile.rs | 2 +- tests/src/world.rs | 6 +- 18 files changed, 160 insertions(+), 85 deletions(-) diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs index af6cf228f..12e80d273 100644 --- a/crates/typst-cli/src/world.rs +++ b/crates/typst-cli/src/world.rs @@ -305,7 +305,7 @@ impl FileSlot { ) -> FileResult { self.file.get_or_init( || read(self.id, project_root, package_storage), - |data, _| Ok(data.into()), + |data, _| Ok(Bytes::new(data)), ) } } diff --git a/crates/typst-ide/src/tests.rs b/crates/typst-ide/src/tests.rs index f41808dac..6678ab841 100644 --- a/crates/typst-ide/src/tests.rs +++ b/crates/typst-ide/src/tests.rs @@ -55,7 +55,7 @@ impl TestWorld { pub fn with_asset_at(mut self, path: &str, filename: &str) -> Self { let id = FileId::new(None, VirtualPath::new(path)); let data = typst_dev_assets::get_by_name(filename).unwrap(); - let bytes = Bytes::from_static(data); + let bytes = Bytes::new(data); Arc::make_mut(&mut self.files).assets.insert(id, bytes); self } @@ -152,7 +152,7 @@ impl Default for TestBase { fn default() -> Self { let fonts: Vec<_> = typst_assets::fonts() .chain(typst_dev_assets::fonts()) - .flat_map(|data| Font::iter(Bytes::from_static(data))) + .flat_map(|data| Font::iter(Bytes::new(data))) .collect(); Self { diff --git a/crates/typst-kit/src/fonts.rs b/crates/typst-kit/src/fonts.rs index 83e13fd8f..c15d739ec 100644 --- a/crates/typst-kit/src/fonts.rs +++ b/crates/typst-kit/src/fonts.rs @@ -13,6 +13,7 @@ use std::path::{Path, PathBuf}; use std::sync::OnceLock; use fontdb::{Database, Source}; +use typst_library::foundations::Bytes; use typst_library::text::{Font, FontBook, FontInfo}; use typst_timing::TimingScope; @@ -52,9 +53,8 @@ impl FontSlot { .as_ref() .expect("`path` is not `None` if `font` is uninitialized"), ) - .ok()? - .into(); - Font::new(data, self.index) + .ok()?; + Font::new(Bytes::new(data), self.index) }) .clone() } @@ -196,7 +196,7 @@ impl FontSearcher { #[cfg(feature = "embed-fonts")] fn add_embedded(&mut self) { for data in typst_assets::fonts() { - let buffer = typst_library::foundations::Bytes::from_static(data); + let buffer = Bytes::new(data); for (i, font) in Font::iter(buffer).enumerate() { self.book.push(font.info().clone()); self.fonts.push(FontSlot { diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index 77e1d0838..59e2c0210 100644 --- a/crates/typst-layout/src/image.rs +++ b/crates/typst-layout/src/image.rs @@ -50,7 +50,7 @@ pub fn layout_image( // Construct the image itself. let image = Image::with_fonts( - data.clone().into(), + data.clone().into_bytes(), format, elem.alt(styles), engine.world, diff --git a/crates/typst-library/src/foundations/bytes.rs b/crates/typst-library/src/foundations/bytes.rs index 05fe4763a..20034d074 100644 --- a/crates/typst-library/src/foundations/bytes.rs +++ b/crates/typst-library/src/foundations/bytes.rs @@ -1,5 +1,6 @@ -use std::borrow::Cow; +use std::any::Any; use std::fmt::{self, Debug, Formatter}; +use std::hash::{Hash, Hasher}; use std::ops::{Add, AddAssign, Deref}; use std::sync::Arc; @@ -39,18 +40,44 @@ use crate::foundations::{cast, func, scope, ty, Array, Reflect, Repr, Str, Value /// #str(data.slice(1, 4)) /// ``` #[ty(scope, cast)] -#[derive(Clone, Hash, Eq, PartialEq)] -pub struct Bytes(Arc>>); +#[derive(Clone, Hash)] +#[allow(clippy::derived_hash_with_manual_eq)] +pub struct Bytes(Arc>); impl Bytes { - /// Create a buffer from a static byte slice. - pub fn from_static(slice: &'static [u8]) -> Self { - Self(Arc::new(LazyHash::new(Cow::Borrowed(slice)))) + /// Create `Bytes` from anything byte-like. + /// + /// The `data` type will directly back this bytes object. This means you can + /// e.g. pass `&'static [u8]` or `[u8; 8]` and no extra vector will be + /// allocated. + /// + /// If the type is `Vec` and the `Bytes` are unique (i.e. not cloned), + /// the vector will be reused when mutating to the `Bytes`. + /// + /// If your source type is a string, prefer [`Bytes::from_string`] to + /// directly use the UTF-8 encoded string data without any copying. + pub fn new(data: T) -> Self + where + T: AsRef<[u8]> + Send + Sync + 'static, + { + Self(Arc::new(LazyHash::new(data))) + } + + /// Create `Bytes` from anything string-like, implicitly viewing the UTF-8 + /// representation. + /// + /// The `data` type will directly back this bytes object. This means you can + /// e.g. pass `String` or `EcoString` without any copying. + pub fn from_string(data: T) -> Self + where + T: AsRef + Send + Sync + 'static, + { + Self(Arc::new(LazyHash::new(StrWrapper(data)))) } /// Return `true` if the length is 0. pub fn is_empty(&self) -> bool { - self.0.is_empty() + self.as_slice().is_empty() } /// Return a view into the buffer. @@ -60,7 +87,7 @@ impl Bytes { /// Return a copy of the buffer as a vector. pub fn to_vec(&self) -> Vec { - self.0.to_vec() + self.as_slice().to_vec() } /// Resolve an index or throw an out of bounds error. @@ -72,12 +99,10 @@ impl Bytes { /// /// `index == len` is considered in bounds. fn locate_opt(&self, index: i64) -> Option { + let len = self.as_slice().len(); let wrapped = - if index >= 0 { Some(index) } else { (self.len() as i64).checked_add(index) }; - - wrapped - .and_then(|v| usize::try_from(v).ok()) - .filter(|&v| v <= self.0.len()) + if index >= 0 { Some(index) } else { (len as i64).checked_add(index) }; + wrapped.and_then(|v| usize::try_from(v).ok()).filter(|&v| v <= len) } } @@ -106,7 +131,7 @@ impl Bytes { /// The length in bytes. #[func(title = "Length")] pub fn len(&self) -> usize { - self.0.len() + self.as_slice().len() } /// Returns the byte at the specified index. Returns the default value if @@ -122,13 +147,13 @@ impl Bytes { default: Option, ) -> StrResult { self.locate_opt(index) - .and_then(|i| self.0.get(i).map(|&b| Value::Int(b.into()))) + .and_then(|i| self.as_slice().get(i).map(|&b| Value::Int(b.into()))) .or(default) .ok_or_else(|| out_of_bounds_no_default(index, self.len())) } - /// Extracts a subslice of the bytes. Fails with an error if the start or end - /// index is out of bounds. + /// Extracts a subslice of the bytes. Fails with an error if the start or + /// end index is out of bounds. #[func] pub fn slice( &self, @@ -148,9 +173,17 @@ impl Bytes { if end.is_none() { end = count.map(|c: i64| start + c); } + let start = self.locate(start)?; let end = self.locate(end.unwrap_or(self.len() as i64))?.max(start); - Ok(self.0[start..end].into()) + let slice = &self.as_slice()[start..end]; + + // We could hold a view into the original bytes here instead of + // making a copy, but it's unclear when that's worth it. Java + // originally did that for strings, but went back on it because a + // very small view into a very large buffer would be a sort of + // memory leak. + Ok(Bytes::new(slice.to_vec())) } } @@ -170,7 +203,15 @@ impl Deref for Bytes { type Target = [u8]; fn deref(&self) -> &Self::Target { - &self.0 + self.0.as_bytes() + } +} + +impl Eq for Bytes {} + +impl PartialEq for Bytes { + fn eq(&self, other: &Self) -> bool { + self.0.eq(&other.0) } } @@ -180,18 +221,6 @@ impl AsRef<[u8]> for Bytes { } } -impl From<&[u8]> for Bytes { - fn from(slice: &[u8]) -> Self { - Self(Arc::new(LazyHash::new(slice.to_vec().into()))) - } -} - -impl From> for Bytes { - fn from(vec: Vec) -> Self { - Self(Arc::new(LazyHash::new(vec.into()))) - } -} - impl Add for Bytes { type Output = Self; @@ -207,10 +236,12 @@ impl AddAssign for Bytes { // Nothing to do } else if self.is_empty() { *self = rhs; - } else if Arc::strong_count(&self.0) == 1 && matches!(**self.0, Cow::Owned(_)) { - Arc::make_mut(&mut self.0).to_mut().extend_from_slice(&rhs); + } else if let Some(vec) = Arc::get_mut(&mut self.0) + .and_then(|unique| unique.as_any_mut().downcast_mut::>()) + { + vec.extend_from_slice(&rhs); } else { - *self = Self::from([self.as_slice(), rhs.as_slice()].concat()); + *self = Self::new([self.as_slice(), rhs.as_slice()].concat()); } } } @@ -228,20 +259,61 @@ impl Serialize for Bytes { } } +/// Any type that can back a byte buffer. +trait Bytelike: Send + Sync { + fn as_bytes(&self) -> &[u8]; + fn as_any_mut(&mut self) -> &mut dyn Any; +} + +impl Bytelike for T +where + T: AsRef<[u8]> + Send + Sync + 'static, +{ + fn as_bytes(&self) -> &[u8] { + self.as_ref() + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + +impl Hash for dyn Bytelike { + fn hash(&self, state: &mut H) { + self.as_bytes().hash(state); + } +} + +/// Makes string-like objects usable with `Bytes`. +struct StrWrapper(T); + +impl Bytelike for StrWrapper +where + T: AsRef + Send + Sync + 'static, +{ + fn as_bytes(&self) -> &[u8] { + self.0.as_ref().as_bytes() + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + /// A value that can be cast to bytes. pub struct ToBytes(Bytes); cast! { ToBytes, - v: Str => Self(v.as_bytes().into()), + v: Str => Self(Bytes::from_string(v)), v: Array => Self(v.iter() .map(|item| match item { Value::Int(byte @ 0..=255) => Ok(*byte as u8), Value::Int(_) => bail!("number must be between 0 and 255"), value => Err(::error(value)), }) - .collect::, _>>()? - .into() + .collect::, _>>() + .map(Bytes::new)? ), v: Bytes => Self(v), } diff --git a/crates/typst-library/src/foundations/float.rs b/crates/typst-library/src/foundations/float.rs index c3d4e0e73..fcc46b034 100644 --- a/crates/typst-library/src/foundations/float.rs +++ b/crates/typst-library/src/foundations/float.rs @@ -163,18 +163,14 @@ impl f64 { size: u32, ) -> StrResult { Ok(match size { - 8 => match endian { + 8 => Bytes::new(match endian { Endianness::Little => self.to_le_bytes(), Endianness::Big => self.to_be_bytes(), - } - .as_slice() - .into(), - 4 => match endian { + }), + 4 => Bytes::new(match endian { Endianness::Little => (self as f32).to_le_bytes(), Endianness::Big => (self as f32).to_be_bytes(), - } - .as_slice() - .into(), + }), _ => bail!("size must be either 4 or 8"), }) } diff --git a/crates/typst-library/src/foundations/int.rs b/crates/typst-library/src/foundations/int.rs index bddffada3..83a89bf8a 100644 --- a/crates/typst-library/src/foundations/int.rs +++ b/crates/typst-library/src/foundations/int.rs @@ -1,6 +1,7 @@ use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError}; use ecow::{eco_format, EcoString}; +use smallvec::SmallVec; use crate::diag::{bail, StrResult}; use crate::foundations::{ @@ -322,7 +323,7 @@ impl i64 { Endianness::Little => self.to_le_bytes(), }; - let mut buf = vec![0u8; size]; + let mut buf = SmallVec::<[u8; 8]>::from_elem(0, size); match endian { Endianness::Big => { // Copy the bytes from the array to the buffer, starting from @@ -339,7 +340,7 @@ impl i64 { } } - Bytes::from(buf) + Bytes::new(buf) } } diff --git a/crates/typst-library/src/foundations/plugin.rs b/crates/typst-library/src/foundations/plugin.rs index f57257a45..a7c341d8c 100644 --- a/crates/typst-library/src/foundations/plugin.rs +++ b/crates/typst-library/src/foundations/plugin.rs @@ -293,7 +293,7 @@ impl Plugin { _ => bail!("plugin did not respect the protocol"), }; - Ok(output.into()) + Ok(Bytes::new(output)) } /// An iterator over all the function names defined by the plugin. diff --git a/crates/typst-library/src/foundations/value.rs b/crates/typst-library/src/foundations/value.rs index eb0d6eedc..efc480d3f 100644 --- a/crates/typst-library/src/foundations/value.rs +++ b/crates/typst-library/src/foundations/value.rs @@ -459,15 +459,15 @@ impl<'de> Visitor<'de> for ValueVisitor { } fn visit_bytes(self, v: &[u8]) -> Result { - Ok(Bytes::from(v).into_value()) + Ok(Bytes::new(v.to_vec()).into_value()) } fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result { - Ok(Bytes::from(v).into_value()) + Ok(Bytes::new(v.to_vec()).into_value()) } fn visit_byte_buf(self, v: Vec) -> Result { - Ok(Bytes::from(v).into_value()) + Ok(Bytes::new(v).into_value()) } fn visit_none(self) -> Result { diff --git a/crates/typst-library/src/loading/cbor.rs b/crates/typst-library/src/loading/cbor.rs index 977059c3d..a03e5c998 100644 --- a/crates/typst-library/src/loading/cbor.rs +++ b/crates/typst-library/src/loading/cbor.rs @@ -55,7 +55,7 @@ impl cbor { let Spanned { v: value, span } = value; let mut res = Vec::new(); ciborium::into_writer(&value, &mut res) - .map(|_| res.into()) + .map(|_| Bytes::new(res)) .map_err(|err| eco_format!("failed to encode value as CBOR ({err})")) .at(span) } diff --git a/crates/typst-library/src/loading/mod.rs b/crates/typst-library/src/loading/mod.rs index ae74df864..120b3e3af 100644 --- a/crates/typst-library/src/loading/mod.rs +++ b/crates/typst-library/src/loading/mod.rs @@ -56,15 +56,22 @@ pub enum Readable { impl Readable { pub fn as_slice(&self) -> &[u8] { match self { - Readable::Bytes(v) => v, - Readable::Str(v) => v.as_bytes(), + Self::Bytes(v) => v, + Self::Str(v) => v.as_bytes(), } } pub fn as_str(&self) -> Option<&str> { match self { - Readable::Str(v) => Some(v.as_str()), - Readable::Bytes(v) => std::str::from_utf8(v).ok(), + Self::Str(v) => Some(v.as_str()), + Self::Bytes(v) => std::str::from_utf8(v).ok(), + } + } + + pub fn into_bytes(self) -> Bytes { + match self { + Self::Bytes(v) => v, + Self::Str(v) => Bytes::from_string(v), } } } @@ -78,12 +85,3 @@ cast! { v: Str => Self::Str(v), v: Bytes => Self::Bytes(v), } - -impl From for Bytes { - fn from(value: Readable) -> Self { - match value { - Readable::Bytes(v) => v, - Readable::Str(v) => v.as_bytes().into(), - } - } -} diff --git a/crates/typst-library/src/text/font/color.rs b/crates/typst-library/src/text/font/color.rs index 08f6fe0a3..e3183e885 100644 --- a/crates/typst-library/src/text/font/color.rs +++ b/crates/typst-library/src/text/font/color.rs @@ -7,6 +7,7 @@ use typst_syntax::Span; use usvg::tiny_skia_path; use xmlwriter::XmlWriter; +use crate::foundations::Bytes; use crate::layout::{Abs, Frame, FrameItem, Point, Size}; use crate::text::{Font, Glyph}; use crate::visualize::{FixedStroke, Geometry, Image, RasterFormat, VectorFormat}; @@ -101,8 +102,12 @@ fn draw_raster_glyph( upem: Abs, raster_image: ttf_parser::RasterGlyphImage, ) -> Option<()> { - let image = - Image::new(raster_image.data.into(), RasterFormat::Png.into(), None).ok()?; + let image = Image::new( + Bytes::new(raster_image.data.to_vec()), + RasterFormat::Png.into(), + None, + ) + .ok()?; // Apple Color emoji doesn't provide offset information (or at least // not in a way ttf-parser understands), so we artificially shift their @@ -175,7 +180,7 @@ fn draw_colr_glyph( let data = svg.end_document().into_bytes(); - let image = Image::new(data.into(), VectorFormat::Svg.into(), None).ok()?; + let image = Image::new(Bytes::new(data), VectorFormat::Svg.into(), None).ok()?; let y_shift = Abs::pt(upem.to_pt() - y_max); let position = Point::new(Abs::pt(x_min), y_shift); @@ -251,7 +256,7 @@ fn draw_svg_glyph( ); let image = - Image::new(wrapper_svg.into_bytes().into(), VectorFormat::Svg.into(), None) + Image::new(Bytes::new(wrapper_svg.into_bytes()), VectorFormat::Svg.into(), None) .ok()?; let position = Point::new(Abs::pt(left), Abs::pt(top) + upem); diff --git a/crates/typst-library/src/visualize/image/raster.rs b/crates/typst-library/src/visualize/image/raster.rs index 829826c75..098843a25 100644 --- a/crates/typst-library/src/visualize/image/raster.rs +++ b/crates/typst-library/src/visualize/image/raster.rs @@ -274,7 +274,7 @@ mod tests { #[track_caller] fn test(path: &str, format: RasterFormat, dpi: f64) { let data = typst_dev_assets::get(path).unwrap(); - let bytes = Bytes::from_static(data); + let bytes = Bytes::new(data); let image = RasterImage::new(bytes, format).unwrap(); assert_eq!(image.dpi().map(f64::round), Some(dpi)); } diff --git a/crates/typst-svg/src/text.rs b/crates/typst-svg/src/text.rs index 80de32089..fa471b2ae 100644 --- a/crates/typst-svg/src/text.rs +++ b/crates/typst-svg/src/text.rs @@ -3,6 +3,7 @@ use std::io::Read; use base64::Engine; use ecow::EcoString; use ttf_parser::GlyphId; +use typst_library::foundations::Bytes; use typst_library::layout::{Abs, Point, Ratio, Size, Transform}; use typst_library::text::{Font, TextItem}; use typst_library::visualize::{FillRule, Image, Paint, RasterFormat, RelativeTo}; @@ -243,7 +244,9 @@ fn convert_bitmap_glyph_to_image(font: &Font, id: GlyphId) -> Option<(Image, f64 if raster.format != ttf_parser::RasterImageFormat::PNG { return None; } - let image = Image::new(raster.data.into(), RasterFormat::Png.into(), None).ok()?; + let image = + Image::new(Bytes::new(raster.data.to_vec()), RasterFormat::Png.into(), None) + .ok()?; Some((image, raster.x as f64, raster.y as f64)) } diff --git a/docs/src/html.rs b/docs/src/html.rs index a1206032d..4eb3954c3 100644 --- a/docs/src/html.rs +++ b/docs/src/html.rs @@ -486,7 +486,7 @@ impl World for DocWorld { fn file(&self, id: FileId) -> FileResult { assert!(id.package().is_none()); - Ok(Bytes::from_static( + Ok(Bytes::new( typst_dev_assets::get_by_name( &id.vpath().as_rootless_path().to_string_lossy(), ) diff --git a/docs/src/lib.rs b/docs/src/lib.rs index e92799718..2751500e3 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -78,7 +78,7 @@ static LIBRARY: LazyLock> = LazyLock::new(|| { static FONTS: LazyLock<(LazyHash, Vec)> = LazyLock::new(|| { let fonts: Vec<_> = typst_assets::fonts() .chain(typst_dev_assets::fonts()) - .flat_map(|data| Font::iter(Bytes::from_static(data))) + .flat_map(|data| Font::iter(Bytes::new(data))) .collect(); let book = FontBook::from_fonts(&fonts); (LazyHash::new(book), fonts) diff --git a/tests/fuzz/src/compile.rs b/tests/fuzz/src/compile.rs index 37e21deb9..3dedfb737 100644 --- a/tests/fuzz/src/compile.rs +++ b/tests/fuzz/src/compile.rs @@ -19,7 +19,7 @@ struct FuzzWorld { impl FuzzWorld { fn new(text: &str) -> Self { let data = typst_assets::fonts().next().unwrap(); - let font = Font::new(Bytes::from_static(data), 0).unwrap(); + let font = Font::new(Bytes::new(data), 0).unwrap(); let book = FontBook::from_fonts([&font]); Self { library: LazyHash::new(Library::default()), diff --git a/tests/src/world.rs b/tests/src/world.rs index a08f1efa8..5c2678328 100644 --- a/tests/src/world.rs +++ b/tests/src/world.rs @@ -98,7 +98,7 @@ impl Default for TestBase { fn default() -> Self { let fonts: Vec<_> = typst_assets::fonts() .chain(typst_dev_assets::fonts()) - .flat_map(|data| Font::iter(Bytes::from_static(data))) + .flat_map(|data| Font::iter(Bytes::new(data))) .collect(); Self { @@ -140,8 +140,8 @@ impl FileSlot { self.file .get_or_init(|| { read(&system_path(self.id)?).map(|cow| match cow { - Cow::Owned(buf) => buf.into(), - Cow::Borrowed(buf) => Bytes::from_static(buf), + Cow::Owned(buf) => Bytes::new(buf), + Cow::Borrowed(buf) => Bytes::new(buf), }) }) .clone() From e2b37fef33a92a7086790e04fb133472413c0c0a Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 9 Jan 2025 10:34:16 +0100 Subject: [PATCH 03/10] Revamp data loading and deprecate `decode` functions (#5671) --- crates/typst-eval/src/import.rs | 2 +- crates/typst-ide/src/complete.rs | 37 +- crates/typst-layout/src/image.rs | 52 ++- crates/typst-library/src/foundations/array.rs | 47 ++ crates/typst-library/src/foundations/bytes.rs | 51 ++- crates/typst-library/src/foundations/cast.rs | 59 ++- .../typst-library/src/foundations/plugin.rs | 14 +- crates/typst-library/src/foundations/str.rs | 6 +- .../typst-library/src/foundations/styles.rs | 10 +- crates/typst-library/src/loading/cbor.rs | 30 +- crates/typst-library/src/loading/csv.rs | 109 ++--- crates/typst-library/src/loading/json.rs | 29 +- crates/typst-library/src/loading/mod.rs | 95 +++- crates/typst-library/src/loading/read.rs | 11 +- crates/typst-library/src/loading/toml.rs | 33 +- crates/typst-library/src/loading/xml.rs | 39 +- crates/typst-library/src/loading/yaml.rs | 29 +- .../typst-library/src/model/bibliography.rs | 407 +++++++++--------- crates/typst-library/src/model/cite.rs | 34 +- crates/typst-library/src/model/document.rs | 10 +- crates/typst-library/src/pdf/embed.rs | 86 ++-- crates/typst-library/src/text/raw.rs | 376 ++++++++-------- .../typst-library/src/visualize/image/mod.rs | 52 ++- .../typst-library/src/visualize/image/svg.rs | 1 + crates/typst-pdf/src/embed.rs | 10 +- crates/typst-utils/src/hash.rs | 71 +++ crates/typst-utils/src/lib.rs | 2 +- tests/suite/pdf/embed.typ | 20 +- 28 files changed, 1000 insertions(+), 722 deletions(-) diff --git a/crates/typst-eval/src/import.rs b/crates/typst-eval/src/import.rs index 5b67c0608..2060d25f1 100644 --- a/crates/typst-eval/src/import.rs +++ b/crates/typst-eval/src/import.rs @@ -211,7 +211,7 @@ fn resolve_package( // Evaluate the manifest. let manifest_id = FileId::new(Some(spec.clone()), VirtualPath::new("typst.toml")); let bytes = engine.world.file(manifest_id).at(span)?; - let string = std::str::from_utf8(&bytes).map_err(FileError::from).at(span)?; + let string = bytes.as_str().map_err(FileError::from).at(span)?; let manifest: PackageManifest = toml::from_str(string) .map_err(|err| eco_format!("package manifest is malformed ({})", err.message())) .at(span)?; diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index c22ea7e40..0f8abddb7 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -817,19 +817,8 @@ fn param_value_completions<'a>( ) { if param.name == "font" { ctx.font_completions(); - } else if param.name == "path" { - ctx.file_completions_with_extensions(match func.name() { - Some("image") => &["png", "jpg", "jpeg", "gif", "svg", "svgz"], - Some("csv") => &["csv"], - Some("plugin") => &["wasm"], - Some("cbor") => &["cbor"], - Some("json") => &["json"], - Some("toml") => &["toml"], - Some("xml") => &["xml"], - Some("yaml") => &["yml", "yaml"], - Some("bibliography") => &["bib", "yml", "yaml"], - _ => &[], - }); + } else if let Some(extensions) = path_completion(func, param) { + ctx.file_completions_with_extensions(extensions); } else if func.name() == Some("figure") && param.name == "body" { ctx.snippet_completion("image", "image(\"${}\"),", "An image in a figure."); ctx.snippet_completion("table", "table(\n ${}\n),", "A table in a figure."); @@ -838,6 +827,28 @@ fn param_value_completions<'a>( ctx.cast_completions(¶m.input); } +/// Returns which file extensions to complete for the given parameter if any. +fn path_completion(func: &Func, param: &ParamInfo) -> Option<&'static [&'static str]> { + Some(match (func.name(), param.name) { + (Some("image"), "source") => &["png", "jpg", "jpeg", "gif", "svg", "svgz"], + (Some("csv"), "source") => &["csv"], + (Some("plugin"), "source") => &["wasm"], + (Some("cbor"), "source") => &["cbor"], + (Some("json"), "source") => &["json"], + (Some("toml"), "source") => &["toml"], + (Some("xml"), "source") => &["xml"], + (Some("yaml"), "source") => &["yml", "yaml"], + (Some("bibliography"), "sources") => &["bib", "yml", "yaml"], + (Some("bibliography"), "style") => &["csl"], + (Some("cite"), "style") => &["csl"], + (Some("raw"), "syntaxes") => &["sublime-syntax"], + (Some("raw"), "theme") => &["tmtheme"], + (Some("embed"), "path") => &[], + (None, "path") => &[], + _ => return None, + }) +} + /// Resolve a callee expression to a global function. fn resolve_global_callee<'a>( ctx: &CompletionContext<'a>, diff --git a/crates/typst-layout/src/image.rs b/crates/typst-layout/src/image.rs index 59e2c0210..e521b993f 100644 --- a/crates/typst-layout/src/image.rs +++ b/crates/typst-layout/src/image.rs @@ -1,13 +1,13 @@ use std::ffi::OsStr; -use typst_library::diag::{bail, warning, At, SourceResult, StrResult}; +use typst_library::diag::{warning, At, SourceResult, StrResult}; use typst_library::engine::Engine; -use typst_library::foundations::{Packed, Smart, StyleChain}; +use typst_library::foundations::{Bytes, Derived, Packed, Smart, StyleChain}; use typst_library::introspection::Locator; use typst_library::layout::{ Abs, Axes, FixedAlignment, Frame, FrameItem, Point, Region, Size, }; -use typst_library::loading::Readable; +use typst_library::loading::DataSource; use typst_library::text::families; use typst_library::visualize::{ Curve, Image, ImageElem, ImageFit, ImageFormat, RasterFormat, VectorFormat, @@ -26,17 +26,17 @@ pub fn layout_image( // Take the format that was explicitly defined, or parse the extension, // or try to detect the format. - let data = elem.data(); + let Derived { source, derived: data } = &elem.source; let format = match elem.format(styles) { Smart::Custom(v) => v, - Smart::Auto => determine_format(elem.path().as_str(), data).at(span)?, + Smart::Auto => determine_format(source, data).at(span)?, }; // Warn the user if the image contains a foreign object. Not perfect // because the svg could also be encoded, but that's an edge case. if format == ImageFormat::Vector(VectorFormat::Svg) { let has_foreign_object = - data.as_str().is_some_and(|s| s.contains(" StrResult { - let ext = std::path::Path::new(path) - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default() - .to_lowercase(); +/// Try to determine the image format based on the data. +fn determine_format(source: &DataSource, data: &Bytes) -> StrResult { + if let DataSource::Path(path) = source { + let ext = std::path::Path::new(path.as_str()) + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default() + .to_lowercase(); - Ok(match ext.as_str() { - "png" => ImageFormat::Raster(RasterFormat::Png), - "jpg" | "jpeg" => ImageFormat::Raster(RasterFormat::Jpg), - "gif" => ImageFormat::Raster(RasterFormat::Gif), - "svg" | "svgz" => ImageFormat::Vector(VectorFormat::Svg), - _ => match &data { - Readable::Str(_) => ImageFormat::Vector(VectorFormat::Svg), - Readable::Bytes(bytes) => match RasterFormat::detect(bytes) { - Some(f) => ImageFormat::Raster(f), - None => bail!("unknown image format"), - }, - }, - }) + match ext.as_str() { + "png" => return Ok(ImageFormat::Raster(RasterFormat::Png)), + "jpg" | "jpeg" => return Ok(ImageFormat::Raster(RasterFormat::Jpg)), + "gif" => return Ok(ImageFormat::Raster(RasterFormat::Gif)), + "svg" | "svgz" => return Ok(ImageFormat::Vector(VectorFormat::Svg)), + _ => {} + } + } + + Ok(ImageFormat::detect(data).ok_or("unknown image format")?) } diff --git a/crates/typst-library/src/foundations/array.rs b/crates/typst-library/src/foundations/array.rs index 4667ee765..e79a4e930 100644 --- a/crates/typst-library/src/foundations/array.rs +++ b/crates/typst-library/src/foundations/array.rs @@ -1124,6 +1124,53 @@ impl FromValue for SmallVec<[T; N]> { } } +/// One element, or multiple provided as an array. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct OneOrMultiple(pub Vec); + +impl Reflect for OneOrMultiple { + fn input() -> CastInfo { + T::input() + Array::input() + } + + fn output() -> CastInfo { + T::output() + Array::output() + } + + fn castable(value: &Value) -> bool { + Array::castable(value) || T::castable(value) + } +} + +impl IntoValue for OneOrMultiple { + fn into_value(self) -> Value { + self.0.into_value() + } +} + +impl FromValue for OneOrMultiple { + fn from_value(value: Value) -> HintedStrResult { + if T::castable(&value) { + return Ok(Self(vec![T::from_value(value)?])); + } + if Array::castable(&value) { + return Ok(Self( + Array::from_value(value)? + .into_iter() + .map(|value| T::from_value(value)) + .collect::>()?, + )); + } + Err(Self::error(&value)) + } +} + +impl Default for OneOrMultiple { + fn default() -> Self { + Self(vec![]) + } +} + /// The error message when the array is empty. #[cold] fn array_is_empty() -> EcoString { diff --git a/crates/typst-library/src/foundations/bytes.rs b/crates/typst-library/src/foundations/bytes.rs index 20034d074..d633c99ad 100644 --- a/crates/typst-library/src/foundations/bytes.rs +++ b/crates/typst-library/src/foundations/bytes.rs @@ -2,6 +2,7 @@ use std::any::Any; use std::fmt::{self, Debug, Formatter}; use std::hash::{Hash, Hasher}; use std::ops::{Add, AddAssign, Deref}; +use std::str::Utf8Error; use std::sync::Arc; use ecow::{eco_format, EcoString}; @@ -80,16 +81,37 @@ impl Bytes { self.as_slice().is_empty() } - /// Return a view into the buffer. + /// Return a view into the bytes. pub fn as_slice(&self) -> &[u8] { self } - /// Return a copy of the buffer as a vector. + /// Try to view the bytes as an UTF-8 string. + /// + /// If these bytes were created via `Bytes::from_string`, UTF-8 validation + /// is skipped. + pub fn as_str(&self) -> Result<&str, Utf8Error> { + self.inner().as_str() + } + + /// Return a copy of the bytes as a vector. pub fn to_vec(&self) -> Vec { self.as_slice().to_vec() } + /// Try to turn the bytes into a `Str`. + /// + /// - If these bytes were created via `Bytes::from_string::`, the + /// string is cloned directly. + /// - If these bytes were created via `Bytes::from_string`, but from a + /// different type of string, UTF-8 validation is still skipped. + pub fn to_str(&self) -> Result { + match self.inner().as_any().downcast_ref::() { + Some(string) => Ok(string.clone()), + None => self.as_str().map(Into::into), + } + } + /// Resolve an index or throw an out of bounds error. fn locate(&self, index: i64) -> StrResult { self.locate_opt(index).ok_or_else(|| out_of_bounds(index, self.len())) @@ -104,6 +126,11 @@ impl Bytes { if index >= 0 { Some(index) } else { (len as i64).checked_add(index) }; wrapped.and_then(|v| usize::try_from(v).ok()).filter(|&v| v <= len) } + + /// Access the inner `dyn Bytelike`. + fn inner(&self) -> &dyn Bytelike { + &**self.0 + } } #[scope] @@ -203,7 +230,7 @@ impl Deref for Bytes { type Target = [u8]; fn deref(&self) -> &Self::Target { - self.0.as_bytes() + self.inner().as_bytes() } } @@ -262,6 +289,8 @@ impl Serialize for Bytes { /// Any type that can back a byte buffer. trait Bytelike: Send + Sync { fn as_bytes(&self) -> &[u8]; + fn as_str(&self) -> Result<&str, Utf8Error>; + fn as_any(&self) -> &dyn Any; fn as_any_mut(&mut self) -> &mut dyn Any; } @@ -273,6 +302,14 @@ where self.as_ref() } + fn as_str(&self) -> Result<&str, Utf8Error> { + std::str::from_utf8(self.as_ref()) + } + + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { self } @@ -295,6 +332,14 @@ where self.0.as_ref().as_bytes() } + fn as_str(&self) -> Result<&str, Utf8Error> { + Ok(self.0.as_ref()) + } + + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { self } diff --git a/crates/typst-library/src/foundations/cast.rs b/crates/typst-library/src/foundations/cast.rs index 84f38f36e..38f409c67 100644 --- a/crates/typst-library/src/foundations/cast.rs +++ b/crates/typst-library/src/foundations/cast.rs @@ -13,7 +13,9 @@ use typst_syntax::{Span, Spanned}; use unicode_math_class::MathClass; use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult}; -use crate::foundations::{array, repr, NativeElement, Packed, Repr, Str, Type, Value}; +use crate::foundations::{ + array, repr, Fold, NativeElement, Packed, Repr, Str, Type, Value, +}; /// Determine details of a type. /// @@ -497,3 +499,58 @@ cast! { /// An operator that can be both unary or binary like `+`. "vary" => MathClass::Vary, } + +/// A type that contains a user-visible source portion and something that is +/// derived from it, but not user-visible. +/// +/// An example usage would be `source` being a `DataSource` and `derived` a +/// TextMate theme parsed from it. With `Derived`, we can store both parts in +/// the `RawElem::theme` field and get automatic nice `Reflect` and `IntoValue` +/// impls. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Derived { + /// The source portion. + pub source: S, + /// The derived portion. + pub derived: D, +} + +impl Derived { + /// Create a new instance from the `source` and the `derived` data. + pub fn new(source: S, derived: D) -> Self { + Self { source, derived } + } +} + +impl Reflect for Derived { + fn input() -> CastInfo { + S::input() + } + + fn output() -> CastInfo { + S::output() + } + + fn castable(value: &Value) -> bool { + S::castable(value) + } + + fn error(found: &Value) -> HintedString { + S::error(found) + } +} + +impl IntoValue for Derived { + fn into_value(self) -> Value { + self.source.into_value() + } +} + +impl Fold for Derived { + fn fold(self, outer: Self) -> Self { + Self { + source: self.source.fold(outer.source), + derived: self.derived.fold(outer.derived), + } + } +} diff --git a/crates/typst-library/src/foundations/plugin.rs b/crates/typst-library/src/foundations/plugin.rs index a7c341d8c..adf23a47c 100644 --- a/crates/typst-library/src/foundations/plugin.rs +++ b/crates/typst-library/src/foundations/plugin.rs @@ -9,7 +9,7 @@ use wasmi::{AsContext, AsContextMut}; use crate::diag::{bail, At, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{func, repr, scope, ty, Bytes}; -use crate::World; +use crate::loading::{DataSource, Load}; /// A WebAssembly plugin. /// @@ -154,15 +154,13 @@ impl Plugin { pub fn construct( /// The engine. engine: &mut Engine, - /// Path to a WebAssembly file. + /// A path to a WebAssembly file or raw WebAssembly bytes. /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// For more details about paths, see the [Paths section]($syntax/#paths). + source: Spanned, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - Plugin::new(data).at(span) + let data = source.load(engine.world)?; + Plugin::new(data).at(source.span) } } diff --git a/crates/typst-library/src/foundations/str.rs b/crates/typst-library/src/foundations/str.rs index 4025d1ab3..2e90b3071 100644 --- a/crates/typst-library/src/foundations/str.rs +++ b/crates/typst-library/src/foundations/str.rs @@ -784,11 +784,7 @@ cast! { v: f64 => Self::Str(repr::display_float(v).into()), v: Decimal => Self::Str(format_str!("{}", v)), v: Version => Self::Str(format_str!("{}", v)), - v: Bytes => Self::Str( - std::str::from_utf8(&v) - .map_err(|_| "bytes are not valid utf-8")? - .into() - ), + v: Bytes => Self::Str(v.to_str().map_err(|_| "bytes are not valid utf-8")?), v: Label => Self::Str(v.resolve().as_str().into()), v: Type => Self::Str(v.long_name().into()), v: Str => Self::Str(v), diff --git a/crates/typst-library/src/foundations/styles.rs b/crates/typst-library/src/foundations/styles.rs index 7354719e9..37094dcd8 100644 --- a/crates/typst-library/src/foundations/styles.rs +++ b/crates/typst-library/src/foundations/styles.rs @@ -12,7 +12,8 @@ use typst_utils::LazyHash; use crate::diag::{SourceResult, Trace, Tracepoint}; use crate::engine::Engine; use crate::foundations::{ - cast, ty, Content, Context, Element, Func, NativeElement, Repr, Selector, + cast, ty, Content, Context, Element, Func, NativeElement, OneOrMultiple, Repr, + Selector, }; use crate::text::{FontFamily, FontList, TextElem}; @@ -939,6 +940,13 @@ impl Fold for SmallVec<[T; N]> { } } +impl Fold for OneOrMultiple { + fn fold(self, mut outer: Self) -> Self { + outer.0.extend(self.0); + outer + } +} + /// A variant of fold for foldable optional (`Option`) values where an inner /// `None` value isn't respected (contrary to `Option`'s usual `Fold` /// implementation, with which folding with an inner `None` always returns diff --git a/crates/typst-library/src/loading/cbor.rs b/crates/typst-library/src/loading/cbor.rs index a03e5c998..13d551201 100644 --- a/crates/typst-library/src/loading/cbor.rs +++ b/crates/typst-library/src/loading/cbor.rs @@ -1,10 +1,10 @@ -use ecow::{eco_format, EcoString}; +use ecow::eco_format; use typst_syntax::Spanned; use crate::diag::{At, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Bytes, Value}; -use crate::World; +use crate::loading::{DataSource, Load}; /// Reads structured data from a CBOR file. /// @@ -21,29 +21,31 @@ use crate::World; pub fn cbor( /// The engine. engine: &mut Engine, - /// Path to a CBOR file. + /// A path to a CBOR file or raw CBOR bytes. /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// For more details about paths, see the [Paths section]($syntax/#paths). + source: Spanned, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - cbor::decode(Spanned::new(data, span)) + let data = source.load(engine.world)?; + ciborium::from_reader(data.as_slice()) + .map_err(|err| eco_format!("failed to parse CBOR ({err})")) + .at(source.span) } #[scope] impl cbor { /// Reads structured data from CBOR bytes. + /// + /// This function is deprecated. The [`cbor`] function now accepts bytes + /// directly. #[func(title = "Decode CBOR")] pub fn decode( - /// cbor data. + /// The engine. + engine: &mut Engine, + /// CBOR data. data: Spanned, ) -> SourceResult { - let Spanned { v: data, span } = data; - ciborium::from_reader(data.as_slice()) - .map_err(|err| eco_format!("failed to parse CBOR ({err})")) - .at(span) + cbor(engine, data.map(DataSource::Bytes)) } /// Encode structured data into CBOR bytes. diff --git a/crates/typst-library/src/loading/csv.rs b/crates/typst-library/src/loading/csv.rs index 6822505d3..8171c4832 100644 --- a/crates/typst-library/src/loading/csv.rs +++ b/crates/typst-library/src/loading/csv.rs @@ -4,8 +4,7 @@ use typst_syntax::Spanned; use crate::diag::{bail, At, SourceResult}; use crate::engine::Engine; use crate::foundations::{cast, func, scope, Array, Dict, IntoValue, Type, Value}; -use crate::loading::Readable; -use crate::World; +use crate::loading::{DataSource, Load, Readable}; /// Reads structured data from a CSV file. /// @@ -28,10 +27,10 @@ use crate::World; pub fn csv( /// The engine. engine: &mut Engine, - /// Path to a CSV file. + /// Path to a CSV file or raw CSV bytes. /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// For more details about paths, see the [Paths section]($syntax/#paths). + source: Spanned, /// The delimiter that separates columns in the CSV file. /// Must be a single ASCII character. #[named] @@ -48,17 +47,63 @@ pub fn csv( #[default(RowType::Array)] row_type: RowType, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - self::csv::decode(Spanned::new(Readable::Bytes(data), span), delimiter, row_type) + let data = source.load(engine.world)?; + + let mut builder = ::csv::ReaderBuilder::new(); + let has_headers = row_type == RowType::Dict; + builder.has_headers(has_headers); + builder.delimiter(delimiter.0 as u8); + + // Counting lines from 1 by default. + let mut line_offset: usize = 1; + let mut reader = builder.from_reader(data.as_slice()); + let mut headers: Option<::csv::StringRecord> = None; + + if has_headers { + // Counting lines from 2 because we have a header. + line_offset += 1; + headers = Some( + reader + .headers() + .map_err(|err| format_csv_error(err, 1)) + .at(source.span)? + .clone(), + ); + } + + let mut array = Array::new(); + for (line, result) in reader.records().enumerate() { + // Original solution was to use line from error, but that is + // incorrect with `has_headers` set to `false`. See issue: + // https://github.com/BurntSushi/rust-csv/issues/184 + let line = line + line_offset; + let row = result.map_err(|err| format_csv_error(err, line)).at(source.span)?; + let item = if let Some(headers) = &headers { + let mut dict = Dict::new(); + for (field, value) in headers.iter().zip(&row) { + dict.insert(field.into(), value.into_value()); + } + dict.into_value() + } else { + let sub = row.into_iter().map(|field| field.into_value()).collect(); + Value::Array(sub) + }; + array.push(item); + } + + Ok(array) } #[scope] impl csv { /// Reads structured data from a CSV string/bytes. + /// + /// This function is deprecated. The [`csv`] function now accepts bytes + /// directly. #[func(title = "Decode CSV")] pub fn decode( + /// The engine. + engine: &mut Engine, /// CSV data. data: Spanned, /// The delimiter that separates columns in the CSV file. @@ -77,51 +122,7 @@ impl csv { #[default(RowType::Array)] row_type: RowType, ) -> SourceResult { - let Spanned { v: data, span } = data; - let has_headers = row_type == RowType::Dict; - - let mut builder = ::csv::ReaderBuilder::new(); - builder.has_headers(has_headers); - builder.delimiter(delimiter.0 as u8); - - // Counting lines from 1 by default. - let mut line_offset: usize = 1; - let mut reader = builder.from_reader(data.as_slice()); - let mut headers: Option<::csv::StringRecord> = None; - - if has_headers { - // Counting lines from 2 because we have a header. - line_offset += 1; - headers = Some( - reader - .headers() - .map_err(|err| format_csv_error(err, 1)) - .at(span)? - .clone(), - ); - } - - let mut array = Array::new(); - for (line, result) in reader.records().enumerate() { - // Original solution was to use line from error, but that is - // incorrect with `has_headers` set to `false`. See issue: - // https://github.com/BurntSushi/rust-csv/issues/184 - let line = line + line_offset; - let row = result.map_err(|err| format_csv_error(err, line)).at(span)?; - let item = if let Some(headers) = &headers { - let mut dict = Dict::new(); - for (field, value) in headers.iter().zip(&row) { - dict.insert(field.into(), value.into_value()); - } - dict.into_value() - } else { - let sub = row.into_iter().map(|field| field.into_value()).collect(); - Value::Array(sub) - }; - array.push(item); - } - - Ok(array) + csv(engine, data.map(Readable::into_source), delimiter, row_type) } } diff --git a/crates/typst-library/src/loading/json.rs b/crates/typst-library/src/loading/json.rs index 597cf4cc6..3128d77da 100644 --- a/crates/typst-library/src/loading/json.rs +++ b/crates/typst-library/src/loading/json.rs @@ -1,11 +1,10 @@ -use ecow::{eco_format, EcoString}; +use ecow::eco_format; use typst_syntax::Spanned; use crate::diag::{At, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Str, Value}; -use crate::loading::Readable; -use crate::World; +use crate::loading::{DataSource, Load, Readable}; /// Reads structured data from a JSON file. /// @@ -53,29 +52,31 @@ use crate::World; pub fn json( /// The engine. engine: &mut Engine, - /// Path to a JSON file. + /// Path to a JSON file or raw JSON bytes. /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// For more details about paths, see the [Paths section]($syntax/#paths). + source: Spanned, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - json::decode(Spanned::new(Readable::Bytes(data), span)) + let data = source.load(engine.world)?; + serde_json::from_slice(data.as_slice()) + .map_err(|err| eco_format!("failed to parse JSON ({err})")) + .at(source.span) } #[scope] impl json { /// Reads structured data from a JSON string/bytes. + /// + /// This function is deprecated. The [`json`] function now accepts bytes + /// directly. #[func(title = "Decode JSON")] pub fn decode( + /// The engine. + engine: &mut Engine, /// JSON data. data: Spanned, ) -> SourceResult { - let Spanned { v: data, span } = data; - serde_json::from_slice(data.as_slice()) - .map_err(|err| eco_format!("failed to parse JSON ({err})")) - .at(span) + json(engine, data.map(Readable::into_source)) } /// Encodes structured data into a JSON string. diff --git a/crates/typst-library/src/loading/mod.rs b/crates/typst-library/src/loading/mod.rs index 120b3e3af..171ae651a 100644 --- a/crates/typst-library/src/loading/mod.rs +++ b/crates/typst-library/src/loading/mod.rs @@ -15,6 +15,10 @@ mod xml_; #[path = "yaml.rs"] mod yaml_; +use comemo::Tracked; +use ecow::EcoString; +use typst_syntax::Spanned; + pub use self::cbor_::*; pub use self::csv_::*; pub use self::json_::*; @@ -23,7 +27,10 @@ pub use self::toml_::*; pub use self::xml_::*; pub use self::yaml_::*; +use crate::diag::{At, SourceResult}; +use crate::foundations::OneOrMultiple; use crate::foundations::{cast, category, Bytes, Category, Scope, Str}; +use crate::World; /// Data loading from external files. /// @@ -44,6 +51,76 @@ pub(super) fn define(global: &mut Scope) { global.define_func::(); } +/// Something we can retrieve byte data from. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum DataSource { + /// A path to a file. + Path(EcoString), + /// Raw bytes. + Bytes(Bytes), +} + +cast! { + DataSource, + self => match self { + Self::Path(v) => v.into_value(), + Self::Bytes(v) => v.into_value(), + }, + v: EcoString => Self::Path(v), + v: Bytes => Self::Bytes(v), +} + +/// Loads data from a path or provided bytes. +pub trait Load { + /// Bytes or a list of bytes (if there are multiple sources). + type Output; + + /// Load the bytes. + fn load(&self, world: Tracked) -> SourceResult; +} + +impl Load for Spanned { + type Output = Bytes; + + fn load(&self, world: Tracked) -> SourceResult { + self.as_ref().load(world) + } +} + +impl Load for Spanned<&DataSource> { + type Output = Bytes; + + fn load(&self, world: Tracked) -> SourceResult { + match &self.v { + DataSource::Path(path) => { + let file_id = self.span.resolve_path(path).at(self.span)?; + world.file(file_id).at(self.span) + } + DataSource::Bytes(bytes) => Ok(bytes.clone()), + } + } +} + +impl Load for Spanned> { + type Output = Vec; + + fn load(&self, world: Tracked) -> SourceResult> { + self.as_ref().load(world) + } +} + +impl Load for Spanned<&OneOrMultiple> { + type Output = Vec; + + fn load(&self, world: Tracked) -> SourceResult> { + self.v + .0 + .iter() + .map(|source| Spanned::new(source, self.span).load(world)) + .collect() + } +} + /// A value that can be read from a file. #[derive(Debug, Clone, PartialEq, Hash)] pub enum Readable { @@ -54,26 +131,16 @@ pub enum Readable { } impl Readable { - pub fn as_slice(&self) -> &[u8] { - match self { - Self::Bytes(v) => v, - Self::Str(v) => v.as_bytes(), - } - } - - pub fn as_str(&self) -> Option<&str> { - match self { - Self::Str(v) => Some(v.as_str()), - Self::Bytes(v) => std::str::from_utf8(v).ok(), - } - } - pub fn into_bytes(self) -> Bytes { match self { Self::Bytes(v) => v, Self::Str(v) => Bytes::from_string(v), } } + + pub fn into_source(self) -> DataSource { + DataSource::Bytes(self.into_bytes()) + } } cast! { diff --git a/crates/typst-library/src/loading/read.rs b/crates/typst-library/src/loading/read.rs index 23e6e27e7..bf363f846 100644 --- a/crates/typst-library/src/loading/read.rs +++ b/crates/typst-library/src/loading/read.rs @@ -1,7 +1,7 @@ use ecow::EcoString; use typst_syntax::Spanned; -use crate::diag::{At, SourceResult}; +use crate::diag::{At, FileError, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, Cast}; use crate::loading::Readable; @@ -42,12 +42,9 @@ pub fn read( let data = engine.world.file(id).at(span)?; Ok(match encoding { None => Readable::Bytes(data), - Some(Encoding::Utf8) => Readable::Str( - std::str::from_utf8(&data) - .map_err(|_| "file is not valid utf-8") - .at(span)? - .into(), - ), + Some(Encoding::Utf8) => { + Readable::Str(data.to_str().map_err(FileError::from).at(span)?) + } }) } diff --git a/crates/typst-library/src/loading/toml.rs b/crates/typst-library/src/loading/toml.rs index 5167703ef..e3a01cdd5 100644 --- a/crates/typst-library/src/loading/toml.rs +++ b/crates/typst-library/src/loading/toml.rs @@ -1,11 +1,10 @@ use ecow::{eco_format, EcoString}; use typst_syntax::{is_newline, Spanned}; -use crate::diag::{At, SourceResult}; +use crate::diag::{At, FileError, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Str, Value}; -use crate::loading::Readable; -use crate::World; +use crate::loading::{DataSource, Load, Readable}; /// Reads structured data from a TOML file. /// @@ -31,32 +30,32 @@ use crate::World; pub fn toml( /// The engine. engine: &mut Engine, - /// Path to a TOML file. + /// A path to a TOML file or raw TOML bytes. /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// For more details about paths, see the [Paths section]($syntax/#paths). + source: Spanned, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - toml::decode(Spanned::new(Readable::Bytes(data), span)) + let data = source.load(engine.world)?; + let raw = data.as_str().map_err(FileError::from).at(source.span)?; + ::toml::from_str(raw) + .map_err(|err| format_toml_error(err, raw)) + .at(source.span) } #[scope] impl toml { /// Reads structured data from a TOML string/bytes. + /// + /// This function is deprecated. The [`toml`] function now accepts bytes + /// directly. #[func(title = "Decode TOML")] pub fn decode( + /// The engine. + engine: &mut Engine, /// TOML data. data: Spanned, ) -> SourceResult { - let Spanned { v: data, span } = data; - let raw = std::str::from_utf8(data.as_slice()) - .map_err(|_| "file is not valid utf-8") - .at(span)?; - ::toml::from_str(raw) - .map_err(|err| format_toml_error(err, raw)) - .at(span) + toml(engine, data.map(Readable::into_source)) } /// Encodes structured data into a TOML string. diff --git a/crates/typst-library/src/loading/xml.rs b/crates/typst-library/src/loading/xml.rs index 3b1a9674b..53ec3d93b 100644 --- a/crates/typst-library/src/loading/xml.rs +++ b/crates/typst-library/src/loading/xml.rs @@ -5,8 +5,7 @@ use typst_syntax::Spanned; use crate::diag::{format_xml_like_error, At, FileError, SourceResult}; use crate::engine::Engine; use crate::foundations::{dict, func, scope, Array, Dict, IntoValue, Str, Value}; -use crate::loading::Readable; -use crate::World; +use crate::loading::{DataSource, Load, Readable}; /// Reads structured data from an XML file. /// @@ -60,36 +59,36 @@ use crate::World; pub fn xml( /// The engine. engine: &mut Engine, - /// Path to an XML file. + /// A path to an XML file or raw XML bytes. /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// For more details about paths, see the [Paths section]($syntax/#paths). + source: Spanned, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - xml::decode(Spanned::new(Readable::Bytes(data), span)) + let data = source.load(engine.world)?; + let text = data.as_str().map_err(FileError::from).at(source.span)?; + let document = roxmltree::Document::parse_with_options( + text, + ParsingOptions { allow_dtd: true, ..Default::default() }, + ) + .map_err(format_xml_error) + .at(source.span)?; + Ok(convert_xml(document.root())) } #[scope] impl xml { /// Reads structured data from an XML string/bytes. + /// + /// This function is deprecated. The [`xml`] function now accepts bytes + /// directly. #[func(title = "Decode XML")] pub fn decode( + /// The engine. + engine: &mut Engine, /// XML data. data: Spanned, ) -> SourceResult { - let Spanned { v: data, span } = data; - let text = std::str::from_utf8(data.as_slice()) - .map_err(FileError::from) - .at(span)?; - let document = roxmltree::Document::parse_with_options( - text, - ParsingOptions { allow_dtd: true, ..Default::default() }, - ) - .map_err(format_xml_error) - .at(span)?; - Ok(convert_xml(document.root())) + xml(engine, data.map(Readable::into_source)) } } diff --git a/crates/typst-library/src/loading/yaml.rs b/crates/typst-library/src/loading/yaml.rs index 0e8ca3fb0..2eb26be8f 100644 --- a/crates/typst-library/src/loading/yaml.rs +++ b/crates/typst-library/src/loading/yaml.rs @@ -1,11 +1,10 @@ -use ecow::{eco_format, EcoString}; +use ecow::eco_format; use typst_syntax::Spanned; use crate::diag::{At, SourceResult}; use crate::engine::Engine; use crate::foundations::{func, scope, Str, Value}; -use crate::loading::Readable; -use crate::World; +use crate::loading::{DataSource, Load, Readable}; /// Reads structured data from a YAML file. /// @@ -43,29 +42,31 @@ use crate::World; pub fn yaml( /// The engine. engine: &mut Engine, - /// Path to a YAML file. + /// A path to a YAML file or raw YAML bytes. /// - /// For more details, see the [Paths section]($syntax/#paths). - path: Spanned, + /// For more details about paths, see the [Paths section]($syntax/#paths). + source: Spanned, ) -> SourceResult { - let Spanned { v: path, span } = path; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - yaml::decode(Spanned::new(Readable::Bytes(data), span)) + let data = source.load(engine.world)?; + serde_yaml::from_slice(data.as_slice()) + .map_err(|err| eco_format!("failed to parse YAML ({err})")) + .at(source.span) } #[scope] impl yaml { /// Reads structured data from a YAML string/bytes. + /// + /// This function is deprecated. The [`yaml`] function now accepts bytes + /// directly. #[func(title = "Decode YAML")] pub fn decode( + /// The engine. + engine: &mut Engine, /// YAML data. data: Spanned, ) -> SourceResult { - let Spanned { v: data, span } = data; - serde_yaml::from_slice(data.as_slice()) - .map_err(|err| eco_format!("failed to parse YAML ({err})")) - .at(span) + yaml(engine, data.map(Readable::into_source)) } /// Encode structured data into a YAML string. diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index 280ac4a42..4ab4ff22c 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -1,7 +1,7 @@ +use std::any::TypeId; use std::collections::HashMap; use std::ffi::OsStr; use std::fmt::{self, Debug, Formatter}; -use std::hash::{Hash, Hasher}; use std::num::NonZeroUsize; use std::path::Path; use std::sync::{Arc, LazyLock}; @@ -12,26 +12,26 @@ use hayagriva::archive::ArchivedStyle; use hayagriva::io::BibLaTeXError; use hayagriva::{ citationberg, BibliographyDriver, BibliographyRequest, CitationItem, CitationRequest, - SpecificLocator, + Library, SpecificLocator, }; use indexmap::IndexMap; use smallvec::{smallvec, SmallVec}; -use typed_arena::Arena; use typst_syntax::{Span, Spanned}; -use typst_utils::{LazyHash, NonZeroExt, PicoStr}; +use typst_utils::{ManuallyHash, NonZeroExt, PicoStr}; use crate::diag::{bail, error, At, FileError, HintedStrResult, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, ty, Args, Array, Bytes, CastInfo, Content, FromValue, IntoValue, Label, - NativeElement, Packed, Reflect, Repr, Scope, Show, ShowSet, Smart, Str, StyleChain, - Styles, Synthesize, Type, Value, + elem, Bytes, CastInfo, Content, Derived, FromValue, IntoValue, Label, NativeElement, + OneOrMultiple, Packed, Reflect, Scope, Show, ShowSet, Smart, StyleChain, Styles, + Synthesize, Value, }; use crate::introspection::{Introspector, Locatable, Location}; use crate::layout::{ BlockBody, BlockElem, Em, GridCell, GridChild, GridElem, GridItem, HElem, PadElem, Sizing, TrackSizings, VElem, }; +use crate::loading::{DataSource, Load}; use crate::model::{ CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem, Url, @@ -86,13 +86,20 @@ use crate::World; /// ``` #[elem(Locatable, Synthesize, Show, ShowSet, LocalName)] pub struct BibliographyElem { - /// Path(s) to Hayagriva `.yml` and/or BibLaTeX `.bib` files. + /// One or multiple paths to or raw bytes for Hayagriva `.yml` and/or + /// BibLaTeX `.bib` files. + /// + /// This can be a: + /// - A path string to load a bibliography file from the given path. For + /// more details about paths, see the [Paths section]($syntax/#paths). + /// - Raw bytes from which the bibliography should be decoded. + /// - An array where each item is one the above. #[required] #[parse( - let (paths, bibliography) = Bibliography::parse(engine, args)?; - paths + let sources = args.expect("sources")?; + Bibliography::load(engine.world, sources)? )] - pub path: BibliographyPaths, + pub sources: Derived, Bibliography>, /// The title of the bibliography. /// @@ -116,19 +123,22 @@ pub struct BibliographyElem { /// The bibliography style. /// - /// Should be either one of the built-in styles (see below) or a path to - /// a [CSL file](https://citationstyles.org/). Some of the styles listed - /// below appear twice, once with their full name and once with a short - /// alias. - #[parse(CslStyle::parse(engine, args)?)] - #[default(CslStyle::from_name("ieee").unwrap())] - pub style: CslStyle, - - /// The loaded bibliography. - #[internal] - #[required] - #[parse(bibliography)] - pub bibliography: Bibliography, + /// This can be: + /// - A string with the name of one of the built-in styles (see below). Some + /// of the styles listed below appear twice, once with their full name and + /// once with a short alias. + /// - A path string to a [CSL file](https://citationstyles.org/). For more + /// details about paths, see the [Paths section]($syntax/#paths). + /// - Raw bytes from which a CSL style should be decoded. + #[parse(match args.named::>("style")? { + Some(source) => Some(CslStyle::load(engine.world, source)?), + None => None, + })] + #[default({ + let default = ArchivedStyle::InstituteOfElectricalAndElectronicsEngineers; + Derived::new(CslSource::Named(default), CslStyle::from_archived(default)) + })] + pub style: Derived, /// The language setting where the bibliography is. #[internal] @@ -141,17 +151,6 @@ pub struct BibliographyElem { pub region: Option, } -/// A list of bibliography file paths. -#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)] -pub struct BibliographyPaths(Vec); - -cast! { - BibliographyPaths, - self => self.0.into_value(), - v: EcoString => Self(vec![v]), - v: Array => Self(v.into_iter().map(Value::cast).collect::>()?), -} - impl BibliographyElem { /// Find the document's bibliography. pub fn find(introspector: Tracked) -> StrResult> { @@ -169,13 +168,12 @@ impl BibliographyElem { } /// Whether the bibliography contains the given key. - pub fn has(engine: &Engine, key: impl Into) -> bool { - let key = key.into(); + pub fn has(engine: &Engine, key: Label) -> bool { engine .introspector .query(&Self::elem().select()) .iter() - .any(|elem| elem.to_packed::().unwrap().bibliography().has(key)) + .any(|elem| elem.to_packed::().unwrap().sources.derived.has(key)) } /// Find all bibliography keys. @@ -183,9 +181,9 @@ impl BibliographyElem { let mut vec = vec![]; for elem in introspector.query(&Self::elem().select()).iter() { let this = elem.to_packed::().unwrap(); - for (key, entry) in this.bibliography().iter() { + for (key, entry) in this.sources.derived.iter() { let detail = entry.title().map(|title| title.value.to_str().into()); - vec.push((Label::new(key), detail)) + vec.push((key, detail)) } } vec @@ -282,63 +280,35 @@ impl LocalName for Packed { } /// A loaded bibliography. -#[derive(Clone, PartialEq)] -pub struct Bibliography { - map: Arc>, - hash: u128, -} +#[derive(Clone, PartialEq, Hash)] +pub struct Bibliography(Arc>>); impl Bibliography { - /// Parse the bibliography argument. - fn parse( - engine: &mut Engine, - args: &mut Args, - ) -> SourceResult<(BibliographyPaths, Bibliography)> { - let Spanned { v: paths, span } = - args.expect::>("path to bibliography file")?; - - // Load bibliography files. - let data = paths - .0 - .iter() - .map(|path| { - let id = span.resolve_path(path).at(span)?; - engine.world.file(id).at(span) - }) - .collect::>>()?; - - // Parse. - let bibliography = Self::load(&paths, &data).at(span)?; - - Ok((paths, bibliography)) + /// Load a bibliography from data sources. + fn load( + world: Tracked, + sources: Spanned>, + ) -> SourceResult, Self>> { + let data = sources.load(world)?; + let bibliography = Self::decode(&sources.v, &data).at(sources.span)?; + Ok(Derived::new(sources.v, bibliography)) } - /// Load bibliography entries from paths. + /// Decode a bibliography from loaded data sources. #[comemo::memoize] #[typst_macros::time(name = "load bibliography")] - fn load(paths: &BibliographyPaths, data: &[Bytes]) -> StrResult { + fn decode( + sources: &OneOrMultiple, + data: &[Bytes], + ) -> StrResult { let mut map = IndexMap::new(); let mut duplicates = Vec::::new(); // We might have multiple bib/yaml files - for (path, bytes) in paths.0.iter().zip(data) { - let src = std::str::from_utf8(bytes).map_err(FileError::from)?; - - let ext = Path::new(path.as_str()) - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default(); - - let library = match ext.to_lowercase().as_str() { - "yml" | "yaml" => hayagriva::io::from_yaml_str(src) - .map_err(|err| eco_format!("failed to parse YAML ({err})"))?, - "bib" => hayagriva::io::from_biblatex_str(src) - .map_err(|errors| format_biblatex_error(path, src, errors))?, - _ => bail!("unknown bibliography format (must be .yml/.yaml or .bib)"), - }; - + for (source, data) in sources.0.iter().zip(data) { + let library = decode_library(source, data)?; for entry in library { - match map.entry(PicoStr::intern(entry.key())) { + match map.entry(Label::new(PicoStr::intern(entry.key()))) { indexmap::map::Entry::Vacant(vacant) => { vacant.insert(entry); } @@ -353,182 +323,210 @@ impl Bibliography { bail!("duplicate bibliography keys: {}", duplicates.join(", ")); } - Ok(Bibliography { - map: Arc::new(map), - hash: typst_utils::hash128(data), - }) + Ok(Bibliography(Arc::new(ManuallyHash::new(map, typst_utils::hash128(data))))) } - fn has(&self, key: impl Into) -> bool { - self.map.contains_key(&key.into()) + fn has(&self, key: Label) -> bool { + self.0.contains_key(&key) } - fn iter(&self) -> impl Iterator { - self.map.iter().map(|(&k, v)| (k, v)) + fn get(&self, key: Label) -> Option<&hayagriva::Entry> { + self.0.get(&key) + } + + fn iter(&self) -> impl Iterator { + self.0.iter().map(|(&k, v)| (k, v)) } } impl Debug for Bibliography { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.debug_set().entries(self.map.keys()).finish() + f.debug_set().entries(self.0.keys()).finish() } } -impl Hash for Bibliography { - fn hash(&self, state: &mut H) { - self.hash.hash(state); +/// Decode on library from one data source. +fn decode_library(source: &DataSource, data: &Bytes) -> StrResult { + let src = data.as_str().map_err(FileError::from)?; + + if let DataSource::Path(path) = source { + // If we got a path, use the extension to determine whether it is + // YAML or BibLaTeX. + let ext = Path::new(path.as_str()) + .extension() + .and_then(OsStr::to_str) + .unwrap_or_default(); + + match ext.to_lowercase().as_str() { + "yml" | "yaml" => hayagriva::io::from_yaml_str(src) + .map_err(|err| eco_format!("failed to parse YAML ({err})")), + "bib" => hayagriva::io::from_biblatex_str(src) + .map_err(|errors| format_biblatex_error(src, Some(path), errors)), + _ => bail!("unknown bibliography format (must be .yml/.yaml or .bib)"), + } + } else { + // If we just got bytes, we need to guess. If it can be decoded as + // hayagriva YAML, we'll use that. + let haya_err = match hayagriva::io::from_yaml_str(src) { + Ok(library) => return Ok(library), + Err(err) => err, + }; + + // If it can be decoded as BibLaTeX, we use that isntead. + let bib_errs = match hayagriva::io::from_biblatex_str(src) { + Ok(library) => return Ok(library), + Err(err) => err, + }; + + // If neither decoded correctly, check whether `:` or `{` appears + // more often to guess whether it's more likely to be YAML or BibLaTeX + // and emit the more appropriate error. + let mut yaml = 0; + let mut biblatex = 0; + for c in src.chars() { + match c { + ':' => yaml += 1, + '{' => biblatex += 1, + _ => {} + } + } + + if yaml > biblatex { + bail!("failed to parse YAML ({haya_err})") + } else { + Err(format_biblatex_error(src, None, bib_errs)) + } } } /// Format a BibLaTeX loading error. -fn format_biblatex_error(path: &str, src: &str, errors: Vec) -> EcoString { +fn format_biblatex_error( + src: &str, + path: Option<&str>, + errors: Vec, +) -> EcoString { let Some(error) = errors.first() else { - return eco_format!("failed to parse BibLaTeX file ({path})"); + return match path { + Some(path) => eco_format!("failed to parse BibLaTeX file ({path})"), + None => eco_format!("failed to parse BibLaTeX"), + }; }; let (span, msg) = match error { BibLaTeXError::Parse(error) => (&error.span, error.kind.to_string()), BibLaTeXError::Type(error) => (&error.span, error.kind.to_string()), }; + let line = src.get(..span.start).unwrap_or_default().lines().count(); - eco_format!("failed to parse BibLaTeX file ({path}:{line}: {msg})") + match path { + Some(path) => eco_format!("failed to parse BibLaTeX file ({path}:{line}: {msg})"), + None => eco_format!("failed to parse BibLaTeX ({line}: {msg})"), + } } /// A loaded CSL style. -#[ty(cast)] #[derive(Debug, Clone, PartialEq, Hash)] -pub struct CslStyle { - name: Option, - style: Arc>, -} +pub struct CslStyle(Arc>); impl CslStyle { - /// Parse the style argument. - pub fn parse(engine: &mut Engine, args: &mut Args) -> SourceResult> { - let Some(Spanned { v: string, span }) = - args.named::>("style")? - else { - return Ok(None); - }; - - Ok(Some(Self::parse_impl(engine, &string, span).at(span)?)) - } - - /// Parse the style argument with `Smart`. - pub fn parse_smart( - engine: &mut Engine, - args: &mut Args, - ) -> SourceResult>> { - let Some(Spanned { v: smart, span }) = - args.named::>>("style")? - else { - return Ok(None); - }; - - Ok(Some(match smart { - Smart::Auto => Smart::Auto, - Smart::Custom(string) => { - Smart::Custom(Self::parse_impl(engine, &string, span).at(span)?) + /// Load a CSL style from a data source. + pub fn load( + world: Tracked, + Spanned { v: source, span }: Spanned, + ) -> SourceResult> { + let style = match &source { + CslSource::Named(style) => Self::from_archived(*style), + CslSource::Normal(source) => { + let data = Spanned::new(source, span).load(world)?; + Self::from_data(data).at(span)? } - })) - } - - /// Parse internally. - fn parse_impl(engine: &mut Engine, string: &str, span: Span) -> StrResult { - let ext = Path::new(string) - .extension() - .and_then(OsStr::to_str) - .unwrap_or_default() - .to_lowercase(); - - if ext == "csl" { - let id = span.resolve_path(string)?; - let data = engine.world.file(id)?; - CslStyle::from_data(&data) - } else { - CslStyle::from_name(string) - } + }; + Ok(Derived::new(source, style)) } /// Load a built-in CSL style. #[comemo::memoize] - pub fn from_name(name: &str) -> StrResult { - match hayagriva::archive::ArchivedStyle::by_name(name).map(ArchivedStyle::get) { - Some(citationberg::Style::Independent(style)) => Ok(Self { - name: Some(name.into()), - style: Arc::new(LazyHash::new(style)), - }), - _ => bail!("unknown style: `{name}`"), + pub fn from_archived(archived: ArchivedStyle) -> CslStyle { + match archived.get() { + citationberg::Style::Independent(style) => Self(Arc::new(ManuallyHash::new( + style, + typst_utils::hash128(&(TypeId::of::(), archived)), + ))), + // Ensured by `test_bibliography_load_builtin_styles`. + _ => unreachable!("archive should not contain dependant styles"), } } /// Load a CSL style from file contents. #[comemo::memoize] - pub fn from_data(data: &Bytes) -> StrResult { - let text = std::str::from_utf8(data.as_slice()).map_err(FileError::from)?; + pub fn from_data(data: Bytes) -> StrResult { + let text = data.as_str().map_err(FileError::from)?; citationberg::IndependentStyle::from_xml(text) - .map(|style| Self { name: None, style: Arc::new(LazyHash::new(style)) }) + .map(|style| { + Self(Arc::new(ManuallyHash::new( + style, + typst_utils::hash128(&(TypeId::of::(), data)), + ))) + }) .map_err(|err| eco_format!("failed to load CSL style ({err})")) } /// Get the underlying independent style. pub fn get(&self) -> &citationberg::IndependentStyle { - self.style.as_ref() + self.0.as_ref() } } -// This Reflect impl is technically a bit wrong because it doesn't say what -// FromValue and IntoValue really do. Instead, it says what the `style` argument -// on `bibliography` and `cite` expect (through manual parsing). -impl Reflect for CslStyle { +/// Source for a CSL style. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum CslSource { + /// A predefined named style. + Named(ArchivedStyle), + /// A normal data source. + Normal(DataSource), +} + +impl Reflect for CslSource { #[comemo::memoize] fn input() -> CastInfo { - let ty = std::iter::once(CastInfo::Type(Type::of::())); - let options = hayagriva::archive::ArchivedStyle::all().iter().map(|name| { + let source = std::iter::once(DataSource::input()); + let names = ArchivedStyle::all().iter().map(|name| { CastInfo::Value(name.names()[0].into_value(), name.display_name()) }); - CastInfo::Union(ty.chain(options).collect()) + CastInfo::Union(source.into_iter().chain(names).collect()) } fn output() -> CastInfo { - EcoString::output() + DataSource::output() } fn castable(value: &Value) -> bool { - if let Value::Dyn(dynamic) = &value { - if dynamic.is::() { - return true; - } - } - - false + DataSource::castable(value) } } -impl FromValue for CslStyle { +impl FromValue for CslSource { fn from_value(value: Value) -> HintedStrResult { - if let Value::Dyn(dynamic) = &value { - if let Some(concrete) = dynamic.downcast::() { - return Ok(concrete.clone()); + if EcoString::castable(&value) { + let string = EcoString::from_value(value.clone())?; + if Path::new(string.as_str()).extension().is_none() { + let style = ArchivedStyle::by_name(&string) + .ok_or_else(|| eco_format!("unknown style: {}", string))?; + return Ok(CslSource::Named(style)); } } - Err(::error(&value)) + DataSource::from_value(value).map(CslSource::Normal) } } -impl IntoValue for CslStyle { +impl IntoValue for CslSource { fn into_value(self) -> Value { - Value::dynamic(self) - } -} - -impl Repr for CslStyle { - fn repr(&self) -> EcoString { - self.name - .as_ref() - .map(|name| name.repr()) - .unwrap_or_else(|| "..".into()) + match self { + // We prefer the shorter names which are at the back of the array. + Self::Named(v) => v.names().last().unwrap().into_value(), + Self::Normal(v) => v.into_value(), + } } } @@ -632,9 +630,8 @@ impl<'a> Generator<'a> { static LOCALES: LazyLock> = LazyLock::new(hayagriva::archive::locales); - let database = self.bibliography.bibliography(); - let bibliography_style = self.bibliography.style(StyleChain::default()); - let styles = Arena::new(); + let database = &self.bibliography.sources.derived; + let bibliography_style = &self.bibliography.style(StyleChain::default()).derived; // Process all citation groups. let mut driver = BibliographyDriver::new(); @@ -654,7 +651,7 @@ impl<'a> Generator<'a> { // Create infos and items for each child in the group. for child in children { let key = *child.key(); - let Some(entry) = database.map.get(&key.into_inner()) else { + let Some(entry) = database.get(key) else { errors.push(error!( child.span(), "key `{}` does not exist in the bibliography", @@ -695,8 +692,8 @@ impl<'a> Generator<'a> { } let style = match first.style(StyleChain::default()) { - Smart::Auto => &bibliography_style.style, - Smart::Custom(style) => styles.alloc(style.style), + Smart::Auto => bibliography_style.get(), + Smart::Custom(style) => style.derived.get(), }; self.infos.push(GroupInfo { @@ -727,7 +724,7 @@ impl<'a> Generator<'a> { // Add hidden items for everything if we should print the whole // bibliography. if self.bibliography.full(StyleChain::default()) { - for entry in database.map.values() { + for (_, entry) in database.iter() { driver.citation(CitationRequest::new( vec![CitationItem::new(entry, None, None, true, None)], bibliography_style.get(), @@ -1097,3 +1094,15 @@ fn locale(lang: Lang, region: Option) -> citationberg::LocaleCode { } citationberg::LocaleCode(value) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_bibliography_load_builtin_styles() { + for &archived in ArchivedStyle::all() { + let _ = CslStyle::from_archived(archived); + } + } +} diff --git a/crates/typst-library/src/model/cite.rs b/crates/typst-library/src/model/cite.rs index ac0cfa790..29497993d 100644 --- a/crates/typst-library/src/model/cite.rs +++ b/crates/typst-library/src/model/cite.rs @@ -1,11 +1,14 @@ +use typst_syntax::Spanned; + use crate::diag::{error, At, HintedString, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, Cast, Content, Label, Packed, Show, Smart, StyleChain, Synthesize, + cast, elem, Cast, Content, Derived, Label, Packed, Show, Smart, StyleChain, + Synthesize, }; use crate::introspection::Locatable; use crate::model::bibliography::Works; -use crate::model::CslStyle; +use crate::model::{CslSource, CslStyle}; use crate::text::{Lang, Region, TextElem}; /// Cite a work from the bibliography. @@ -87,15 +90,24 @@ pub struct CiteElem { /// The citation style. /// - /// Should be either `{auto}`, one of the built-in styles (see below) or a - /// path to a [CSL file](https://citationstyles.org/). Some of the styles - /// listed below appear twice, once with their full name and once with a - /// short alias. - /// - /// When set to `{auto}`, automatically use the - /// [bibliography's style]($bibliography.style) for the citations. - #[parse(CslStyle::parse_smart(engine, args)?)] - pub style: Smart, + /// This can be: + /// - `{auto}` to automatically use the + /// [bibliography's style]($bibliography.style) for citations. + /// - A string with the name of one of the built-in styles (see below). Some + /// of the styles listed below appear twice, once with their full name and + /// once with a short alias. + /// - A path string to a [CSL file](https://citationstyles.org/). For more + /// details about paths, see the [Paths section]($syntax/#paths). + /// - Raw bytes from which a CSL style should be decoded. + #[parse(match args.named::>>("style")? { + Some(Spanned { v: Smart::Custom(source), span }) => Some(Smart::Custom( + CslStyle::load(engine.world, Spanned::new(source, span))? + )), + Some(Spanned { v: Smart::Auto, .. }) => Some(Smart::Auto), + None => None, + })] + #[borrowed] + pub style: Smart>, /// The text language setting where the citation is. #[internal] diff --git a/crates/typst-library/src/model/document.rs b/crates/typst-library/src/model/document.rs index 5124b2487..1bce6b357 100644 --- a/crates/typst-library/src/model/document.rs +++ b/crates/typst-library/src/model/document.rs @@ -3,8 +3,8 @@ use ecow::EcoString; use crate::diag::{bail, HintedStrResult, SourceResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, Args, Array, Construct, Content, Datetime, Fields, Smart, StyleChain, - Styles, Value, + cast, elem, Args, Array, Construct, Content, Datetime, Fields, OneOrMultiple, Smart, + StyleChain, Styles, Value, }; /// The root element of a document and its metadata. @@ -35,7 +35,7 @@ pub struct DocumentElem { /// The document's authors. #[ghost] - pub author: Author, + pub author: OneOrMultiple, /// The document's description. #[ghost] @@ -43,7 +43,7 @@ pub struct DocumentElem { /// The document's keywords. #[ghost] - pub keywords: Keywords, + pub keywords: OneOrMultiple, /// The document's creation date. /// @@ -93,7 +93,7 @@ cast! { pub struct DocumentInfo { /// The document's title. pub title: Option, - /// The document's author. + /// The document's author(s). pub author: Vec, /// The document's description. pub description: Option, diff --git a/crates/typst-library/src/pdf/embed.rs b/crates/typst-library/src/pdf/embed.rs index db4986225..f9ca3ca09 100644 --- a/crates/typst-library/src/pdf/embed.rs +++ b/crates/typst-library/src/pdf/embed.rs @@ -1,13 +1,10 @@ use ecow::EcoString; -use typst_syntax::{Span, Spanned}; +use typst_syntax::Spanned; -use crate::diag::{At, SourceResult, StrResult}; +use crate::diag::{At, SourceResult}; use crate::engine::Engine; -use crate::foundations::{ - elem, func, scope, Cast, Content, NativeElement, Packed, Show, StyleChain, -}; +use crate::foundations::{elem, Bytes, Cast, Content, Derived, Packed, Show, StyleChain}; use crate::introspection::Locatable; -use crate::loading::Readable; use crate::World; /// A file that will be embedded into the output PDF. @@ -33,33 +30,40 @@ use crate::World; /// - This element is ignored if exporting to a format other than PDF. /// - File embeddings are not currently supported for PDF/A-2, even if the /// embedded file conforms to PDF/A-1 or PDF/A-2. -#[elem(scope, Show, Locatable)] +#[elem(Show, Locatable)] pub struct EmbedElem { - /// Path to a file to be embedded. + /// Path of the file to be embedded. /// - /// For more details, see the [Paths section]($syntax/#paths). + /// Must always be specified, but is only read from if no data is provided + /// in the following argument. + /// + /// For more details about paths, see the [Paths section]($syntax/#paths). #[required] #[parse( let Spanned { v: path, span } = - args.expect::>("path to the file to be embedded")?; + args.expect::>("path")?; let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - path + // The derived part is the project-relative resolved path. + let resolved = id.vpath().as_rootless_path().to_string_lossy().replace("\\", "/").into(); + Derived::new(path.clone(), resolved) )] #[borrowed] - pub path: EcoString, + pub path: Derived, - /// The resolved project-relative path. - #[internal] + /// Raw file data, optionally. + /// + /// If omitted, the data is read from the specified path. + #[positional] + // Not actually required as an argument, but always present as a field. + // We can't distinguish between the two at the moment. #[required] - #[parse(id.vpath().as_rootless_path().to_string_lossy().replace("\\", "/").into())] - pub resolved_path: EcoString, - - /// The raw file data. - #[internal] - #[required] - #[parse(Readable::Bytes(data))] - pub data: Readable, + #[parse( + match args.find::()? { + Some(data) => data, + None => engine.world.file(id).at(span)?, + } + )] + pub data: Bytes, /// The relationship of the embedded file to the document. /// @@ -75,42 +79,6 @@ pub struct EmbedElem { pub description: Option, } -#[scope] -impl EmbedElem { - /// Decode a file embedding from bytes or a string. - #[func(title = "Embed Data")] - fn decode( - /// The call span of this function. - span: Span, - /// The path that will be written into the PDF. Typst will not read from - /// this path since the data is provided in the following argument. - path: EcoString, - /// The data to embed as a file. - data: Readable, - /// The relationship of the embedded file to the document. - #[named] - relationship: Option>, - /// The MIME type of the embedded file. - #[named] - mime_type: Option>, - /// A description for the embedded file. - #[named] - description: Option>, - ) -> StrResult { - let mut elem = EmbedElem::new(path.clone(), path, data); - if let Some(description) = description { - elem.push_description(description); - } - if let Some(mime_type) = mime_type { - elem.push_mime_type(mime_type); - } - if let Some(relationship) = relationship { - elem.push_relationship(relationship); - } - Ok(elem.pack().spanned(span)) - } -} - impl Show for Packed { fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { Ok(Content::empty()) diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs index 10a7cfee1..cd718d2a1 100644 --- a/crates/typst-library/src/text/raw.rs +++ b/crates/typst-library/src/text/raw.rs @@ -1,23 +1,25 @@ use std::cell::LazyCell; -use std::hash::Hash; use std::ops::Range; use std::sync::{Arc, LazyLock}; +use comemo::Tracked; use ecow::{eco_format, EcoString, EcoVec}; -use syntect::highlighting::{self as synt, Theme}; +use syntect::highlighting as synt; use syntect::parsing::{SyntaxDefinition, SyntaxSet, SyntaxSetBuilder}; use typst_syntax::{split_newlines, LinkedNode, Span, Spanned}; +use typst_utils::ManuallyHash; use unicode_segmentation::UnicodeSegmentation; use super::Lang; -use crate::diag::{At, FileError, HintedStrResult, SourceResult, StrResult}; +use crate::diag::{At, FileError, SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, scope, Args, Array, Bytes, Content, Fold, NativeElement, Packed, - PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, TargetElem, Value, + cast, elem, scope, Bytes, Content, Derived, NativeElement, OneOrMultiple, Packed, + PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, TargetElem, }; use crate::html::{tag, HtmlElem}; use crate::layout::{BlockBody, BlockElem, Em, HAlignment}; +use crate::loading::{DataSource, Load}; use crate::model::{Figurable, ParElem}; use crate::text::{ FontFamily, FontList, Hyphenate, LinebreakElem, LocalName, TextElem, TextSize, @@ -25,12 +27,6 @@ use crate::text::{ use crate::visualize::Color; use crate::World; -// Shorthand for highlighter closures. -type StyleFn<'a> = - &'a mut dyn FnMut(usize, &LinkedNode, Range, synt::Style) -> Content; -type LineFn<'a> = &'a mut dyn FnMut(usize, Range, &mut Vec); -type ThemeArgType = Smart>; - /// Raw text with optional syntax highlighting. /// /// Displays the text verbatim and in a monospace font. This is typically used @@ -186,9 +182,15 @@ pub struct RawElem { #[default(HAlignment::Start)] pub align: HAlignment, - /// One or multiple additional syntax definitions to load. The syntax - /// definitions should be in the - /// [`sublime-syntax` file format](https://www.sublimetext.com/docs/syntax.html). + /// Additional syntax definitions to load. The syntax definitions should be + /// in the [`sublime-syntax` file format](https://www.sublimetext.com/docs/syntax.html). + /// + /// You can pass any of the following values: + /// + /// - A path string to load a syntax file from the given path. For more + /// details about paths, see the [Paths section]($syntax/#paths). + /// - Raw bytes from which the syntax should be decoded. + /// - An array where each item is one the above. /// /// ````example /// #set raw(syntaxes: "SExpressions.sublime-syntax") @@ -201,22 +203,24 @@ pub struct RawElem { /// (* x (factorial (- x 1))))) /// ``` /// ```` - #[parse( - let (syntaxes, syntaxes_data) = parse_syntaxes(engine, args)?; - syntaxes - )] + #[parse(match args.named("syntaxes")? { + Some(sources) => Some(RawSyntax::load(engine.world, sources)?), + None => None, + })] #[fold] - pub syntaxes: SyntaxPaths, + pub syntaxes: Derived, Vec>, - /// The raw file buffers of syntax definition files. - #[internal] - #[parse(syntaxes_data)] - #[fold] - pub syntaxes_data: Vec, - - /// The theme to use for syntax highlighting. Theme files should be in the + /// The theme to use for syntax highlighting. Themes should be in the /// [`tmTheme` file format](https://www.sublimetext.com/docs/color_schemes_tmtheme.html). /// + /// You can pass any of the following values: + /// + /// - `{none}`: Disables syntax highlighting. + /// - `{auto}`: Highlights with Typst's default theme. + /// - A path string to load a theme file from the given path. For more + /// details about paths, see the [Paths section]($syntax/#paths). + /// - Raw bytes from which the theme should be decoded. + /// /// Applying a theme only affects the color of specifically highlighted /// text. It does not consider the theme's foreground and background /// properties, so that you retain control over the color of raw text. You @@ -224,8 +228,6 @@ pub struct RawElem { /// the background with a [filled block]($block.fill). You could also use /// the [`xml`] function to extract these properties from the theme. /// - /// Additionally, you can set the theme to `{none}` to disable highlighting. - /// /// ````example /// #set raw(theme: "halcyon.tmTheme") /// #show raw: it => block( @@ -240,18 +242,16 @@ pub struct RawElem { /// #let hi = "Hello World" /// ``` /// ```` - #[parse( - let (theme_path, theme_data) = parse_theme(engine, args)?; - theme_path - )] + #[parse(match args.named::>>>("theme")? { + Some(Spanned { v: Smart::Custom(Some(source)), span }) => Some(Smart::Custom( + Some(RawTheme::load(engine.world, Spanned::new(source, span))?) + )), + Some(Spanned { v: Smart::Custom(None), .. }) => Some(Smart::Custom(None)), + Some(Spanned { v: Smart::Auto, .. }) => Some(Smart::Auto), + None => None, + })] #[borrowed] - pub theme: ThemeArgType, - - /// The raw file buffer of syntax theme file. - #[internal] - #[parse(theme_data.map(Some))] - #[borrowed] - pub theme_data: Option, + pub theme: Smart>>, /// The size for a tab stop in spaces. A tab is replaced with enough spaces to /// align with the next multiple of the size. @@ -325,9 +325,6 @@ impl Packed { .map(|s| s.to_lowercase()) .or(Some("txt".into())); - let extra_syntaxes = LazyCell::new(|| { - load_syntaxes(&elem.syntaxes(styles), &elem.syntaxes_data(styles)).unwrap() - }); let non_highlighted_result = |lines: EcoVec<(EcoString, Span)>| { lines.into_iter().enumerate().map(|(i, (line, line_span))| { Packed::new(RawLine::new( @@ -340,17 +337,13 @@ impl Packed { }) }; - let theme = elem.theme(styles).as_ref().as_ref().map(|theme_path| { - theme_path.as_ref().map(|path| { - load_theme(path, elem.theme_data(styles).as_ref().as_ref().unwrap()) - .unwrap() - }) - }); - let theme: &Theme = match theme { + let syntaxes = LazyCell::new(|| elem.syntaxes(styles)); + let theme: &synt::Theme = match elem.theme(styles) { Smart::Auto => &RAW_THEME, - Smart::Custom(Some(ref theme)) => theme, + Smart::Custom(Some(theme)) => theme.derived.get(), Smart::Custom(None) => return non_highlighted_result(lines).collect(), }; + let foreground = theme.settings.foreground.unwrap_or(synt::Color::BLACK); let mut seq = vec![]; @@ -391,13 +384,14 @@ impl Packed { ) .highlight(); } else if let Some((syntax_set, syntax)) = lang.and_then(|token| { - RAW_SYNTAXES - .find_syntax_by_token(&token) - .map(|syntax| (&*RAW_SYNTAXES, syntax)) - .or_else(|| { - extra_syntaxes - .find_syntax_by_token(&token) - .map(|syntax| (&**extra_syntaxes, syntax)) + // Prefer user-provided syntaxes over built-in ones. + syntaxes + .derived + .iter() + .map(|syntax| syntax.get()) + .chain(std::iter::once(&*RAW_SYNTAXES)) + .find_map(|set| { + set.find_syntax_by_token(&token).map(|syntax| (set, syntax)) }) }) { let mut highlighter = syntect::easy::HighlightLines::new(syntax, theme); @@ -532,6 +526,89 @@ cast! { v: EcoString => Self::Text(v), } +/// A loaded syntax. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct RawSyntax(Arc>); + +impl RawSyntax { + /// Load syntaxes from sources. + fn load( + world: Tracked, + sources: Spanned>, + ) -> SourceResult, Vec>> { + let data = sources.load(world)?; + let list = sources + .v + .0 + .iter() + .zip(&data) + .map(|(source, data)| Self::decode(source, data)) + .collect::>() + .at(sources.span)?; + Ok(Derived::new(sources.v, list)) + } + + /// Decode a syntax from a loaded source. + #[comemo::memoize] + #[typst_macros::time(name = "load syntaxes")] + fn decode(source: &DataSource, data: &Bytes) -> StrResult { + let src = data.as_str().map_err(FileError::from)?; + let syntax = SyntaxDefinition::load_from_str(src, false, None).map_err( + |err| match source { + DataSource::Path(path) => { + eco_format!("failed to parse syntax file `{path}` ({err})") + } + DataSource::Bytes(_) => { + eco_format!("failed to parse syntax ({err})") + } + }, + )?; + + let mut builder = SyntaxSetBuilder::new(); + builder.add(syntax); + + Ok(RawSyntax(Arc::new(ManuallyHash::new( + builder.build(), + typst_utils::hash128(data), + )))) + } + + /// Return the underlying syntax set. + fn get(&self) -> &SyntaxSet { + self.0.as_ref() + } +} + +/// A loaded syntect theme. +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct RawTheme(Arc>); + +impl RawTheme { + /// Load a theme from a data source. + fn load( + world: Tracked, + source: Spanned, + ) -> SourceResult> { + let data = source.load(world)?; + let theme = Self::decode(&data).at(source.span)?; + Ok(Derived::new(source.v, theme)) + } + + /// Decode a theme from bytes. + #[comemo::memoize] + fn decode(data: &Bytes) -> StrResult { + let mut cursor = std::io::Cursor::new(data.as_slice()); + let theme = synt::ThemeSet::load_from_reader(&mut cursor) + .map_err(|err| eco_format!("failed to parse theme ({err})"))?; + Ok(RawTheme(Arc::new(ManuallyHash::new(theme, typst_utils::hash128(data))))) + } + + /// Get the underlying syntect theme. + pub fn get(&self) -> &synt::Theme { + self.0.as_ref() + } +} + /// A highlighted line of raw text. /// /// This is a helper element that is synthesized by [`raw`] elements. @@ -593,6 +670,11 @@ struct ThemedHighlighter<'a> { line_fn: LineFn<'a>, } +// Shorthands for highlighter closures. +type StyleFn<'a> = + &'a mut dyn FnMut(usize, &LinkedNode, Range, synt::Style) -> Content; +type LineFn<'a> = &'a mut dyn FnMut(usize, Range, &mut Vec); + impl<'a> ThemedHighlighter<'a> { pub fn new( code: &'a str, @@ -738,108 +820,50 @@ fn to_syn(color: Color) -> synt::Color { synt::Color { r, g, b, a } } -/// A list of raw syntax file paths. -#[derive(Debug, Default, Clone, PartialEq, Hash)] -pub struct SyntaxPaths(Vec); - -cast! { - SyntaxPaths, - self => self.0.into_value(), - v: EcoString => Self(vec![v]), - v: Array => Self(v.into_iter().map(Value::cast).collect::>()?), -} - -impl Fold for SyntaxPaths { - fn fold(self, outer: Self) -> Self { - Self(self.0.fold(outer.0)) +/// Create a syntect theme item. +fn item( + scope: &str, + color: Option<&str>, + font_style: Option, +) -> synt::ThemeItem { + synt::ThemeItem { + scope: scope.parse().unwrap(), + style: synt::StyleModifier { + foreground: color.map(|s| to_syn(s.parse::().unwrap())), + background: None, + font_style, + }, } } -/// Load a syntax set from a list of syntax file paths. -#[comemo::memoize] -#[typst_macros::time(name = "load syntaxes")] -fn load_syntaxes(paths: &SyntaxPaths, bytes: &[Bytes]) -> StrResult> { - let mut out = SyntaxSetBuilder::new(); +/// Replace tabs with spaces to align with multiples of `tab_size`. +fn align_tabs(text: &str, tab_size: usize) -> EcoString { + let replacement = " ".repeat(tab_size); + let divisor = tab_size.max(1); + let amount = text.chars().filter(|&c| c == '\t').count(); - // We might have multiple sublime-syntax/yaml files - for (path, bytes) in paths.0.iter().zip(bytes.iter()) { - let src = std::str::from_utf8(bytes).map_err(FileError::from)?; - out.add(SyntaxDefinition::load_from_str(src, false, None).map_err(|err| { - eco_format!("failed to parse syntax file `{path}` ({err})") - })?); + let mut res = EcoString::with_capacity(text.len() - amount + amount * tab_size); + let mut column = 0; + + for grapheme in text.graphemes(true) { + match grapheme { + "\t" => { + let required = tab_size - column % divisor; + res.push_str(&replacement[..required]); + column += required; + } + "\n" => { + res.push_str(grapheme); + column = 0; + } + _ => { + res.push_str(grapheme); + column += 1; + } + } } - Ok(Arc::new(out.build())) -} - -/// Function to parse the syntaxes argument. -/// Much nicer than having it be part of the `element` macro. -fn parse_syntaxes( - engine: &mut Engine, - args: &mut Args, -) -> SourceResult<(Option, Option>)> { - let Some(Spanned { v: paths, span }) = - args.named::>("syntaxes")? - else { - return Ok((None, None)); - }; - - // Load syntax files. - let data = paths - .0 - .iter() - .map(|path| { - let id = span.resolve_path(path).at(span)?; - engine.world.file(id).at(span) - }) - .collect::>>()?; - - // Check that parsing works. - let _ = load_syntaxes(&paths, &data).at(span)?; - - Ok((Some(paths), Some(data))) -} - -#[comemo::memoize] -#[typst_macros::time(name = "load theme")] -fn load_theme(path: &str, bytes: &Bytes) -> StrResult> { - let mut cursor = std::io::Cursor::new(bytes.as_slice()); - - synt::ThemeSet::load_from_reader(&mut cursor) - .map(Arc::new) - .map_err(|err| eco_format!("failed to parse theme file `{path}` ({err})")) -} - -/// Function to parse the theme argument. -/// Much nicer than having it be part of the `element` macro. -fn parse_theme( - engine: &mut Engine, - args: &mut Args, -) -> SourceResult<(Option, Option)> { - let Some(Spanned { v: path, span }) = args.named::>("theme")? - else { - // Argument `theme` not found. - return Ok((None, None)); - }; - - let Smart::Custom(path) = path else { - // Argument `theme` is `auto`. - return Ok((Some(Smart::Auto), None)); - }; - - let Some(path) = path else { - // Argument `theme` is `none`. - return Ok((Some(Smart::Custom(None)), None)); - }; - - // Load theme file. - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - - // Check that parsing works. - let _ = load_theme(&path, &data).at(span)?; - - Ok((Some(Smart::Custom(Some(path))), Some(data))) + res } /// The syntect syntax definitions. @@ -886,49 +910,3 @@ pub static RAW_THEME: LazyLock = LazyLock::new(|| synt::Theme { item("markup.deleted, meta.diff.header.from-file", Some("#d73a49"), None), ], }); - -/// Create a syntect theme item. -fn item( - scope: &str, - color: Option<&str>, - font_style: Option, -) -> synt::ThemeItem { - synt::ThemeItem { - scope: scope.parse().unwrap(), - style: synt::StyleModifier { - foreground: color.map(|s| to_syn(s.parse::().unwrap())), - background: None, - font_style, - }, - } -} - -/// Replace tabs with spaces to align with multiples of `tab_size`. -fn align_tabs(text: &str, tab_size: usize) -> EcoString { - let replacement = " ".repeat(tab_size); - let divisor = tab_size.max(1); - let amount = text.chars().filter(|&c| c == '\t').count(); - - let mut res = EcoString::with_capacity(text.len() - amount + amount * tab_size); - let mut column = 0; - - for grapheme in text.graphemes(true) { - match grapheme { - "\t" => { - let required = tab_size - column % divisor; - res.push_str(&replacement[..required]); - column += required; - } - "\n" => { - res.push_str(grapheme); - column = 0; - } - _ => { - res.push_str(grapheme); - column += 1; - } - } - } - - res -} diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 452bb65c1..0f0602011 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -14,14 +14,14 @@ use ecow::EcoString; use typst_syntax::{Span, Spanned}; use typst_utils::LazyHash; -use crate::diag::{At, SourceResult, StrResult}; +use crate::diag::{SourceResult, StrResult}; use crate::engine::Engine; use crate::foundations::{ - cast, elem, func, scope, Bytes, Cast, Content, NativeElement, Packed, Show, Smart, - StyleChain, + cast, elem, func, scope, Bytes, Cast, Content, Derived, NativeElement, Packed, Show, + Smart, StyleChain, }; use crate::layout::{BlockElem, Length, Rel, Sizing}; -use crate::loading::Readable; +use crate::loading::{DataSource, Load, Readable}; use crate::model::Figurable; use crate::text::LocalName; use crate::World; @@ -46,25 +46,16 @@ use crate::World; /// ``` #[elem(scope, Show, LocalName, Figurable)] pub struct ImageElem { - /// Path to an image file. + /// A path to an image file or raw bytes making up an encoded image. /// - /// For more details, see the [Paths section]($syntax/#paths). + /// For more details about paths, see the [Paths section]($syntax/#paths). #[required] #[parse( - let Spanned { v: path, span } = - args.expect::>("path to image file")?; - let id = span.resolve_path(&path).at(span)?; - let data = engine.world.file(id).at(span)?; - path + let source = args.expect::>("source")?; + let data = source.load(engine.world)?; + Derived::new(source.v, data) )] - #[borrowed] - pub path: EcoString, - - /// The raw file data. - #[internal] - #[required] - #[parse(Readable::Bytes(data))] - pub data: Readable, + pub source: Derived, /// The image's format. Detected automatically by default. /// @@ -106,6 +97,9 @@ pub struct ImageElem { impl ImageElem { /// Decode a raster or vector graphic from bytes or a string. /// + /// This function is deprecated. The [`image`] function now accepts bytes + /// directly. + /// /// ```example /// #let original = read("diagram.svg") /// #let changed = original.replace( @@ -138,7 +132,9 @@ impl ImageElem { #[named] fit: Option, ) -> StrResult { - let mut elem = ImageElem::new(EcoString::new(), data); + let bytes = data.into_bytes(); + let source = Derived::new(DataSource::Bytes(bytes.clone()), bytes); + let mut elem = ImageElem::new(source); if let Some(format) = format { elem.push_format(format); } @@ -337,6 +333,22 @@ pub enum ImageFormat { Vector(VectorFormat), } +impl ImageFormat { + /// Try to detect the format of an image from data. + pub fn detect(data: &[u8]) -> Option { + if let Some(format) = RasterFormat::detect(data) { + return Some(Self::Raster(format)); + } + + // SVG or compressed SVG. + if data.starts_with(b"().unwrap(); - if embed.resolved_path.len() > Str::PDFA_LIMIT { + if embed.path.derived.len() > Str::PDFA_LIMIT { bail!(embed.span(), "embedded file path is too long"); } let id = embed_file(ctx, &mut chunk, embed)?; - if embedded_files.insert(embed.resolved_path.clone(), id).is_some() { + if embedded_files.insert(embed.path.derived.clone(), id).is_some() { bail!( elem.span(), - "duplicate embedded file for path `{}`", embed.resolved_path; + "duplicate embedded file for path `{}`", embed.path.derived; hint: "embedded file paths must be unique", ); } @@ -92,8 +92,8 @@ fn embed_file( embedded_file.finish(); let mut file_spec = chunk.file_spec(file_spec_dict_ref); - file_spec.path(Str(embed.resolved_path.as_bytes())); - file_spec.unic_file(TextStr(&embed.resolved_path)); + file_spec.path(Str(embed.path.derived.as_bytes())); + file_spec.unic_file(TextStr(&embed.path.derived)); file_spec .insert(Name(b"EF")) .dict() diff --git a/crates/typst-utils/src/hash.rs b/crates/typst-utils/src/hash.rs index 3dbadbe20..9687da20b 100644 --- a/crates/typst-utils/src/hash.rs +++ b/crates/typst-utils/src/hash.rs @@ -162,3 +162,74 @@ impl Debug for LazyHash { self.value.fmt(f) } } + +/// A wrapper type with a manually computed hash. +/// +/// This can be used to turn an unhashable type into a hashable one where the +/// hash is provided manually. Typically, the hash is derived from the data +/// which was used to construct to the unhashable type. +/// +/// For instance, you could hash the bytes that were parsed into an unhashable +/// data structure. +/// +/// # Equality +/// Because Typst uses high-quality 128 bit hashes in all places, the risk of a +/// hash collision is reduced to an absolute minimum. Therefore, this type +/// additionally provides `PartialEq` and `Eq` implementations that compare by +/// hash instead of by value. For this to be correct, your hash implementation +/// **must feed all information relevant to the `PartialEq` impl to the +/// hasher.** +#[derive(Clone)] +pub struct ManuallyHash { + /// A manually computed hash. + hash: u128, + /// The underlying value. + value: T, +} + +impl ManuallyHash { + /// Wraps an item with a pre-computed hash. + /// + /// The hash should be computed with `typst_utils::hash128`. + #[inline] + pub fn new(value: T, hash: u128) -> Self { + Self { hash, value } + } + + /// Returns the wrapped value. + #[inline] + pub fn into_inner(self) -> T { + self.value + } +} + +impl Hash for ManuallyHash { + #[inline] + fn hash(&self, state: &mut H) { + state.write_u128(self.hash); + } +} + +impl Eq for ManuallyHash {} + +impl PartialEq for ManuallyHash { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.hash == other.hash + } +} + +impl Deref for ManuallyHash { + type Target = T; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.value + } +} + +impl Debug for ManuallyHash { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.value.fmt(f) + } +} diff --git a/crates/typst-utils/src/lib.rs b/crates/typst-utils/src/lib.rs index 61703250a..d392e4093 100644 --- a/crates/typst-utils/src/lib.rs +++ b/crates/typst-utils/src/lib.rs @@ -15,7 +15,7 @@ mod scalar; pub use self::bitset::{BitSet, SmallBitSet}; pub use self::deferred::Deferred; pub use self::duration::format_duration; -pub use self::hash::LazyHash; +pub use self::hash::{LazyHash, ManuallyHash}; pub use self::pico::{PicoStr, ResolvedPicoStr}; pub use self::round::{round_int_with_precision, round_with_precision}; pub use self::scalar::Scalar; diff --git a/tests/suite/pdf/embed.typ b/tests/suite/pdf/embed.typ index bb5c9316c..83f006d63 100644 --- a/tests/suite/pdf/embed.typ +++ b/tests/suite/pdf/embed.typ @@ -10,6 +10,16 @@ description: "Information about a secret project", ) +--- pdf-embed-bytes --- +#pdf.embed("hello.txt", read("/assets/text/hello.txt", encoding: none)) +#pdf.embed( + "a_file_name.txt", + read("/assets/text/hello.txt", encoding: none), + relationship: "supplement", + mime-type: "text/plain", + description: "A description", +) + --- pdf-embed-invalid-relationship --- #pdf.embed( "/assets/text/hello.txt", @@ -18,13 +28,3 @@ mime-type: "text/plain", description: "A test file", ) - ---- pdf-embed-decode --- -#pdf.embed.decode("hello.txt", read("/assets/text/hello.txt")) -#pdf.embed.decode( - "a_file_name.txt", - read("/assets/text/hello.txt"), - relationship: "supplement", - mime-type: "text/plain", - description: "A description", -) From be6629c7cbd00b06beab2b1477c4270859906cb2 Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 9 Jan 2025 10:49:06 +0000 Subject: [PATCH 04/10] Better math argument parsing (#5008) --- crates/typst-eval/src/call.rs | 3 +- crates/typst-library/src/math/mod.rs | 6 +- crates/typst-syntax/src/lexer.rs | 44 +++++ crates/typst-syntax/src/parser.rs | 171 ++++++++++-------- crates/typst-syntax/src/set.rs | 4 + tests/ref/math-call-named-args.png | Bin 0 -> 526 bytes .../ref/math-call-spread-shorthand-clash.png | Bin 0 -> 119 bytes tests/ref/math-mat-gaps.png | Bin 489 -> 1309 bytes tests/ref/math-mat-spread-1d.png | Bin 0 -> 1017 bytes tests/ref/math-mat-spread-2d.png | Bin 0 -> 3391 bytes tests/ref/math-mat-spread.png | Bin 0 -> 1814 bytes tests/suite/math/call.typ | 134 ++++++++++++++ tests/suite/math/mat.typ | 26 +++ 13 files changed, 308 insertions(+), 80 deletions(-) create mode 100644 tests/ref/math-call-named-args.png create mode 100644 tests/ref/math-call-spread-shorthand-clash.png create mode 100644 tests/ref/math-mat-spread-1d.png create mode 100644 tests/ref/math-mat-spread-2d.png create mode 100644 tests/ref/math-mat-spread.png diff --git a/crates/typst-eval/src/call.rs b/crates/typst-eval/src/call.rs index fc934cef5..0a9e1c486 100644 --- a/crates/typst-eval/src/call.rs +++ b/crates/typst-eval/src/call.rs @@ -685,8 +685,7 @@ mod tests { // Named-params. test(s, "$ foo(bar: y) $", &["foo"]); - // This should be updated when we improve named-param parsing: - test(s, "$ foo(x-y: 1, bar-z: 2) $", &["bar", "foo"]); + test(s, "$ foo(x-y: 1, bar-z: 2) $", &["foo"]); // Field access in math. test(s, "$ foo.bar $", &["foo"]); diff --git a/crates/typst-library/src/math/mod.rs b/crates/typst-library/src/math/mod.rs index 5a83c854f..3b4b133d9 100644 --- a/crates/typst-library/src/math/mod.rs +++ b/crates/typst-library/src/math/mod.rs @@ -82,8 +82,9 @@ use crate::text::TextElem; /// - Within them, Typst is still in "math mode". Thus, you can write math /// directly into them, but need to use hash syntax to pass code expressions /// (except for strings, which are available in the math syntax). -/// - They support positional and named arguments, but don't support trailing -/// content blocks and argument spreading. +/// - They support positional and named arguments, as well as argument +/// spreading. +/// - They don't support trailing content blocks. /// - They provide additional syntax for 2-dimensional argument lists. The /// semicolon (`;`) merges preceding arguments separated by commas into an /// array argument. @@ -92,6 +93,7 @@ use crate::text::TextElem; /// $ frac(a^2, 2) $ /// $ vec(1, 2, delim: "[") $ /// $ mat(1, 2; 3, 4) $ +/// $ mat(..#range(1, 5).chunks(2)) $ /// $ lim_x = /// op("lim", limits: #true)_x $ /// ``` diff --git a/crates/typst-syntax/src/lexer.rs b/crates/typst-syntax/src/lexer.rs index b0cb5c464..6b5d28162 100644 --- a/crates/typst-syntax/src/lexer.rs +++ b/crates/typst-syntax/src/lexer.rs @@ -616,6 +616,11 @@ impl Lexer<'_> { '~' if self.s.eat_if('>') => SyntaxKind::MathShorthand, '*' | '-' | '~' => SyntaxKind::MathShorthand, + '.' => SyntaxKind::Dot, + ',' => SyntaxKind::Comma, + ';' => SyntaxKind::Semicolon, + ')' => SyntaxKind::RightParen, + '#' => SyntaxKind::Hash, '_' => SyntaxKind::Underscore, '$' => SyntaxKind::Dollar, @@ -685,6 +690,45 @@ impl Lexer<'_> { } SyntaxKind::Text } + + /// Handle named arguments in math function call. + pub fn maybe_math_named_arg(&mut self, start: usize) -> Option { + let cursor = self.s.cursor(); + self.s.jump(start); + if self.s.eat_if(is_id_start) { + self.s.eat_while(is_id_continue); + // Check that a colon directly follows the identifier, and not the + // `:=` or `::=` math shorthands. + if self.s.at(':') && !self.s.at(":=") && !self.s.at("::=") { + // Check that the identifier is not just `_`. + let node = if self.s.from(start) != "_" { + SyntaxNode::leaf(SyntaxKind::Ident, self.s.from(start)) + } else { + let msg = SyntaxError::new("expected identifier, found underscore"); + SyntaxNode::error(msg, self.s.from(start)) + }; + return Some(node); + } + } + self.s.jump(cursor); + None + } + + /// Handle spread arguments in math function call. + pub fn maybe_math_spread_arg(&mut self, start: usize) -> Option { + let cursor = self.s.cursor(); + self.s.jump(start); + if self.s.eat_if("..") { + // Check that neither a space nor a dot follows the spread syntax. + // A dot would clash with the `...` math shorthand. + if !self.space_or_end() && !self.s.at('.') { + let node = SyntaxNode::leaf(SyntaxKind::Dots, self.s.from(start)); + return Some(node); + } + } + self.s.jump(cursor); + None + } } /// Code. diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index 6c1778c4a..335b8f1a2 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -217,16 +217,20 @@ fn math(p: &mut Parser, stop_set: SyntaxSet) { p.wrap(m, SyntaxKind::Math); } -/// Parses a sequence of math expressions. -fn math_exprs(p: &mut Parser, stop_set: SyntaxSet) { +/// Parses a sequence of math expressions. Returns the number of expressions +/// parsed. +fn math_exprs(p: &mut Parser, stop_set: SyntaxSet) -> usize { debug_assert!(stop_set.contains(SyntaxKind::End)); + let mut count = 0; while !p.at_set(stop_set) { if p.at_set(set::MATH_EXPR) { math_expr(p); + count += 1; } else { p.unexpected(); } } + count } /// Parses a single math expression: This includes math elements like @@ -254,6 +258,13 @@ fn math_expr_prec(p: &mut Parser, min_prec: usize, stop: SyntaxKind) { } } + SyntaxKind::Dot + | SyntaxKind::Comma + | SyntaxKind::Semicolon + | SyntaxKind::RightParen => { + p.convert_and_eat(SyntaxKind::Text); + } + SyntaxKind::Text | SyntaxKind::MathShorthand => { continuable = matches!( math_class(p.current_text()), @@ -398,7 +409,13 @@ fn math_delimited(p: &mut Parser) { while !p.at_set(syntax_set!(Dollar, End)) { if math_class(p.current_text()) == Some(MathClass::Closing) { p.wrap(m2, SyntaxKind::Math); - p.eat(); + // We could be at the shorthand `|]`, which shouldn't be converted + // to a `Text` kind. + if p.at(SyntaxKind::RightParen) { + p.convert_and_eat(SyntaxKind::Text); + } else { + p.eat(); + } p.wrap(m, SyntaxKind::MathDelimited); return; } @@ -455,94 +472,90 @@ fn math_args(p: &mut Parser) { let m = p.marker(); p.convert_and_eat(SyntaxKind::LeftParen); - let mut namable = true; - let mut named = None; + let mut positional = true; let mut has_arrays = false; - let mut array = p.marker(); - let mut arg = p.marker(); - // The number of math expressions per argument. - let mut count = 0; - while !p.at_set(syntax_set!(Dollar, End)) { - if namable - && (p.at(SyntaxKind::MathIdent) || p.at(SyntaxKind::Text)) - && p.text[p.current_end()..].starts_with(':') - { - p.convert_and_eat(SyntaxKind::Ident); - p.convert_and_eat(SyntaxKind::Colon); - named = Some(arg); - arg = p.marker(); - array = p.marker(); - } + let mut maybe_array_start = p.marker(); + let mut seen = HashSet::new(); + while !p.at_set(syntax_set!(End, Dollar, RightParen)) { + positional = math_arg(p, &mut seen); - match p.current_text() { - ")" => break, - ";" => { - maybe_wrap_in_math(p, arg, count, named); - p.wrap(array, SyntaxKind::Array); - p.convert_and_eat(SyntaxKind::Semicolon); - array = p.marker(); - arg = p.marker(); - count = 0; - namable = true; - named = None; - has_arrays = true; - continue; - } - "," => { - maybe_wrap_in_math(p, arg, count, named); - p.convert_and_eat(SyntaxKind::Comma); - arg = p.marker(); - count = 0; - namable = true; - if named.is_some() { - array = p.marker(); - named = None; + match p.current() { + SyntaxKind::Comma => { + p.eat(); + if !positional { + maybe_array_start = p.marker(); } - continue; } - _ => {} - } + SyntaxKind::Semicolon => { + if !positional { + maybe_array_start = p.marker(); + } - if p.at_set(set::MATH_EXPR) { - math_expr(p); - count += 1; - } else { - p.unexpected(); - } - - namable = false; - } - - if arg != p.marker() { - maybe_wrap_in_math(p, arg, count, named); - if named.is_some() { - array = p.marker(); + // Parses an array: `a, b, c;`. + // The semicolon merges preceding arguments separated by commas + // into an array argument. + p.wrap(maybe_array_start, SyntaxKind::Array); + p.eat(); + maybe_array_start = p.marker(); + has_arrays = true; + } + SyntaxKind::End | SyntaxKind::Dollar | SyntaxKind::RightParen => {} + _ => p.expected("comma or semicolon"), } } - if has_arrays && array != p.marker() { - p.wrap(array, SyntaxKind::Array); - } - - if p.at(SyntaxKind::Text) && p.current_text() == ")" { - p.convert_and_eat(SyntaxKind::RightParen); - } else { - p.expected("closing paren"); - p.balanced = false; + // Check if we need to wrap the preceding arguments in an array. + if maybe_array_start != p.marker() && has_arrays && positional { + p.wrap(maybe_array_start, SyntaxKind::Array); } + p.expect_closing_delimiter(m, SyntaxKind::RightParen); p.wrap(m, SyntaxKind::Args); } -/// Wrap math function arguments to join adjacent math content or create an -/// empty 'Math' node for when we have 0 args. +/// Parses a single argument in a math argument list. /// -/// We don't wrap when `count == 1`, since wrapping would change the type of the -/// expression from potentially non-content to content. Ex: `$ func(#12pt) $` -/// would change the type from size to content if wrapped. -fn maybe_wrap_in_math(p: &mut Parser, arg: Marker, count: usize, named: Option) { +/// Returns whether the parsed argument was positional or not. +fn math_arg<'s>(p: &mut Parser<'s>, seen: &mut HashSet<&'s str>) -> bool { + let m = p.marker(); + let start = p.current_start(); + + if p.at(SyntaxKind::Dot) { + // Parses a spread argument: `..args`. + if let Some(spread) = p.lexer.maybe_math_spread_arg(start) { + p.token.node = spread; + p.eat(); + math_expr(p); + p.wrap(m, SyntaxKind::Spread); + return true; + } + } + + let mut positional = true; + if p.at_set(syntax_set!(Text, MathIdent, Underscore)) { + // Parses a named argument: `thickness: #12pt`. + if let Some(named) = p.lexer.maybe_math_named_arg(start) { + p.token.node = named; + let text = p.current_text(); + p.eat(); + p.convert_and_eat(SyntaxKind::Colon); + if !seen.insert(text) { + p[m].convert_to_error(eco_format!("duplicate argument: {text}")); + } + positional = false; + } + } + + // Parses a normal positional argument. + let arg = p.marker(); + let count = math_exprs(p, syntax_set!(End, Dollar, Comma, Semicolon, RightParen)); if count == 0 { + // Named argument requires a value. + if !positional { + p.expected("expression"); + } + // Flush trivia so that the new empty Math node will be wrapped _inside_ // any `SyntaxKind::Array` elements created in `math_args`. // (And if we don't follow by wrapping in an array, it has no effect.) @@ -553,13 +566,19 @@ fn maybe_wrap_in_math(p: &mut Parser, arg: Marker, count: usize, named: OptionlYDU@OZcq8+xeR|OVW@dc;k#Z6&Sg_!P z!wbQ-37jv^by&cSeN6USz>8@v&%QN)TNnMK1Cap?JN!_`f&~i}oNRcbe|W?MPSuw? zE#TEdjEq^p>zOz;Q#F8@^rni+Nw8qSf_DpCsoB8Kvt2gu`6_PUPq}ou3{)ooSlDyV z4O~C@rh6J-Z4LlZxPfzJ27LbQ^;t*orH`#+I{+U-03HrHf@@=U-vRKf1VB&4q~pf@ zZcN~BSLXHAf&~i}oIto11fPK@0{9*T!KxFumFeAk^Cd7k3M^;OT+RAU;Gq(Ivj6~W zk}s~~1U}K=_!s^z@OoF=zz_Uk4}d}e;PlquxEq+L$$x9|J66Yn1q*h$KT)ZH31x;{ Q3jhEB07*qoM6N<$f;J`WvH$=8 literal 0 HcmV?d00001 diff --git a/tests/ref/math-call-spread-shorthand-clash.png b/tests/ref/math-call-spread-shorthand-clash.png new file mode 100644 index 0000000000000000000000000000000000000000..4129ef5d2200ad7666e413e0845a80cd8798cc16 GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^6+kS_0VEhE<%|3RQg)s$jv*Ddl7HAcG$dYm6xi*q zE4Q@*#9-fgl>h(#s(=6Qitmd4WBcO&%U}PE&3lf0Tyb=FVdQ&MBb@06@$x0ssI2 literal 0 HcmV?d00001 diff --git a/tests/ref/math-mat-gaps.png b/tests/ref/math-mat-gaps.png index 5c954766c7bd01ca2b3a9d3a5959873a35dcbbae..405358776c3c42f9e20a7ac48ae152506cf3dfa1 100644 GIT binary patch delta 1303 zcmV+y1?c+e1Dy(x7k_OC00000PZatl000E!NklcPa0;jeqW?GhtmxUHynDfG;a*KJ19c7$ zXhcfm0G-2@a-@`7`fSH$*gRuD!vXh@QXimmctjaeE(Pfvo^S?5{?UVoxxPn{IZ@~E z{1kxSqM1;FP^uM0!a|+HJ68g@nq6Z%5V=*LhFPAVbAPzr}&7Z|EFm z#A8(9c?cY-xihgHaEwIn@Y)O@ZWGb#5h%?*5ONX7UZs1uc?NLRmSt^3#JMrl4$N-W zJE8K6);;0#OeE)z$><@TtPi`+(~u7k~2M%1?pglS_TO@ukxX;cgV5vgV_> z9p*&j+}v$3e*#P@K|UN&3$UaVd?$Ny4zTDts0OPq^(krTh@7?A4xspaw|ux}9{&i_ z$&YcsrA4TEp7{$Y->#MquS^kyX`0;w@MbHD+UJpzxIjJ}@<0%FZ0xQ9h{-@v5{#U~ zQ-7tyjiXVx7v_VP`{ONJ;{j5mQ526tPP$b(d~6DeQ)#J{cs;b)Y-Ipduc9anL5^*T zbT~c|4IKf7?Re=rv;?#m6%J2AOJJE%VQV^CMim69&OzH609cUSx z+h?PG>cYViXKY9~bSGMdzLYgz;iF?{3sCqSD)+P907uiWh9dUECKNGo z@?m02fQqyf4?fNuKLNbo3aY?50Q>nzLp_MxUZaM2?-26gwgn*eOB|q@a}vePFY*vd zJ*9>jQ;B?-tTcok%yt2){a>QGG=CM5>~=LwS1|sB*=8rnZV_b}lO zdbRG10paW4(3`u_7!YPpKH54yYBMCvzBV)+Y&3rxn_)9-hM%7B-&bRxJ`iZnT1o%_ N002ovPDHLkV1nF(Wyt^l delta 476 zcmV<20VDpM3h4uo7k@Pf00000ugP${00057NklR@q(inw=kf(WD*#3Iv zQQx8Td;bHkzI}He8()-7!y4AGhCc)DoyWAL!O+02DuUGK41WcDnGGD(p>Cws3FKZO z59jT{)9N3Txu&r%z)6lge3l77EXq8?6mIDGOCQ&qce3EnrJjNgmSBwKyHRM;KXw}{QcU28=mx;svRA8&iW%r@#wK~=V zr2EBT3Jw5#0ZiH`L>|7&CRhQ-E%Gp)c7jwkM<`$>mN9L~t)~bLYgohI8h!&^m0`w3 S3eG%zR|d%uk&H|0z=f;($0H z4u~g8EGA0^uDaAQ#@|jGxIju5#}UUhE5*kGQGyXS$caAQ`PTO=m5Cw4VeDJhI9YAkVpF%<)J9p&8-yK+^;E$nh@Lhdp;`Eq9^U{*Q- zuHVxYZy}|12XhK-I{xY!=8h#}U+y5Dbi$uM@XC<$2ZtWv~M zxSse;vP$fb2WH6l2#?h=kjn~)*oHfsV?!}Z2h?swY>oMfcX<-3h#~p4jyR?n#XPAG z5nOp`4640UEH9^>v`|usK@9te^l?8v^b(-r*4;Ydd7u_oKmI*Pp{yUKeJ9{gv_@|K zLN=w9wDu@ai?e&W>2aV8p0T?%oU+b6Cn_*oTkKgv4^2_n+u}>(MYh|n-sv7(&AdYD z(F#&4(}5(T9M7E)N;4@q)NirCYEt1sDHZYu|5>0WA2@Izt znAG4FFdS3PM2cTcZt)ou_W`D>hg07uf%t=p*ZlLShx~wP^Kx?Z15eKg6`#=*UrQj> zo)wGz&APOGjEZZoMgu0TnAH1Gq;9=OdDBfiC)mPnGdg77Rf>!9QYSeimTQWe!_)(0 z^%n^8QYvieoP&$+VvFgDnba%`sAlziXj;KA-d}i-kD*{H4u7R7_Rd#}o#2^?_%ZbX z1L;};6(6rAwd$}+3_sy^G-5-HgTdFQp;WwRKvN8JxR@uY9-;5b0et*ECXReh>e~pl z82n*wq^^W&)vShjpe;^pREuvkxP26gJTSGJfd8O1g3M}hG5)F@R*7Y^tCNDa5E|xE zdY;2(^|Lq~ar<6UUu?m)ZqMai!<5##ok9F9%_t(LF0$rA9Uam9JxV=oIYFU21j<4I zv$RVo``UHHuu<`}S@LmKmkWZGk>gHg8=uvA`5gee@w538O=fj|0nTbc5FX)oUTOC5 zS)FH*K-va+;(99>%8Lw6S)agA%=Nlr$m{?E`4u8^6cdKQK%S(4zF5wdVEhQqF?LxD n{dp?~{*wrZ1LA;q(u@BB_|mxg8^q3M00000NkvXXu0mjfI)~vcUop8U~xBK)t-#PP&GN=~3Vkj^~ z8=?&i(S~S4w1FYo5N%+Ht^q{H#>QU1eqH&GO`A3uqWvH`JUsmJ<;%){%%4Bs5FH57 zrKP3&_wV1ibLXQ+k5mP`c<~}GE^gnxeTL{@iJmoUR(yOsg!bvvM^(VBTemiB*zon& zUmK!>CAv|gM!KhS=gzi+`sU4>hUiKl`i(c<&^@q8ry;tkh<@v>w{*`}UwwrgVu-FP zqCflWv!bFR8ZBG4%*)FQfatq-@2b!TXHm68U%GT@?b@|bQBgnt{IjZn#KgogW5#^& z!3P)~-iThga;37Mq@+Xz2Mi$=XXPFImt$0LC@wDcIEyN`qYDZO?%lhm>`-1_URGAd zUrx*o(c89d!&p>sQCb$;Z$(U(1rIi!SH6(4{A76JN$K0eXzOdgc=2N89rNbRi;Rq< zQJ9n&-DzHAn_0duUS45T`#BsOKXe1%jhAI?O#Y12jz#`GOwiL+o_ zg*6&qUhHG^m1N~T*l;d+$ua3^+qNxN*V}mi;)^eIyS%Wttl;^j24j^kyeKIv%YRNI zZouly!!ddc@hKrm^pht~x^(HH+jquo<9xDykGjk-Y5q^*+m=-AwbB=ovF+ z96fqe+2O!}19Rrg35@8Z#YdAC9+9U52M)y03O%B;va)1Er>y)vb^6!24t1sumg$qZH9H}bc`Gf4^f9u6zJf571({r?Z z+v*@12BAPKWe*G+HmtC)@ZrOUuf6t~8=^OF-hA-jL3vvGG+)}BKjwsYd##LzDu1EB z+kyoPly}UYJv%x&TDN(Xe$WQJdiAomh^kQFGsbq*YL>m~Q4bg^ZLxCaM5N%+HHbfhu z4GhtLU7~R+`RgG6oK3v(MckkW^v8;DP z|Ni|A(Z2EBci)L18e!3d2@^DZi2mIW4LWx0hz!9H?F3|ipMLtOJYBzj{iI2gw8p1l z!-g)2M*4+T+z{;qG-s11PnM@gjvPVvMFZ&<8E`I%M&t3xC!ZLioiKg+bZb60Gr%|B zd_#kjh~IwuP1yl?jfe^b=si3v6Ot8FD1)S_y?ggo8qu(J?%X*K#qps-hscmsc7Q&v z4d(Oh+qabk@4WMlb(&taYLzwpX5+?U%0lAG*@r*r9v6ckj+vmIKC)9a}j>Bfv#`uYyL;o;@We?&QgnEUGj_q^ynf;*6ZA zcC%?b+8c52m;K5YM7}TUhl7lyRjXFkoz$)28--|}$d6A$k*?dTSFhf_eY^F`vyc0e z5{IOpKa!Rz>lqU6WA2U`m_L*-8$AAR)E{rmSlycvwa z8#iv)W=~SkgLS!Cvt}Y8lPQu>Y`YjD?Bi9sO_E{!u#Mz*iudZh_uivno6p0VrXRY8 zjsD}uj~zAyVs)ghKCNH)H0+MnB9r5;?10Ikk0fbKsts^qDt|E~HR{ZnGtOHQ0YXSU zG%)`9_U)@Is9U!#ta&rC$7!Aj-F9%^Kwhs^$dnBL?^(;5t~MDc97YLkIp^)ic3YQ`H(&2T=8j z@m0B>f``k2N1Thqw&f1SwU02t_B#-4!q0BX=$jsb!Y+wMna}tLkZ6MQ z9!jvGLx&Q<6g35M0Q4?_5$&KX{qoB%acT!ZG*yTZk`<+9Iebck1`W6X9-cDf4D$5Y zv16R3jw^lq@y9NSM%O{j&H#xfXzkInbm>yj_eK17I*JI?5vYnJD*$b-OQOlWP+mav zo```rqL~nF)jUixF`egghgrkb6sPt$jI~{651BU@kJhyg{fSO&Ks4);Xi&R$?Ldge z(tzlSk$*~$>UQ?|c({xB&-C1w`?{xHyLKl|oKSW^4RfZ}APS755!JUa1>KYvV9ZJt z;zzr~!<$aI@Q;yqr|uS!iDbuELGj^>^+v%p4@jry`0?WsOA}B4PNn-m}%$xG9 ze1h>G}4H1Q`{plcGanmlS!Q*<&tJzWJ2=z=suv?2O0O|;BqH4dyj9;v1E>eUnL zLTU-48k9~CV9c;_Lx}FNCP5dQ9lwdjc0qTmY8>XhgtDM(*RJ9n6)TpDVO#Yc{iZfd zQDJ-hGQj7bf6f*1n`jpIH{X0y)i_{TB34%x;Jv|h=W|OU>Irr?+L~qut1Ni;-FIY-=2_n#prf7T^v=67P~|0lRiKHOhkb-+w>AEjWt?yJ}Ul039AX5&SvisryAV znt7Ry#a+b!4~S^VJ*q(4Q>PpfjhrhWQQ`rj0w-Er+T=;_Mxky#>)4P3@QzA; zl}qT~?1rMs(RGlDBCgR81$&T6JWK#BkYkieMiOR-HZVjRq74kuhG;{yfgw7i@jvI% V44U_U8)N_g002ovPDHLkV1kQrigN$} literal 0 HcmV?d00001 diff --git a/tests/ref/math-mat-spread.png b/tests/ref/math-mat-spread.png new file mode 100644 index 0000000000000000000000000000000000000000..dc8b2bf7e6b1ead308d6f3d127edb61bc7cb0c16 GIT binary patch literal 1814 zcmV+x2kH2UP)(>&(DjCi~IZg#>U2deSN;ZzOS#Z{QUgj;NY5?nlm#qDk>_vy1Hg&X4~7_ z$jHcqgoJu}diM7A@$vCPLqla{Wwf-k!otFFadFht)LmU&#l^*wlauc5?&am>Yinyp zMn->sf6dL!pP!$Sl9K1==Z%exVq#+6i?Z;{-TCS8%~6c_=kGEyGW_-UZEbCkkdQ(` zLQqgp;g`CcoSdPdp;lH_-QC^T*x1+C*TXJ#KtMo;hllOI)y-0k>b1^nY;4Fuf%4Mf zg@uKtrlv(jMJy~VT3TATxVWdMr+j>TfPjGT$=l(WyZP(#%Sne*Q&UJtNYGuB+=Z_8 z;Og3cs`S|9>$=hV@b&fE=ilGoxw*OIqQS{Ugzm-J^3&q?$uS4pTGCw?8{4t;+wtSj)sc~r>bB3Rsi~KjmmeP=qN1Xno}OuG zY0%KnS65f_)#Kse;p5}uwY9aCm6hh^=IZL|etv#TOiWQxQJ9#RR8&-CWMm;BA#`+f zv$M0nz`%lnf?;7{$H&Lk*4AojY91aQHa0ddFE3tRUZ|+3s;a8SIebk`O{1fu+S=MW zIy%hE%y4jUq@<)dIXRDykA{YZot>S%y}ilF$s;2pK|w)FOH0|=+3V};DJdy+b#3vQn9hIva+&ZU|?@=Z?3Mc;^N{oG&IZ0%jxOq z?d|RG@bL2T^6&5OI5;@-^YcDFKK1qWSXfwS@00Y`dL_t(|+U?kPP?Ki>$MG+a zkU;{13MxaHDj@DHSUc@*)!M!H-h1!8_uhNjJ+)Qax++efs8E$k!2yzltp3q^?s@J= za^!MHeZqP7e*Svz_r90!-J82N?*)UwVA$)WOd^~qlXw_=s2P$_%(*sr=UvdR)CJZ9 zD?rG~%pPxt#3RtIUm`%CG!TkLQe6P1QnV|`4Gr7+GPM}^%!_93;i$0E*qQ{as6n&z zZ(TrybX|)K+nbafMv}9=`~+9Abee0TU!M1mKVrN z2I}CoXzsC)mkun-p?m9J0n8&RzHdhIFGcun4P-fiiATfv z7t8_AzDP4FD=VRN3`*G{Fz>NPq2U~)ODZd8!buijAW^*eDqz{g<}FA~K7^%yz%dj^ zM=C5QggF~f+K@Wzgb1DtEdG=zY8?Q!{Ga?Fq^w$va3VlXHvdmj8PT*&Kv=b?US4TM zgmIsMX2J15djgI(5UfAc&2IuGO-8r<8=!dv6JAG|5AdTO-73BX%Bv4ZMtc8)CirFn z`mG%Xk;dhsUtI zYs4@7>u;?cAk-~>`6WPY+QUJww-c$o5I}93!_gVxJt`3Ev@RQf&K+$*5Nt)Gkkqz8 z>#g49cW(xvzBmy;ZQ8%%0Bj9=Y^Y6l*dw0M{;;sxG>2RMf!)brVYO)vvmOG3h1I4z zyzZBuw}4>pv$7dboAxlSzDUWIWDB4+t>M37oz}${B~ zpP7g>Zm4H1Ah&qF>;)ju8}HR#A1vwYg>Xu-WZt#{XglgCQ>d1AEkC6|_)yo&W#<07*qoM6N<$ Ef(eM%`v3p{ literal 0 HcmV?d00001 diff --git a/tests/suite/math/call.typ b/tests/suite/math/call.typ index 136be8a74..5caacfac6 100644 --- a/tests/suite/math/call.typ +++ b/tests/suite/math/call.typ @@ -8,6 +8,112 @@ $ pi(a,) $ $ pi(a,b) $ $ pi(a,b,) $ +--- math-call-unclosed-func --- +#let func(x) = x +// Error: 6-7 unclosed delimiter +$func(a$ + +--- math-call-unclosed-non-func --- +// Error: 5-6 unclosed delimiter +$sin(x$ + +--- math-call-named-args --- +#let func1(my: none) = my +#let func2(_my: none) = _my +#let func3(my-body: none) = my-body +#let func4(_my-body: none) = _my-body +#let func5(m: none) = m +$ func1(my: a) $ +$ func2(_my: a) $ +$ func3(my-body: a) $ +$ func4(_my-body: a) $ +$ func5(m: a) $ +$ func5(m: sigma : f) $ +$ func5(m: sigma:pi) $ + +--- math-call-named-args-no-expr --- +#let func(m: none) = m +// Error: 10 expected expression +$ func(m: ) $ + +--- math-call-named-args-duplicate --- +#let func(my: none) = my +// Error: 15-17 duplicate argument: my +$ func(my: a, my: b) $ + +--- math-call-named-args-shorthand-clash-1 --- +#let func(m: none) = m +// Error: 18-21 unexpected argument +$func(m: =) func(m:=)$ + +--- math-call-named-args-shorthand-clash-2 --- +#let func(m: none) = m +// Error: 41-45 unexpected argument +$func(m::) func(m: :=) func(m:: =) func(m::=)$ + +--- math-call-named-single-underscore --- +#let func(x) = x +// Error: 8-9 expected identifier, found underscore +$ func(_: a) $ + +--- math-call-named-single-char-error --- +#let func(m: none) = m +// Error: 8-13 unexpected argument +$ func(m : a) $ + +--- math-call-named-args-repr --- +#let args(..body) = body +#let check(it, r) = test-repr(it.body.text, r) +#check($args(_a: a)$, "arguments(_a: [a])") +#check($args(_a-b: a)$, "arguments(_a-b: [a])") +#check($args(a-b: a)$, "arguments(a-b: [a])") +#check($args(a-b-c: a)$, "arguments(a-b-c: [a])") +#check($args(a--c: a)$, "arguments(a--c: [a])") +#check($args(a: a-b)$, "arguments(a: sequence([a], [−], [b]))") +#check($args(a-b: a-b)$, "arguments(a-b: sequence([a], [−], [b]))") +#check($args(a-b)$, "arguments(sequence([a], [−], [b]))") + +--- math-call-spread-content-error --- +#let args(..body) = body +// Error: 7-16 cannot spread content +$args(..(a + b))$ + +--- math-call-spread-multiple-exprs --- +#let args(..body) = body +// Error: 10 expected comma or semicolon +$args(..a + b)$ + +--- math-call-spread-unexpected-dots --- +#let args(..body) = body +// Error: 8-10 unexpected dots +$args(#..range(1, 5).chunks(2))$ + +--- math-call-spread-shorthand-clash --- +#let func(body) = body +$func(...)$ + +--- math-call-spread-repr --- +#let args(..body) = body +#let check(it, r) = test-repr(it.body.text, r) +#check($args(..#range(0, 4).chunks(2))$, "arguments((0, 1), (2, 3))") +#check($#args(range(1, 5).chunks(2))$, "arguments(((1, 2), (3, 4)))") +#check($#args(..range(1, 5).chunks(2))$, "arguments((1, 2), (3, 4))") +#check($args(#(..range(2, 6).chunks(2)))$, "arguments(((2, 3), (4, 5)))") +#let nums = range(0, 4).chunks(2) +#check($args(..nums)$, "arguments((0, 1), (2, 3))") +#check($args(..nums;)$, "arguments(((0, 1), (2, 3)))") +#check($args(..nums, ..nums)$, "arguments((0, 1), (2, 3), (0, 1), (2, 3))") +#check($args(..nums, 4, 5)$, "arguments((0, 1), (2, 3), [4], [5])") +#check($args(..nums, ..#range(4, 6))$, "arguments((0, 1), (2, 3), 4, 5)") +#check($args(..nums, #range(4, 6))$, "arguments((0, 1), (2, 3), (4, 5))") +#check($args(..nums, 1, 2; 3, 4)$, "arguments(((0, 1), (2, 3), [1], [2]), ([3], [4]))") +#check($args(1, 2; ..nums)$, "arguments(([1], [2]), ((0, 1), (2, 3)))") +#check($args(1, 2; 3, 4)$, "arguments(([1], [2]), ([3], [4]))") +#check($args(1, 2; 3, 4; ..#range(5, 7))$, "arguments(([1], [2]), ([3], [4]), (5, 6))") +#check($args(1, 2; 3, 4, ..#range(5, 7))$, "arguments(([1], [2]), ([3], [4], 5, 6))") +#check($args(1, 2; 3, 4, ..#range(5, 7);)$, "arguments(([1], [2]), ([3], [4], 5, 6))") +#check($args(1, 2; 3, 4, ..#range(5, 7),)$, "arguments(([1], [2]), ([3], [4], 5, 6))") + --- math-call-repr --- #let args(..body) = body #let check(it, r) = test-repr(it.body.text, r) @@ -35,6 +141,34 @@ $ mat(#"code"; "wins") $ #check($args(a,b;c)$, "arguments(([a], [b]), ([c],))") #check($args(a,b;c,d;e,f)$, "arguments(([a], [b]), ([c], [d]), ([e], [f]))") +--- math-call-2d-named-repr --- +#let args(..body) = (body.pos(), body.named()) +#let check(it, r) = test-repr(it.body.text, r) +#check($args(a: b)$, "((), (a: [b]))") +#check($args(1, 2; 3, 4)$, "((([1], [2]), ([3], [4])), (:))") +#check($args(a: b, 1, 2; 3, 4)$, "((([1], [2]), ([3], [4])), (a: [b]))") +#check($args(1, a: b, 2; 3, 4)$, "(([1], ([2],), ([3], [4])), (a: [b]))") +#check($args(1, 2, a: b; 3, 4)$, "(([1], [2], (), ([3], [4])), (a: [b]))") +#check($args(1, 2; a: b, 3, 4)$, "((([1], [2]), ([3], [4])), (a: [b]))") +#check($args(1, 2; 3, a: b, 4)$, "((([1], [2]), [3], ([4],)), (a: [b]))") +#check($args(1, 2; 3, 4, a: b)$, "((([1], [2]), [3], [4]), (a: [b]))") +#check($args(a: b, 1, 2, 3, c: d)$, "(([1], [2], [3]), (a: [b], c: [d]))") +#check($args(1, 2, 3; a: b)$, "((([1], [2], [3]),), (a: [b]))") +#check($args(a-b: a,, e:f;; d)$, "(([], (), ([],), ([d],)), (a-b: [a], e: [f]))") +#check($args(a: b, ..#range(0, 4))$, "((0, 1, 2, 3), (a: [b]))") + +--- math-call-2d-escape-repr --- +#let args(..body) = body +#let check(it, r) = test-repr(it.body.text, r) +#check($args(a\;b)$, "arguments(sequence([a], [;], [b]))") +#check($args(a\,b;c)$, "arguments((sequence([a], [,], [b]),), ([c],))") +#check($args(b\;c\,d;e)$, "arguments((sequence([b], [;], [c], [,], [d]),), ([e],))") +#check($args(a\: b)$, "arguments(sequence([a], [:], [ ], [b]))") +#check($args(a : b)$, "arguments(sequence([a], [ ], [:], [ ], [b]))") +#check($args(\..a)$, "arguments(sequence([.], [.], [a]))") +#check($args(.. a)$, "arguments(sequence([.], [.], [ ], [a]))") +#check($args(a..b)$, "arguments(sequence([a], [.], [.], [b]))") + --- math-call-2d-repr-structure --- #let args(..body) = body #let check(it, r) = test-repr(it.body.text, r) diff --git a/tests/suite/math/mat.typ b/tests/suite/math/mat.typ index 391ff1677..b7d6a6871 100644 --- a/tests/suite/math/mat.typ +++ b/tests/suite/math/mat.typ @@ -54,6 +54,30 @@ $ a + mat(delim: #none, 1, 2; 3, 4) + b $ $ mat(1, 2; 3, 4; delim: "[") $, ) +--- math-mat-spread --- +// Test argument spreading in matrix. +$ mat(..#range(1, 5).chunks(2)) + mat(#(..range(2).map(_ => range(2)))) $ + +#let nums = ((1,) * 5).intersperse(0).chunks(3) +$ mat(..nums, delim: "[") $ + +--- math-mat-spread-1d --- +$ mat(..#range(1, 5) ; 1, ..#range(2, 5)) + mat(..#range(1, 3), ..#range(3, 5) ; ..#range(1, 4), 4) $ + +--- math-mat-spread-2d --- +#let nums = range(0, 2).map(i => (i, i+1)) +$ mat(..nums, delim: "|",) + mat(..nums; delim: "|",) $ +$ mat(..nums) mat(..nums;) \ + mat(..nums;,) mat(..nums,) $ + +--- math-mat-spread-expected-array-error --- +#let nums = range(0, 2).map(i => (i, i+1)) +// Error: 15-16 expected array, found content +$ mat(..nums, 0, 1) $ + --- math-mat-gap --- #set math.mat(gap: 1em) $ mat(1, 2; 3, 4) $ @@ -61,6 +85,8 @@ $ mat(1, 2; 3, 4) $ --- math-mat-gaps --- #set math.mat(row-gap: 1em, column-gap: 2em) $ mat(1, 2; 3, 4) $ +$ mat(column-gap: #1em, 1, 2; 3, 4) + mat(row-gap: #2em, 1, 2; 3, 4) $ --- math-mat-augment --- // Test matrix line drawing (augmentation). From 9473aface183feaf48601c5264c3604f5798169e Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 9 Jan 2025 14:00:18 +0100 Subject: [PATCH 05/10] Fix memory size of `TextElem` (#5674) --- crates/typst-library/src/text/mod.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index 25ed009e9..d372c399f 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -555,6 +555,7 @@ pub struct TextElem { /// #lorem(10) /// ``` #[fold] + #[ghost] pub costs: Costs, /// Whether to apply kerning. @@ -1431,3 +1432,13 @@ fn check_font_list(engine: &mut Engine, list: &Spanned) { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_text_elem_size() { + assert_eq!(std::mem::size_of::(), std::mem::size_of::()); + } +} From 6b9b78596a6103dfbcadafaeb03eda624da5306a Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 10 Jan 2025 17:54:11 +0100 Subject: [PATCH 06/10] Don't generate accessors for required fields (#5680) --- crates/typst-layout/src/inline/collect.rs | 11 +++++----- crates/typst-layout/src/lists.rs | 4 ++-- crates/typst-layout/src/math/accent.rs | 6 +++--- crates/typst-layout/src/math/attach.rs | 12 +++++------ crates/typst-layout/src/math/cancel.rs | 2 +- crates/typst-layout/src/math/frac.rs | 6 +++--- crates/typst-layout/src/math/lr.rs | 9 ++++----- crates/typst-layout/src/math/mat.rs | 8 ++++---- crates/typst-layout/src/math/mod.rs | 13 ++++++------ crates/typst-layout/src/math/root.rs | 3 +-- crates/typst-layout/src/math/stretch.rs | 2 +- crates/typst-layout/src/math/text.rs | 2 +- crates/typst-layout/src/math/underover.rs | 20 +++++++++---------- crates/typst-layout/src/shapes.rs | 6 +++--- crates/typst-layout/src/stack.rs | 6 +++--- crates/typst-layout/src/transforms.rs | 6 +++--- .../src/introspection/counter.rs | 2 +- .../typst-library/src/introspection/state.rs | 2 +- crates/typst-library/src/layout/align.rs | 2 +- crates/typst-library/src/layout/container.rs | 2 +- crates/typst-library/src/layout/grid/mod.rs | 2 +- .../typst-library/src/layout/grid/resolve.rs | 12 +++++------ crates/typst-library/src/layout/hide.rs | 2 +- crates/typst-library/src/layout/layout.rs | 2 +- crates/typst-library/src/math/accent.rs | 2 +- crates/typst-library/src/math/attach.rs | 4 ++-- .../typst-library/src/model/bibliography.rs | 9 ++++----- crates/typst-library/src/model/figure.rs | 17 ++++++++-------- crates/typst-library/src/model/footnote.rs | 15 +++++++------- crates/typst-library/src/model/heading.rs | 4 ++-- crates/typst-library/src/model/link.rs | 7 +++---- crates/typst-library/src/model/outline.rs | 13 ++++++------ crates/typst-library/src/model/quote.rs | 2 +- crates/typst-library/src/model/reference.rs | 14 ++++++------- crates/typst-library/src/model/table.rs | 2 +- crates/typst-library/src/model/terms.rs | 6 +++--- crates/typst-library/src/text/deco.rs | 8 ++++---- crates/typst-library/src/text/mod.rs | 2 +- crates/typst-library/src/text/raw.rs | 8 ++++---- crates/typst-library/src/text/shift.rs | 8 ++++---- crates/typst-library/src/text/smallcaps.rs | 2 +- crates/typst-macros/src/elem.rs | 11 +++++++--- crates/typst-pdf/src/embed.rs | 2 +- crates/typst-pdf/src/outline.rs | 3 +-- 44 files changed, 137 insertions(+), 144 deletions(-) diff --git a/crates/typst-layout/src/inline/collect.rs b/crates/typst-layout/src/inline/collect.rs index 23e82c417..fcf7508e9 100644 --- a/crates/typst-layout/src/inline/collect.rs +++ b/crates/typst-layout/src/inline/collect.rs @@ -161,9 +161,9 @@ pub fn collect<'a>( } if let Some(case) = TextElem::case_in(styles) { - full.push_str(&case.apply(elem.text())); + full.push_str(&case.apply(&elem.text)); } else { - full.push_str(elem.text()); + full.push_str(&elem.text); } if dir != outer_dir { @@ -172,13 +172,12 @@ pub fn collect<'a>( } }); } else if let Some(elem) = child.to_packed::() { - let amount = elem.amount(); - if amount.is_zero() { + if elem.amount.is_zero() { continue; } - collector.push_item(match amount { - Spacing::Fr(fr) => Item::Fractional(*fr, None), + collector.push_item(match elem.amount { + Spacing::Fr(fr) => Item::Fractional(fr, None), Spacing::Rel(rel) => Item::Absolute( rel.resolve(styles).relative_to(region.x), elem.weak(styles), diff --git a/crates/typst-layout/src/lists.rs b/crates/typst-layout/src/lists.rs index 9479959b2..63127474b 100644 --- a/crates/typst-layout/src/lists.rs +++ b/crates/typst-layout/src/lists.rs @@ -40,7 +40,7 @@ pub fn layout_list( let mut cells = vec![]; let mut locator = locator.split(); - for item in elem.children() { + for item in &elem.children { cells.push(Cell::new(Content::empty(), locator.next(&()))); cells.push(Cell::new(marker.clone(), locator.next(&marker.span()))); cells.push(Cell::new(Content::empty(), locator.next(&()))); @@ -101,7 +101,7 @@ pub fn layout_enum( // relation to the item it refers to. let number_align = elem.number_align(styles); - for item in elem.children() { + for item in &elem.children { number = item.number(styles).unwrap_or(number); let context = Context::new(None, Some(styles)); diff --git a/crates/typst-layout/src/math/accent.rs b/crates/typst-layout/src/math/accent.rs index 0ebe785f1..951870d68 100644 --- a/crates/typst-layout/src/math/accent.rs +++ b/crates/typst-layout/src/math/accent.rs @@ -16,7 +16,7 @@ pub fn layout_accent( styles: StyleChain, ) -> SourceResult<()> { let cramped = style_cramped(); - let mut base = ctx.layout_into_fragment(elem.base(), styles.chain(&cramped))?; + let mut base = ctx.layout_into_fragment(&elem.base, styles.chain(&cramped))?; // Try to replace a glyph with its dotless variant. if let MathFragment::Glyph(glyph) = &mut base { @@ -29,8 +29,8 @@ pub fn layout_accent( let width = elem.size(styles).relative_to(base.width()); - let Accent(c) = elem.accent(); - let mut glyph = GlyphFragment::new(ctx, styles, *c, elem.span()); + let Accent(c) = elem.accent; + let mut glyph = GlyphFragment::new(ctx, styles, c, elem.span()); // Try to replace accent glyph with flattened variant. let flattened_base_height = scaled!(ctx, styles, flattened_accent_base_height); diff --git a/crates/typst-layout/src/math/attach.rs b/crates/typst-layout/src/math/attach.rs index 263fc5c6d..8a67d53b3 100644 --- a/crates/typst-layout/src/math/attach.rs +++ b/crates/typst-layout/src/math/attach.rs @@ -29,7 +29,7 @@ pub fn layout_attach( let elem = merged.as_ref().unwrap_or(elem); let stretch = stretch_size(styles, elem); - let mut base = ctx.layout_into_fragment(elem.base(), styles)?; + let mut base = ctx.layout_into_fragment(&elem.base, styles)?; let sup_style = style_for_superscript(styles); let sup_style_chain = styles.chain(&sup_style); let tl = elem.tl(sup_style_chain); @@ -95,7 +95,7 @@ pub fn layout_primes( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - match *elem.count() { + match elem.count { count @ 1..=4 => { let c = match count { 1 => '′', @@ -134,7 +134,7 @@ pub fn layout_scripts( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?; + let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?; fragment.set_limits(Limits::Never); ctx.push(fragment); Ok(()) @@ -148,7 +148,7 @@ pub fn layout_limits( styles: StyleChain, ) -> SourceResult<()> { let limits = if elem.inline(styles) { Limits::Always } else { Limits::Display }; - let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?; + let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?; fragment.set_limits(limits); ctx.push(fragment); Ok(()) @@ -157,9 +157,9 @@ pub fn layout_limits( /// Get the size to stretch the base to. fn stretch_size(styles: StyleChain, elem: &Packed) -> Option> { // Extract from an EquationElem. - let mut base = elem.base(); + let mut base = &elem.base; while let Some(equation) = base.to_packed::() { - base = equation.body(); + base = &equation.body; } base.to_packed::().map(|stretch| stretch.size(styles)) diff --git a/crates/typst-layout/src/math/cancel.rs b/crates/typst-layout/src/math/cancel.rs index 716832fbf..9826397fa 100644 --- a/crates/typst-layout/src/math/cancel.rs +++ b/crates/typst-layout/src/math/cancel.rs @@ -16,7 +16,7 @@ pub fn layout_cancel( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let body = ctx.layout_into_fragment(elem.body(), styles)?; + let body = ctx.layout_into_fragment(&elem.body, styles)?; // Preserve properties of body. let body_class = body.class(); diff --git a/crates/typst-layout/src/math/frac.rs b/crates/typst-layout/src/math/frac.rs index fdc3be172..63463d761 100644 --- a/crates/typst-layout/src/math/frac.rs +++ b/crates/typst-layout/src/math/frac.rs @@ -23,8 +23,8 @@ pub fn layout_frac( layout_frac_like( ctx, styles, - elem.num(), - std::slice::from_ref(elem.denom()), + &elem.num, + std::slice::from_ref(&elem.denom), false, elem.span(), ) @@ -37,7 +37,7 @@ pub fn layout_binom( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - layout_frac_like(ctx, styles, elem.upper(), elem.lower(), true, elem.span()) + layout_frac_like(ctx, styles, &elem.upper, &elem.lower, true, elem.span()) } /// Layout a fraction or binomial. diff --git a/crates/typst-layout/src/math/lr.rs b/crates/typst-layout/src/math/lr.rs index 2f4556fe5..19176ee88 100644 --- a/crates/typst-layout/src/math/lr.rs +++ b/crates/typst-layout/src/math/lr.rs @@ -13,17 +13,16 @@ pub fn layout_lr( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let mut body = elem.body(); - // Extract from an EquationElem. + let mut body = &elem.body; if let Some(equation) = body.to_packed::() { - body = equation.body(); + body = &equation.body; } // Extract implicit LrElem. if let Some(lr) = body.to_packed::() { if lr.size(styles).is_one() { - body = lr.body(); + body = &lr.body; } } @@ -100,7 +99,7 @@ pub fn layout_mid( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let mut fragments = ctx.layout_into_fragments(elem.body(), styles)?; + let mut fragments = ctx.layout_into_fragments(&elem.body, styles)?; for fragment in &mut fragments { match fragment { diff --git a/crates/typst-layout/src/math/mat.rs b/crates/typst-layout/src/math/mat.rs index d28bb037d..bf4929026 100644 --- a/crates/typst-layout/src/math/mat.rs +++ b/crates/typst-layout/src/math/mat.rs @@ -27,7 +27,7 @@ pub fn layout_vec( let frame = layout_vec_body( ctx, styles, - elem.children(), + &elem.children, elem.align(styles), elem.gap(styles), LeftRightAlternator::Right, @@ -44,7 +44,7 @@ pub fn layout_mat( styles: StyleChain, ) -> SourceResult<()> { let augment = elem.augment(styles); - let rows = elem.rows(); + let rows = &elem.rows; if let Some(aug) = &augment { for &offset in &aug.hline.0 { @@ -58,7 +58,7 @@ pub fn layout_mat( } } - let ncols = elem.rows().first().map_or(0, |row| row.len()); + let ncols = rows.first().map_or(0, |row| row.len()); for &offset in &aug.vline.0 { if offset == 0 || offset.unsigned_abs() >= ncols { @@ -97,7 +97,7 @@ pub fn layout_cases( let frame = layout_vec_body( ctx, styles, - elem.children(), + &elem.children, FixedAlignment::Start, elem.gap(styles), LeftRightAlternator::None, diff --git a/crates/typst-layout/src/math/mod.rs b/crates/typst-layout/src/math/mod.rs index 62ecd1725..06dc6653b 100644 --- a/crates/typst-layout/src/math/mod.rs +++ b/crates/typst-layout/src/math/mod.rs @@ -632,7 +632,7 @@ fn layout_h( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - if let Spacing::Rel(rel) = elem.amount() { + if let Spacing::Rel(rel) = elem.amount { if rel.rel.is_zero() { ctx.push(MathFragment::Spacing(rel.abs.resolve(styles), elem.weak(styles))); } @@ -647,11 +647,10 @@ fn layout_class( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let class = *elem.class(); - let style = EquationElem::set_class(Some(class)).wrap(); - let mut fragment = ctx.layout_into_fragment(elem.body(), styles.chain(&style))?; - fragment.set_class(class); - fragment.set_limits(Limits::for_class(class)); + let style = EquationElem::set_class(Some(elem.class)).wrap(); + let mut fragment = ctx.layout_into_fragment(&elem.body, styles.chain(&style))?; + fragment.set_class(elem.class); + fragment.set_limits(Limits::for_class(elem.class)); ctx.push(fragment); Ok(()) } @@ -663,7 +662,7 @@ fn layout_op( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let fragment = ctx.layout_into_fragment(elem.text(), styles)?; + let fragment = ctx.layout_into_fragment(&elem.text, styles)?; let italics = fragment.italics_correction(); let accent_attach = fragment.accent_attach(); let text_like = fragment.is_text_like(); diff --git a/crates/typst-layout/src/math/root.rs b/crates/typst-layout/src/math/root.rs index 4e5d844f2..a6b5c03d0 100644 --- a/crates/typst-layout/src/math/root.rs +++ b/crates/typst-layout/src/math/root.rs @@ -18,7 +18,6 @@ pub fn layout_root( styles: StyleChain, ) -> SourceResult<()> { let index = elem.index(styles); - let radicand = elem.radicand(); let span = elem.span(); let gap = scaled!( @@ -36,7 +35,7 @@ pub fn layout_root( let radicand = { let cramped = style_cramped(); let styles = styles.chain(&cramped); - let run = ctx.layout_into_run(radicand, styles)?; + let run = ctx.layout_into_run(&elem.radicand, styles)?; let multiline = run.is_multiline(); let mut radicand = run.into_fragment(styles).into_frame(); if multiline { diff --git a/crates/typst-layout/src/math/stretch.rs b/crates/typst-layout/src/math/stretch.rs index 4bc5a9262..6379bdb2e 100644 --- a/crates/typst-layout/src/math/stretch.rs +++ b/crates/typst-layout/src/math/stretch.rs @@ -21,7 +21,7 @@ pub fn layout_stretch( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let mut fragment = ctx.layout_into_fragment(elem.body(), styles)?; + let mut fragment = ctx.layout_into_fragment(&elem.body, styles)?; stretch_fragment( ctx, styles, diff --git a/crates/typst-layout/src/math/text.rs b/crates/typst-layout/src/math/text.rs index eb30373dd..7e849c46c 100644 --- a/crates/typst-layout/src/math/text.rs +++ b/crates/typst-layout/src/math/text.rs @@ -20,7 +20,7 @@ pub fn layout_text( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - let text = elem.text(); + let text = &elem.text; let span = elem.span(); let mut chars = text.chars(); let math_size = EquationElem::size_in(styles); diff --git a/crates/typst-layout/src/math/underover.rs b/crates/typst-layout/src/math/underover.rs index e55996389..7b3617c3e 100644 --- a/crates/typst-layout/src/math/underover.rs +++ b/crates/typst-layout/src/math/underover.rs @@ -32,7 +32,7 @@ pub fn layout_underline( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - layout_underoverline(ctx, styles, elem.body(), elem.span(), Position::Under) + layout_underoverline(ctx, styles, &elem.body, elem.span(), Position::Under) } /// Lays out an [`OverlineElem`]. @@ -42,7 +42,7 @@ pub fn layout_overline( ctx: &mut MathContext, styles: StyleChain, ) -> SourceResult<()> { - layout_underoverline(ctx, styles, elem.body(), elem.span(), Position::Over) + layout_underoverline(ctx, styles, &elem.body, elem.span(), Position::Over) } /// Lays out an [`UnderbraceElem`]. @@ -55,7 +55,7 @@ pub fn layout_underbrace( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⏟', BRACE_GAP, @@ -74,7 +74,7 @@ pub fn layout_overbrace( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⏞', BRACE_GAP, @@ -93,7 +93,7 @@ pub fn layout_underbracket( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⎵', BRACKET_GAP, @@ -112,7 +112,7 @@ pub fn layout_overbracket( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⎴', BRACKET_GAP, @@ -131,7 +131,7 @@ pub fn layout_underparen( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⏝', PAREN_GAP, @@ -150,7 +150,7 @@ pub fn layout_overparen( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⏜', PAREN_GAP, @@ -169,7 +169,7 @@ pub fn layout_undershell( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⏡', SHELL_GAP, @@ -188,7 +188,7 @@ pub fn layout_overshell( layout_underoverspreader( ctx, styles, - elem.body(), + &elem.body, &elem.annotation(styles), '⏠', SHELL_GAP, diff --git a/crates/typst-layout/src/shapes.rs b/crates/typst-layout/src/shapes.rs index 7c56bf763..eb665f06a 100644 --- a/crates/typst-layout/src/shapes.rs +++ b/crates/typst-layout/src/shapes.rs @@ -62,7 +62,7 @@ pub fn layout_path( axes.resolve(styles).zip_map(region.size, Rel::relative_to).to_point() }; - let vertices = elem.vertices(); + let vertices = &elem.vertices; let points: Vec = vertices.iter().map(|c| resolve(c.vertex())).collect(); let mut size = Size::zero(); @@ -150,7 +150,7 @@ pub fn layout_curve( ) -> SourceResult { let mut builder = CurveBuilder::new(region, styles); - for item in elem.components() { + for item in &elem.components { match item { CurveComponent::Move(element) => { let relative = element.relative(styles); @@ -399,7 +399,7 @@ pub fn layout_polygon( region: Region, ) -> SourceResult { let points: Vec = elem - .vertices() + .vertices .iter() .map(|c| c.resolve(styles).zip_map(region.size, Rel::relative_to).to_point()) .collect(); diff --git a/crates/typst-layout/src/stack.rs b/crates/typst-layout/src/stack.rs index a3ebc9f36..c468945eb 100644 --- a/crates/typst-layout/src/stack.rs +++ b/crates/typst-layout/src/stack.rs @@ -27,7 +27,7 @@ pub fn layout_stack( let spacing = elem.spacing(styles); let mut deferred = None; - for child in elem.children() { + for child in &elem.children { match child { StackChild::Spacing(kind) => { layouter.layout_spacing(*kind); @@ -36,14 +36,14 @@ pub fn layout_stack( StackChild::Block(block) => { // Transparently handle `h`. if let (Axis::X, Some(h)) = (axis, block.to_packed::()) { - layouter.layout_spacing(*h.amount()); + layouter.layout_spacing(h.amount); deferred = None; continue; } // Transparently handle `v`. if let (Axis::Y, Some(v)) = (axis, block.to_packed::()) { - layouter.layout_spacing(*v.amount()); + layouter.layout_spacing(v.amount); deferred = None; continue; } diff --git a/crates/typst-layout/src/transforms.rs b/crates/typst-layout/src/transforms.rs index e0f29c4c2..f4526dd09 100644 --- a/crates/typst-layout/src/transforms.rs +++ b/crates/typst-layout/src/transforms.rs @@ -52,7 +52,7 @@ pub fn layout_rotate( region, size, styles, - elem.body(), + &elem.body, Transform::rotate(angle), align, elem.reflow(styles), @@ -81,7 +81,7 @@ pub fn layout_scale( region, size, styles, - elem.body(), + &elem.body, Transform::scale(scale.x, scale.y), elem.origin(styles).resolve(styles), elem.reflow(styles), @@ -169,7 +169,7 @@ pub fn layout_skew( region, size, styles, - elem.body(), + &elem.body, Transform::skew(ax, ay), align, elem.reflow(styles), diff --git a/crates/typst-library/src/introspection/counter.rs b/crates/typst-library/src/introspection/counter.rs index e189103d9..d26a9f9f5 100644 --- a/crates/typst-library/src/introspection/counter.rs +++ b/crates/typst-library/src/introspection/counter.rs @@ -800,7 +800,7 @@ impl ManualPageCounter { let Some(elem) = elem.to_packed::() else { continue; }; - if *elem.key() == CounterKey::Page { + if elem.key == CounterKey::Page { let mut state = CounterState(smallvec![self.logical]); state.update(engine, elem.update.clone())?; self.logical = state.first(); diff --git a/crates/typst-library/src/introspection/state.rs b/crates/typst-library/src/introspection/state.rs index 7e019e6c7..e6ab926bf 100644 --- a/crates/typst-library/src/introspection/state.rs +++ b/crates/typst-library/src/introspection/state.rs @@ -245,7 +245,7 @@ impl State { for elem in introspector.query(&self.selector()) { let elem = elem.to_packed::().unwrap(); - match elem.update() { + match &elem.update { StateUpdate::Set(value) => state = value.clone(), StateUpdate::Func(func) => { state = func.call(&mut engine, Context::none().track(), [state])? diff --git a/crates/typst-library/src/layout/align.rs b/crates/typst-library/src/layout/align.rs index e8ba4d7c3..5604d6831 100644 --- a/crates/typst-library/src/layout/align.rs +++ b/crates/typst-library/src/layout/align.rs @@ -100,7 +100,7 @@ pub struct AlignElem { impl Show for Packed { #[typst_macros::time(name = "align", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(self.body().clone().aligned(self.alignment(styles))) + Ok(self.body.clone().aligned(self.alignment(styles))) } } diff --git a/crates/typst-library/src/layout/container.rs b/crates/typst-library/src/layout/container.rs index 266d1d88f..c8c74269b 100644 --- a/crates/typst-library/src/layout/container.rs +++ b/crates/typst-library/src/layout/container.rs @@ -166,7 +166,7 @@ impl Packed { styles: StyleChain, region: Size, ) -> SourceResult> { - self.body().call(engine, locator, styles, region) + self.body.call(engine, locator, styles, region) } } diff --git a/crates/typst-library/src/layout/grid/mod.rs b/crates/typst-library/src/layout/grid/mod.rs index e46440fb4..6616c3311 100644 --- a/crates/typst-library/src/layout/grid/mod.rs +++ b/crates/typst-library/src/layout/grid/mod.rs @@ -749,7 +749,7 @@ cast! { impl Show for Packed { fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult { - show_grid_cell(self.body().clone(), self.inset(styles), self.align(styles)) + show_grid_cell(self.body.clone(), self.inset(styles), self.align(styles)) } } diff --git a/crates/typst-library/src/layout/grid/resolve.rs b/crates/typst-library/src/layout/grid/resolve.rs index adaff1c18..504159e83 100644 --- a/crates/typst-library/src/layout/grid/resolve.rs +++ b/crates/typst-library/src/layout/grid/resolve.rs @@ -42,16 +42,16 @@ pub fn grid_to_cellgrid<'a>( // Use trace to link back to the grid when a specific cell errors let tracepoint = || Tracepoint::Call(Some(eco_format!("grid"))); let resolve_item = |item: &GridItem| grid_item_to_resolvable(item, styles); - let children = elem.children().iter().map(|child| match child { + let children = elem.children.iter().map(|child| match child { GridChild::Header(header) => ResolvableGridChild::Header { repeat: header.repeat(styles), span: header.span(), - items: header.children().iter().map(resolve_item), + items: header.children.iter().map(resolve_item), }, GridChild::Footer(footer) => ResolvableGridChild::Footer { repeat: footer.repeat(styles), span: footer.span(), - items: footer.children().iter().map(resolve_item), + items: footer.children.iter().map(resolve_item), }, GridChild::Item(item) => { ResolvableGridChild::Item(grid_item_to_resolvable(item, styles)) @@ -95,16 +95,16 @@ pub fn table_to_cellgrid<'a>( // Use trace to link back to the table when a specific cell errors let tracepoint = || Tracepoint::Call(Some(eco_format!("table"))); let resolve_item = |item: &TableItem| table_item_to_resolvable(item, styles); - let children = elem.children().iter().map(|child| match child { + let children = elem.children.iter().map(|child| match child { TableChild::Header(header) => ResolvableGridChild::Header { repeat: header.repeat(styles), span: header.span(), - items: header.children().iter().map(resolve_item), + items: header.children.iter().map(resolve_item), }, TableChild::Footer(footer) => ResolvableGridChild::Footer { repeat: footer.repeat(styles), span: footer.span(), - items: footer.children().iter().map(resolve_item), + items: footer.children.iter().map(resolve_item), }, TableChild::Item(item) => { ResolvableGridChild::Item(table_item_to_resolvable(item, styles)) diff --git a/crates/typst-library/src/layout/hide.rs b/crates/typst-library/src/layout/hide.rs index 1b8b9bd57..eca33471a 100644 --- a/crates/typst-library/src/layout/hide.rs +++ b/crates/typst-library/src/layout/hide.rs @@ -29,6 +29,6 @@ pub struct HideElem { impl Show for Packed { #[typst_macros::time(name = "hide", span = self.span())] fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(HideElem::set_hidden(true))) + Ok(self.body.clone().styled(HideElem::set_hidden(true))) } } diff --git a/crates/typst-library/src/layout/layout.rs b/crates/typst-library/src/layout/layout.rs index c3d112e16..05e4f6d9b 100644 --- a/crates/typst-library/src/layout/layout.rs +++ b/crates/typst-library/src/layout/layout.rs @@ -89,7 +89,7 @@ impl Show for Packed { let loc = elem.location().unwrap(); let context = Context::new(Some(loc), Some(styles)); let result = elem - .func() + .func .call( engine, context.track(), diff --git a/crates/typst-library/src/math/accent.rs b/crates/typst-library/src/math/accent.rs index fee705ee4..b87e527f2 100644 --- a/crates/typst-library/src/math/accent.rs +++ b/crates/typst-library/src/math/accent.rs @@ -143,7 +143,7 @@ cast! { self => self.0.into_value(), v: char => Self::new(v), v: Content => match v.to_packed::() { - Some(elem) => Value::Str(elem.text().clone().into()).cast()?, + Some(elem) => Value::Str(elem.text.clone().into()).cast()?, None => bail!("expected text"), }, } diff --git a/crates/typst-library/src/math/attach.rs b/crates/typst-library/src/math/attach.rs index e1f577272..d526aba57 100644 --- a/crates/typst-library/src/math/attach.rs +++ b/crates/typst-library/src/math/attach.rs @@ -47,9 +47,9 @@ impl Packed { /// base AttachElem where possible. pub fn merge_base(&self) -> Option { // Extract from an EquationElem. - let mut base = self.base(); + let mut base = &self.base; while let Some(equation) = base.to_packed::() { - base = equation.body(); + base = &equation.body; } // Move attachments from elem into base where possible. diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index 4ab4ff22c..95db8a222 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -638,7 +638,7 @@ impl<'a> Generator<'a> { for elem in &self.groups { let group = elem.to_packed::().unwrap(); let location = elem.location().unwrap(); - let children = group.children(); + let children = &group.children; // Groups should never be empty. let Some(first) = children.first() else { continue }; @@ -650,12 +650,11 @@ impl<'a> Generator<'a> { // Create infos and items for each child in the group. for child in children { - let key = *child.key(); - let Some(entry) = database.get(key) else { + let Some(entry) = database.get(child.key) else { errors.push(error!( child.span(), "key `{}` does not exist in the bibliography", - key.resolve() + child.key.resolve() )); continue; }; @@ -682,7 +681,7 @@ impl<'a> Generator<'a> { }; normal &= special_form.is_none(); - subinfos.push(CiteInfo { key, supplement, hidden }); + subinfos.push(CiteInfo { key: child.key, supplement, hidden }); items.push(CitationItem::new(entry, locator, None, hidden, special_form)); } diff --git a/crates/typst-library/src/model/figure.rs b/crates/typst-library/src/model/figure.rs index fd843ee53..52dca966d 100644 --- a/crates/typst-library/src/model/figure.rs +++ b/crates/typst-library/src/model/figure.rs @@ -257,7 +257,7 @@ impl Synthesize for Packed { // Determine the figure's kind. let kind = elem.kind(styles).unwrap_or_else(|| { - elem.body() + elem.body .query_first(&Selector::can::()) .map(|elem| FigureKind::Elem(elem.func())) .unwrap_or_else(|| FigureKind::Elem(ImageElem::elem())) @@ -288,14 +288,13 @@ impl Synthesize for Packed { // Resolve the supplement with the first descendant of the kind or // just the body, if none was found. let descendant = match kind { - FigureKind::Elem(func) => elem - .body() - .query_first(&Selector::Elem(func, None)) - .map(Cow::Owned), + FigureKind::Elem(func) => { + elem.body.query_first(&Selector::Elem(func, None)).map(Cow::Owned) + } FigureKind::Name(_) => None, }; - let target = descendant.unwrap_or_else(|| Cow::Borrowed(elem.body())); + let target = descendant.unwrap_or_else(|| Cow::Borrowed(&elem.body)); Some(supplement.resolve(engine, styles, [target])?) } }; @@ -437,7 +436,7 @@ impl Outlinable for Packed { return Ok(None); }; - let mut realized = caption.body().clone(); + let mut realized = caption.body.clone(); if let ( Smart::Custom(Some(Supplement::Content(mut supplement))), Some(Some(counter)), @@ -460,7 +459,7 @@ impl Outlinable for Packed { let separator = caption.get_separator(StyleChain::default()); - realized = supplement + numbers + separator + caption.body(); + realized = supplement + numbers + separator + caption.body.clone(); } Ok(Some(realized)) @@ -604,7 +603,7 @@ impl Synthesize for Packed { impl Show for Packed { #[typst_macros::time(name = "figure.caption", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let mut realized = self.body().clone(); + let mut realized = self.body.clone(); if let ( Some(Some(mut supplement)), diff --git a/crates/typst-library/src/model/footnote.rs b/crates/typst-library/src/model/footnote.rs index ffc78ea05..f3b2a19eb 100644 --- a/crates/typst-library/src/model/footnote.rs +++ b/crates/typst-library/src/model/footnote.rs @@ -105,12 +105,12 @@ impl FootnoteElem { /// Tests if this footnote is a reference to another footnote. pub fn is_ref(&self) -> bool { - matches!(self.body(), FootnoteBody::Reference(_)) + matches!(self.body, FootnoteBody::Reference(_)) } /// Returns the content of the body of this footnote if it is not a ref. pub fn body_content(&self) -> Option<&Content> { - match self.body() { + match &self.body { FootnoteBody::Content(content) => Some(content), _ => None, } @@ -120,9 +120,9 @@ impl FootnoteElem { impl Packed { /// Returns the location of the definition of this footnote. pub fn declaration_location(&self, engine: &Engine) -> StrResult { - match self.body() { + match self.body { FootnoteBody::Reference(label) => { - let element = engine.introspector.query_label(*label)?; + let element = engine.introspector.query_label(label)?; let footnote = element .to_packed::() .ok_or("referenced element should be a footnote")?; @@ -281,12 +281,11 @@ impl Show for Packed { #[typst_macros::time(name = "footnote.entry", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { let span = self.span(); - let note = self.note(); let number_gap = Em::new(0.05); let default = StyleChain::default(); - let numbering = note.numbering(default); + let numbering = self.note.numbering(default); let counter = Counter::of(FootnoteElem::elem()); - let Some(loc) = note.location() else { + let Some(loc) = self.note.location() else { bail!( span, "footnote entry must have a location"; hint: "try using a query or a show rule to customize the footnote instead" @@ -304,7 +303,7 @@ impl Show for Packed { HElem::new(self.indent(styles).into()).pack(), sup, HElem::new(number_gap.into()).with_weak(true).pack(), - note.body_content().unwrap().clone(), + self.note.body_content().unwrap().clone(), ])) } } diff --git a/crates/typst-library/src/model/heading.rs b/crates/typst-library/src/model/heading.rs index ec9cf4e99..db131afec 100644 --- a/crates/typst-library/src/model/heading.rs +++ b/crates/typst-library/src/model/heading.rs @@ -223,7 +223,7 @@ impl Show for Packed { const SPACING_TO_NUMBERING: Em = Em::new(0.3); let span = self.span(); - let mut realized = self.body().clone(); + let mut realized = self.body.clone(); let hanging_indent = self.hanging_indent(styles); let mut indent = match hanging_indent { @@ -360,7 +360,7 @@ impl Outlinable for Packed { return Ok(None); } - let mut content = self.body().clone(); + let mut content = self.body.clone(); if let Some(numbering) = (**self).numbering(StyleChain::default()).as_ref() { let numbers = Counter::of(HeadingElem::elem()).display_at_loc( engine, diff --git a/crates/typst-library/src/model/link.rs b/crates/typst-library/src/model/link.rs index bbc47da05..4558cb394 100644 --- a/crates/typst-library/src/model/link.rs +++ b/crates/typst-library/src/model/link.rs @@ -102,11 +102,10 @@ impl LinkElem { impl Show for Packed { #[typst_macros::time(name = "link", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let body = self.body().clone(); - let dest = self.dest(); + let body = self.body.clone(); Ok(if TargetElem::target_in(styles).is_html() { - if let LinkTarget::Dest(Destination::Url(url)) = dest { + if let LinkTarget::Dest(Destination::Url(url)) = &self.dest { HtmlElem::new(tag::a) .with_attr(attr::href, url.clone().into_inner()) .with_body(Some(body)) @@ -120,7 +119,7 @@ impl Show for Packed { body } } else { - let linked = match self.dest() { + let linked = match &self.dest { LinkTarget::Dest(dest) => body.linked(dest.clone()), LinkTarget::Label(label) => { let elem = engine.introspector.query_label(*label).at(self.span())?; diff --git a/crates/typst-library/src/model/outline.rs b/crates/typst-library/src/model/outline.rs index e8d32a540..84661c1c2 100644 --- a/crates/typst-library/src/model/outline.rs +++ b/crates/typst-library/src/model/outline.rs @@ -219,8 +219,7 @@ impl Show for Packed { continue; }; - let level = entry.level(); - if depth < *level { + if depth < entry.level { continue; } @@ -229,7 +228,7 @@ impl Show for Packed { while ancestors .last() .and_then(|ancestor| ancestor.with::()) - .is_some_and(|last| last.level() >= *level) + .is_some_and(|last| last.level() >= entry.level) { ancestors.pop(); } @@ -483,7 +482,7 @@ impl Show for Packed { #[typst_macros::time(name = "outline.entry", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { let mut seq = vec![]; - let elem = self.element(); + let elem = &self.element; // In case a user constructs an outline entry with an arbitrary element. let Some(location) = elem.location() else { @@ -512,7 +511,7 @@ impl Show for Packed { seq.push(TextElem::packed("\u{202B}")); } - seq.push(self.body().clone().linked(Destination::Location(location))); + seq.push(self.body.clone().linked(Destination::Location(location))); if rtl { // "Pop Directional Formatting" @@ -520,7 +519,7 @@ impl Show for Packed { } // Add filler symbols between the section name and page number. - if let Some(filler) = self.fill() { + if let Some(filler) = &self.fill { seq.push(SpaceElem::shared().clone()); seq.push( BoxElem::new() @@ -535,7 +534,7 @@ impl Show for Packed { } // Add the page number. - let page = self.page().clone().linked(Destination::Location(location)); + let page = self.page.clone().linked(Destination::Location(location)); seq.push(page); Ok(Content::sequence(seq)) diff --git a/crates/typst-library/src/model/quote.rs b/crates/typst-library/src/model/quote.rs index 110825f13..2eaa32d4c 100644 --- a/crates/typst-library/src/model/quote.rs +++ b/crates/typst-library/src/model/quote.rs @@ -156,7 +156,7 @@ cast! { impl Show for Packed { #[typst_macros::time(name = "quote", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - let mut realized = self.body().clone(); + let mut realized = self.body.clone(); let block = self.block(styles); if self.quotes(styles) == Smart::Custom(true) || !block { diff --git a/crates/typst-library/src/model/reference.rs b/crates/typst-library/src/model/reference.rs index 96aa2117d..316617688 100644 --- a/crates/typst-library/src/model/reference.rs +++ b/crates/typst-library/src/model/reference.rs @@ -182,9 +182,8 @@ impl Synthesize for Packed { elem.push_citation(Some(citation)); elem.push_element(None); - let target = *elem.target(); - if !BibliographyElem::has(engine, target) { - if let Ok(found) = engine.introspector.query_label(target).cloned() { + if !BibliographyElem::has(engine, elem.target) { + if let Ok(found) = engine.introspector.query_label(elem.target).cloned() { elem.push_element(Some(found)); return Ok(()); } @@ -197,8 +196,7 @@ impl Synthesize for Packed { impl Show for Packed { #[typst_macros::time(name = "ref", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let target = *self.target(); - let elem = engine.introspector.query_label(target); + let elem = engine.introspector.query_label(self.target); let span = self.span(); let form = self.form(styles); @@ -229,7 +227,7 @@ impl Show for Packed { } // RefForm::Normal - if BibliographyElem::has(engine, target) { + if BibliographyElem::has(engine, self.target) { if elem.is_ok() { bail!(span, "label occurs in the document and its bibliography"); } @@ -240,7 +238,7 @@ impl Show for Packed { let elem = elem.at(span)?; if let Some(footnote) = elem.to_packed::() { - return Ok(footnote.into_ref(target).pack().spanned(span)); + return Ok(footnote.into_ref(self.target).pack().spanned(span)); } let elem = elem.clone(); @@ -319,7 +317,7 @@ fn to_citation( engine: &mut Engine, styles: StyleChain, ) -> SourceResult> { - let mut elem = Packed::new(CiteElem::new(*reference.target()).with_supplement( + let mut elem = Packed::new(CiteElem::new(reference.target).with_supplement( match reference.supplement(styles).clone() { Smart::Custom(Some(Supplement::Content(content))) => Some(content), _ => None, diff --git a/crates/typst-library/src/model/table.rs b/crates/typst-library/src/model/table.rs index 7dfaf45d7..fa44cb58a 100644 --- a/crates/typst-library/src/model/table.rs +++ b/crates/typst-library/src/model/table.rs @@ -706,7 +706,7 @@ cast! { impl Show for Packed { fn show(&self, _engine: &mut Engine, styles: StyleChain) -> SourceResult { - show_grid_cell(self.body().clone(), self.inset(styles), self.align(styles)) + show_grid_cell(self.body.clone(), self.inset(styles), self.align(styles)) } } diff --git a/crates/typst-library/src/model/terms.rs b/crates/typst-library/src/model/terms.rs index 13aa8c6d5..1261ea4f4 100644 --- a/crates/typst-library/src/model/terms.rs +++ b/crates/typst-library/src/model/terms.rs @@ -151,12 +151,12 @@ impl Show for Packed { .then(|| HElem::new((-hanging_indent).into()).pack().spanned(span)); let mut children = vec![]; - for child in self.children().iter() { + for child in self.children.iter() { let mut seq = vec![]; seq.extend(unpad.clone()); - seq.push(child.term().clone().strong()); + seq.push(child.term.clone().strong()); seq.push((*separator).clone()); - seq.push(child.description().clone()); + seq.push(child.description.clone()); children.push(StackChild::Block(Content::sequence(seq))); } diff --git a/crates/typst-library/src/text/deco.rs b/crates/typst-library/src/text/deco.rs index 5da7ecec4..485d0edcf 100644 --- a/crates/typst-library/src/text/deco.rs +++ b/crates/typst-library/src/text/deco.rs @@ -81,7 +81,7 @@ pub struct UnderlineElem { impl Show for Packed { #[typst_macros::time(name = "underline", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(TextElem::set_deco(smallvec![Decoration { + Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { line: DecoLine::Underline { stroke: self.stroke(styles).unwrap_or_default(), offset: self.offset(styles), @@ -173,7 +173,7 @@ pub struct OverlineElem { impl Show for Packed { #[typst_macros::time(name = "overline", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(TextElem::set_deco(smallvec![Decoration { + Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { line: DecoLine::Overline { stroke: self.stroke(styles).unwrap_or_default(), offset: self.offset(styles), @@ -250,7 +250,7 @@ pub struct StrikeElem { impl Show for Packed { #[typst_macros::time(name = "strike", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(TextElem::set_deco(smallvec![Decoration { + Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { // Note that we do not support evade option for strikethrough. line: DecoLine::Strikethrough { stroke: self.stroke(styles).unwrap_or_default(), @@ -345,7 +345,7 @@ pub struct HighlightElem { impl Show for Packed { #[typst_macros::time(name = "highlight", span = self.span())] fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(TextElem::set_deco(smallvec![Decoration { + Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { line: DecoLine::Highlight { fill: self.fill(styles), stroke: self diff --git a/crates/typst-library/src/text/mod.rs b/crates/typst-library/src/text/mod.rs index d372c399f..6cca24587 100644 --- a/crates/typst-library/src/text/mod.rs +++ b/crates/typst-library/src/text/mod.rs @@ -794,7 +794,7 @@ impl Construct for TextElem { impl PlainText for Packed { fn plain_text(&self, text: &mut EcoString) { - text.push_str(self.text()); + text.push_str(&self.text); } } diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs index cd718d2a1..01d6d8f01 100644 --- a/crates/typst-library/src/text/raw.rs +++ b/crates/typst-library/src/text/raw.rs @@ -315,7 +315,7 @@ impl Packed { #[comemo::memoize] fn highlight(&self, styles: StyleChain) -> Vec> { let elem = self.as_ref(); - let lines = preprocess(elem.text(), styles, self.span()); + let lines = preprocess(&elem.text, styles, self.span()); let count = lines.len() as i64; let lang = elem @@ -490,7 +490,7 @@ impl Figurable for Packed {} impl PlainText for Packed { fn plain_text(&self, text: &mut EcoString) { - text.push_str(&self.text().get()); + text.push_str(&self.text.get()); } } @@ -638,13 +638,13 @@ pub struct RawLine { impl Show for Packed { #[typst_macros::time(name = "raw.line", span = self.span())] fn show(&self, _: &mut Engine, _styles: StyleChain) -> SourceResult { - Ok(self.body().clone()) + Ok(self.body.clone()) } } impl PlainText for Packed { fn plain_text(&self, text: &mut EcoString) { - text.push_str(self.text()); + text.push_str(&self.text); } } diff --git a/crates/typst-library/src/text/shift.rs b/crates/typst-library/src/text/shift.rs index 9723bbf0c..3eec0758b 100644 --- a/crates/typst-library/src/text/shift.rs +++ b/crates/typst-library/src/text/shift.rs @@ -50,7 +50,7 @@ pub struct SubElem { impl Show for Packed { #[typst_macros::time(name = "sub", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let body = self.body().clone(); + let body = self.body.clone(); if self.typographic(styles) { if let Some(text) = convert_script(&body, true) { @@ -109,7 +109,7 @@ pub struct SuperElem { impl Show for Packed { #[typst_macros::time(name = "super", span = self.span())] fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult { - let body = self.body().clone(); + let body = self.body.clone(); if self.typographic(styles) { if let Some(text) = convert_script(&body, false) { @@ -132,9 +132,9 @@ fn convert_script(content: &Content, sub: bool) -> Option { Some(' '.into()) } else if let Some(elem) = content.to_packed::() { if sub { - elem.text().chars().map(to_subscript_codepoint).collect() + elem.text.chars().map(to_subscript_codepoint).collect() } else { - elem.text().chars().map(to_superscript_codepoint).collect() + elem.text.chars().map(to_superscript_codepoint).collect() } } else if let Some(sequence) = content.to_packed::() { sequence diff --git a/crates/typst-library/src/text/smallcaps.rs b/crates/typst-library/src/text/smallcaps.rs index bf003bd1c..1e88974f5 100644 --- a/crates/typst-library/src/text/smallcaps.rs +++ b/crates/typst-library/src/text/smallcaps.rs @@ -53,6 +53,6 @@ pub struct SmallcapsElem { impl Show for Packed { #[typst_macros::time(name = "smallcaps", span = self.span())] fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult { - Ok(self.body().clone().styled(TextElem::set_smallcaps(true))) + Ok(self.body.clone().styled(TextElem::set_smallcaps(true))) } } diff --git a/crates/typst-macros/src/elem.rs b/crates/typst-macros/src/elem.rs index 78a3c1800..67fe7ed6a 100644 --- a/crates/typst-macros/src/elem.rs +++ b/crates/typst-macros/src/elem.rs @@ -63,6 +63,11 @@ impl Elem { self.real_fields().filter(|field| !field.ghost) } + /// Fields that get accessor, with, and push methods. + fn accessor_fields(&self) -> impl Iterator + Clone { + self.struct_fields().filter(|field| !field.required) + } + /// Fields that are relevant for equality. /// /// Synthesized fields are excluded to ensure equality before and after @@ -442,9 +447,9 @@ fn create_inherent_impl(element: &Elem) -> TokenStream { let Elem { ident, .. } = element; let new_func = create_new_func(element); - let with_field_methods = element.struct_fields().map(create_with_field_method); - let push_field_methods = element.struct_fields().map(create_push_field_method); - let field_methods = element.struct_fields().map(create_field_method); + let with_field_methods = element.accessor_fields().map(create_with_field_method); + let push_field_methods = element.accessor_fields().map(create_push_field_method); + let field_methods = element.accessor_fields().map(create_field_method); let field_in_methods = element.style_fields().map(create_field_in_method); let set_field_methods = element.style_fields().map(create_set_field_method); diff --git a/crates/typst-pdf/src/embed.rs b/crates/typst-pdf/src/embed.rs index 3ba2ac076..597638f4b 100644 --- a/crates/typst-pdf/src/embed.rs +++ b/crates/typst-pdf/src/embed.rs @@ -59,7 +59,7 @@ fn embed_file( let embedded_file_stream_ref = chunk.alloc.bump(); let file_spec_dict_ref = chunk.alloc.bump(); - let data = embed.data().as_slice(); + let data = embed.data.as_slice(); let compressed = deflate(data); let mut embedded_file = chunk.embedded_file(embedded_file_stream_ref, &compressed); diff --git a/crates/typst-pdf/src/outline.rs b/crates/typst-pdf/src/outline.rs index b9e71319f..ff72eb86a 100644 --- a/crates/typst-pdf/src/outline.rs +++ b/crates/typst-pdf/src/outline.rs @@ -184,8 +184,7 @@ fn write_outline_item( outline.count(-(node.children.len() as i32)); } - let body = node.element.body(); - outline.title(TextStr::trimmed(body.plain_text().trim())); + outline.title(TextStr::trimmed(node.element.body.plain_text().trim())); let loc = node.element.location().unwrap(); let pos = ctx.document.introspector.position(loc); From a4ac4e656267e718a5cf60d1e959f74b2b7346f3 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 13 Jan 2025 20:19:37 +0100 Subject: [PATCH 07/10] Make `typst-timing` WASM-compatible (#5689) --- Cargo.lock | 11 + Cargo.toml | 1 + crates/typst-timing/Cargo.toml | 6 + crates/typst-timing/src/lib.rs | 372 +++++++++++++++++++-------------- 4 files changed, 235 insertions(+), 155 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c0bfe138..8aa7c0ec1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3093,6 +3093,7 @@ dependencies = [ "parking_lot", "serde", "serde_json", + "web-sys", ] [[package]] @@ -3418,6 +3419,16 @@ dependencies = [ "indexmap-nostd", ] +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "weezl" version = "0.1.8" diff --git a/Cargo.toml b/Cargo.toml index b4f704f80..1be7816a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -134,6 +134,7 @@ ureq = { version = "2", default-features = false, features = ["native-tls", "gzi usvg = { version = "0.43", default-features = false, features = ["text"] } walkdir = "2" wasmi = "0.39.0" +web-sys = "0.3" xmlparser = "0.13.5" xmlwriter = "0.1.0" xmp-writer = "0.3" diff --git a/crates/typst-timing/Cargo.toml b/crates/typst-timing/Cargo.toml index 2d42269fc..dbc2813c7 100644 --- a/crates/typst-timing/Cargo.toml +++ b/crates/typst-timing/Cargo.toml @@ -17,5 +17,11 @@ parking_lot = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +[target.'cfg(target_arch = "wasm32")'.dependencies] +web-sys = { workspace = true, features = ["Window", "WorkerGlobalScope", "Performance"], optional = true } + +[features] +wasm = ["dep:web-sys"] + [lints] workspace = true diff --git a/crates/typst-timing/src/lib.rs b/crates/typst-timing/src/lib.rs index b4653170b..6da2cdf02 100644 --- a/crates/typst-timing/src/lib.rs +++ b/crates/typst-timing/src/lib.rs @@ -1,149 +1,13 @@ //! Performance timing for Typst. -#![cfg_attr(target_arch = "wasm32", allow(dead_code, unused_variables))] - -use std::hash::Hash; use std::io::Write; use std::num::NonZeroU64; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::Ordering::Relaxed; -use std::thread::ThreadId; -use std::time::{Duration, SystemTime}; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use parking_lot::Mutex; use serde::ser::SerializeSeq; use serde::{Serialize, Serializer}; -/// Whether the timer is enabled. Defaults to `false`. -static ENABLED: AtomicBool = AtomicBool::new(false); - -/// The global event recorder. -static RECORDER: Mutex = Mutex::new(Recorder::new()); - -/// The recorder of events. -struct Recorder { - /// The events that have been recorded. - events: Vec, - /// The discriminator of the next event. - discriminator: u64, -} - -impl Recorder { - /// Create a new recorder. - const fn new() -> Self { - Self { events: Vec::new(), discriminator: 0 } - } -} - -/// An event that has been recorded. -#[derive(Clone, Copy, Eq, PartialEq, Hash)] -struct Event { - /// Whether this is a start or end event. - kind: EventKind, - /// The start time of this event. - timestamp: SystemTime, - /// The discriminator of this event. - id: u64, - /// The name of this event. - name: &'static str, - /// The raw value of the span of code that this event was recorded in. - span: Option, - /// The thread ID of this event. - thread_id: ThreadId, -} - -/// Whether an event marks the start or end of a scope. -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] -enum EventKind { - Start, - End, -} - -/// Enable the timer. -#[inline] -pub fn enable() { - // We only need atomicity and no synchronization of other - // operations, so `Relaxed` is fine. - ENABLED.store(true, Relaxed); -} - -/// Whether the timer is enabled. -#[inline] -pub fn is_enabled() -> bool { - ENABLED.load(Relaxed) -} - -/// Clears the recorded events. -#[inline] -pub fn clear() { - RECORDER.lock().events.clear(); -} - -/// A scope that records an event when it is dropped. -pub struct TimingScope { - name: &'static str, - span: Option, - id: u64, - thread_id: ThreadId, -} - -impl TimingScope { - /// Create a new scope if timing is enabled. - #[inline] - pub fn new(name: &'static str) -> Option { - Self::with_span(name, None) - } - - /// Create a new scope with a span if timing is enabled. - /// - /// The span is a raw number because `typst-timing` can't depend on - /// `typst-syntax` (or else `typst-syntax` couldn't depend on - /// `typst-timing`). - #[inline] - pub fn with_span(name: &'static str, span: Option) -> Option { - #[cfg(not(target_arch = "wasm32"))] - if is_enabled() { - return Some(Self::new_impl(name, span)); - } - None - } - - /// Create a new scope without checking if timing is enabled. - fn new_impl(name: &'static str, span: Option) -> Self { - let timestamp = SystemTime::now(); - let thread_id = std::thread::current().id(); - - let mut recorder = RECORDER.lock(); - let id = recorder.discriminator; - recorder.discriminator += 1; - recorder.events.push(Event { - kind: EventKind::Start, - timestamp, - id, - name, - span, - thread_id, - }); - - Self { name, span, id, thread_id } - } -} - -impl Drop for TimingScope { - fn drop(&mut self) { - let event = Event { - kind: EventKind::End, - timestamp: SystemTime::now(), - id: self.id, - name: self.name, - span: self.span, - thread_id: self.thread_id, - }; - - RECORDER.lock().events.push(event); - } -} - /// Creates a timing scope around an expression. /// /// The output of the expression is returned. @@ -179,6 +43,46 @@ macro_rules! timed { }}; } +thread_local! { + /// Data that is initialized once per thread. + static THREAD_DATA: ThreadData = ThreadData { + id: { + // We only need atomicity and no synchronization of other + // operations, so `Relaxed` is fine. + static COUNTER: AtomicU64 = AtomicU64::new(1); + COUNTER.fetch_add(1, Ordering::Relaxed) + }, + #[cfg(all(target_arch = "wasm32", feature = "wasm"))] + timer: WasmTimer::new(), + }; +} + +/// Whether the timer is enabled. Defaults to `false`. +static ENABLED: AtomicBool = AtomicBool::new(false); + +/// The list of collected events. +static EVENTS: Mutex> = Mutex::new(Vec::new()); + +/// Enable the timer. +#[inline] +pub fn enable() { + // We only need atomicity and no synchronization of other + // operations, so `Relaxed` is fine. + ENABLED.store(true, Ordering::Relaxed); +} + +/// Whether the timer is enabled. +#[inline] +pub fn is_enabled() -> bool { + ENABLED.load(Ordering::Relaxed) +} + +/// Clears the recorded events. +#[inline] +pub fn clear() { + EVENTS.lock().clear(); +} + /// Export data as JSON for Chrome's tracing tool. /// /// The `source` function is called for each span to get the source code @@ -205,19 +109,15 @@ pub fn export_json( line: u32, } - let recorder = RECORDER.lock(); - let run_start = recorder - .events - .first() - .map(|event| event.timestamp) - .unwrap_or_else(SystemTime::now); + let lock = EVENTS.lock(); + let events = lock.as_slice(); let mut serializer = serde_json::Serializer::new(writer); let mut seq = serializer - .serialize_seq(Some(recorder.events.len())) + .serialize_seq(Some(events.len())) .map_err(|e| format!("failed to serialize events: {e}"))?; - for event in recorder.events.iter() { + for event in events.iter() { seq.serialize_element(&Entry { name: event.name, cat: "typst", @@ -225,17 +125,9 @@ pub fn export_json( EventKind::Start => "B", EventKind::End => "E", }, - ts: event - .timestamp - .duration_since(run_start) - .unwrap_or(Duration::ZERO) - .as_nanos() as f64 - / 1_000.0, + ts: event.timestamp.micros_since(events[0].timestamp), pid: 1, - tid: unsafe { - // Safety: `thread_id` is a `ThreadId` which is a `u64`. - std::mem::transmute_copy(&event.thread_id) - }, + tid: event.thread_id, args: event.span.map(&mut source).map(|(file, line)| Args { file, line }), }) .map_err(|e| format!("failed to serialize event: {e}"))?; @@ -245,3 +137,173 @@ pub fn export_json( Ok(()) } + +/// A scope that records an event when it is dropped. +pub struct TimingScope { + name: &'static str, + span: Option, + thread_id: u64, +} + +impl TimingScope { + /// Create a new scope if timing is enabled. + #[inline] + pub fn new(name: &'static str) -> Option { + Self::with_span(name, None) + } + + /// Create a new scope with a span if timing is enabled. + /// + /// The span is a raw number because `typst-timing` can't depend on + /// `typst-syntax` (or else `typst-syntax` couldn't depend on + /// `typst-timing`). + #[inline] + pub fn with_span(name: &'static str, span: Option) -> Option { + if is_enabled() { + return Some(Self::new_impl(name, span)); + } + None + } + + /// Create a new scope without checking if timing is enabled. + fn new_impl(name: &'static str, span: Option) -> Self { + let (thread_id, timestamp) = + THREAD_DATA.with(|data| (data.id, Timestamp::now_with(data))); + EVENTS.lock().push(Event { + kind: EventKind::Start, + timestamp, + name, + span, + thread_id, + }); + Self { name, span, thread_id } + } +} + +impl Drop for TimingScope { + fn drop(&mut self) { + let timestamp = Timestamp::now(); + EVENTS.lock().push(Event { + kind: EventKind::End, + timestamp, + name: self.name, + span: self.span, + thread_id: self.thread_id, + }); + } +} + +/// An event that has been recorded. +struct Event { + /// Whether this is a start or end event. + kind: EventKind, + /// The time at which this event occurred. + timestamp: Timestamp, + /// The name of this event. + name: &'static str, + /// The raw value of the span of code that this event was recorded in. + span: Option, + /// The thread ID of this event. + thread_id: u64, +} + +/// Whether an event marks the start or end of a scope. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +enum EventKind { + Start, + End, +} + +/// A cross-platform way to get the current time. +#[derive(Copy, Clone)] +struct Timestamp { + #[cfg(not(target_arch = "wasm32"))] + inner: std::time::SystemTime, + #[cfg(target_arch = "wasm32")] + inner: f64, +} + +impl Timestamp { + fn now() -> Self { + #[cfg(target_arch = "wasm32")] + return THREAD_DATA.with(Self::now_with); + + #[cfg(not(target_arch = "wasm32"))] + Self { inner: std::time::SystemTime::now() } + } + + #[allow(unused_variables)] + fn now_with(data: &ThreadData) -> Self { + #[cfg(all(target_arch = "wasm32", feature = "wasm"))] + return Self { inner: data.timer.now() }; + + #[cfg(all(target_arch = "wasm32", not(feature = "wasm")))] + return Self { inner: 0.0 }; + + #[cfg(not(target_arch = "wasm32"))] + Self::now() + } + + fn micros_since(self, start: Self) -> f64 { + #[cfg(target_arch = "wasm32")] + return (self.inner - start.inner) * 1000.0; + + #[cfg(not(target_arch = "wasm32"))] + (self + .inner + .duration_since(start.inner) + .unwrap_or(std::time::Duration::ZERO) + .as_nanos() as f64 + / 1_000.0) + } +} + +/// Per-thread data. +struct ThreadData { + /// The thread's ID. + /// + /// In contrast to `std::thread::current().id()`, this is wasm-compatible + /// and also a bit cheaper to access because the std version does a bit more + /// stuff (including cloning an `Arc`). + id: u64, + /// A way to get the time from WebAssembly. + #[cfg(all(target_arch = "wasm32", feature = "wasm"))] + timer: WasmTimer, +} + +/// A way to get the time from WebAssembly. +#[cfg(all(target_arch = "wasm32", feature = "wasm"))] +struct WasmTimer { + /// The cached JS performance handle for the thread. + perf: web_sys::Performance, + /// The cached JS time origin. + time_origin: f64, +} + +#[cfg(all(target_arch = "wasm32", feature = "wasm"))] +impl WasmTimer { + fn new() -> Self { + // Retrieve `performance` from global object, either the window or + // globalThis. + let perf = web_sys::window() + .and_then(|window| window.performance()) + .or_else(|| { + use web_sys::wasm_bindgen::JsCast; + web_sys::js_sys::global() + .dyn_into::() + .ok() + .and_then(|scope| scope.performance()) + }) + .expect("failed to get JS performance handle"); + + // Every thread gets its own time origin. To make the results consistent + // across threads, we need to add this to each `now()` call. + let time_origin = perf.time_origin(); + + Self { perf, time_origin } + } + + fn now(&self) -> f64 { + self.time_origin + self.perf.now() + } +} From 63c4720ed2b9e034fda6810a9c0e521355a24c44 Mon Sep 17 00:00:00 2001 From: Ian Wrzesinski <133046678+wrzian@users.noreply.github.com> Date: Thu, 16 Jan 2025 08:40:29 -0500 Subject: [PATCH 08/10] Fix list indent when starting at an open bracket (#5677) --- crates/typst-syntax/src/parser.rs | 19 +++++++++----- tests/suite/model/list.typ | 43 +++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/crates/typst-syntax/src/parser.rs b/crates/typst-syntax/src/parser.rs index 335b8f1a2..a65e5ff6b 100644 --- a/crates/typst-syntax/src/parser.rs +++ b/crates/typst-syntax/src/parser.rs @@ -1605,10 +1605,12 @@ impl AtNewline { _ => true, }, AtNewline::StopParBreak => parbreak, - AtNewline::RequireColumn(min_col) => match column { - Some(column) => column <= min_col, - None => false, // Don't stop if we had no column. - }, + AtNewline::RequireColumn(min_col) => { + // Don't stop if this newline doesn't start a column (this may + // be checked on the boundary of lexer modes, since we only + // report a column in Markup). + column.is_some_and(|column| column <= min_col) + } } } } @@ -1703,10 +1705,13 @@ impl<'s> Parser<'s> { self.token.newline.is_some() } - /// The number of characters until the most recent newline from the current - /// token, or 0 if it did not follow a newline. + /// The number of characters until the most recent newline from the start of + /// the current token. Uses a cached value from the newline mode if present. fn current_column(&self) -> usize { - self.token.newline.and_then(|newline| newline.column).unwrap_or(0) + self.token + .newline + .and_then(|newline| newline.column) + .unwrap_or_else(|| self.lexer.column(self.token.start)) } /// The current token's text. diff --git a/tests/suite/model/list.typ b/tests/suite/model/list.typ index 138abf70e..b3d9a830b 100644 --- a/tests/suite/model/list.typ +++ b/tests/suite/model/list.typ @@ -77,6 +77,49 @@ _Shopping list_ #test(indented, manual) +--- list-indent-bracket-nesting --- +// Test list indent nesting behavior when directly at a starting bracket. + +#let indented = { + [- indented + - less + ] + [- indented + - same + - then less + - then same + ] + [- indented + - more + - then same + - then less + ] +} + +#let item = list.item +#let manual = { + { + item[indented]; [ ] + item[less]; [ ] + } + { + item[indented]; [ ] + item[same]; [ ] + item[then less #{ + item[then same] + }]; [ ] + } + { + item[indented #{ + item[more] + }]; [ ] + item[then same]; [ ] + item[then less]; [ ] + } +} + +#test(indented, manual) + --- list-tabs --- // This works because tabs are used consistently. - A with 1 tab From c22c47b9c97062309ed841679bb49fc15bb6a398 Mon Sep 17 00:00:00 2001 From: Eric Biedert Date: Thu, 16 Jan 2025 14:40:57 +0100 Subject: [PATCH 09/10] Add font exception for NewCM Sans Math (#5682) --- crates/typst-library/src/text/font/exceptions.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/typst-library/src/text/font/exceptions.rs b/crates/typst-library/src/text/font/exceptions.rs index 465ec510c..00038c50c 100644 --- a/crates/typst-library/src/text/font/exceptions.rs +++ b/crates/typst-library/src/text/font/exceptions.rs @@ -228,6 +228,8 @@ static EXCEPTION_MAP: phf::Map<&'static str, Exception> = phf::phf_map! { .style(FontStyle::Oblique), "NewCMSans10-Regular" => Exception::new() .family("New Computer Modern Sans"), + "NewCMSansMath-Regular" => Exception::new() + .family("New Computer Modern Sans Math"), "NewCMUncial08-Bold" => Exception::new() .family("New Computer Modern Uncial 08"), "NewCMUncial08-Book" => Exception::new() From b90ad470d60f4a90e3ba2e78aa4746fbe08783ab Mon Sep 17 00:00:00 2001 From: Laurenz Date: Tue, 21 Jan 2025 12:10:06 +0100 Subject: [PATCH 10/10] Remove redundant doc comments on standard arguments (#5725) --- crates/typst-library/src/foundations/args.rs | 2 -- crates/typst-library/src/foundations/array.rs | 25 ------------------- crates/typst-library/src/foundations/calc.rs | 11 -------- .../typst-library/src/foundations/datetime.rs | 1 - crates/typst-library/src/foundations/func.rs | 4 --- crates/typst-library/src/foundations/mod.rs | 1 - .../typst-library/src/foundations/plugin.rs | 1 - crates/typst-library/src/foundations/str.rs | 2 -- .../typst-library/src/foundations/symbol.rs | 1 - .../typst-library/src/foundations/target.rs | 5 +--- .../src/introspection/counter.rs | 14 ----------- .../typst-library/src/introspection/here.rs | 5 +--- .../typst-library/src/introspection/locate.rs | 2 -- .../typst-library/src/introspection/query.rs | 2 -- .../typst-library/src/introspection/state.rs | 10 -------- crates/typst-library/src/layout/layout.rs | 1 - crates/typst-library/src/layout/measure.rs | 3 --- crates/typst-library/src/loading/cbor.rs | 2 -- crates/typst-library/src/loading/csv.rs | 2 -- crates/typst-library/src/loading/json.rs | 2 -- crates/typst-library/src/loading/read.rs | 1 - crates/typst-library/src/loading/toml.rs | 2 -- crates/typst-library/src/loading/xml.rs | 2 -- crates/typst-library/src/loading/yaml.rs | 2 -- crates/typst-library/src/math/root.rs | 1 - crates/typst-library/src/model/numbering.rs | 2 -- crates/typst-library/src/visualize/color.rs | 19 -------------- .../typst-library/src/visualize/gradient.rs | 4 --- .../typst-library/src/visualize/image/mod.rs | 1 - crates/typst-library/src/visualize/polygon.rs | 2 +- crates/typst-library/src/visualize/stroke.rs | 2 -- crates/typst-library/src/visualize/tiling.rs | 1 - 32 files changed, 3 insertions(+), 132 deletions(-) diff --git a/crates/typst-library/src/foundations/args.rs b/crates/typst-library/src/foundations/args.rs index 44aa9dd6d..430c4e9ad 100644 --- a/crates/typst-library/src/foundations/args.rs +++ b/crates/typst-library/src/foundations/args.rs @@ -305,8 +305,6 @@ impl Args { /// ``` #[func(constructor)] pub fn construct( - /// The real arguments (the other argument is just for the docs). - /// The docs argument cannot be called `args`. args: &mut Args, /// The arguments to construct. #[external] diff --git a/crates/typst-library/src/foundations/array.rs b/crates/typst-library/src/foundations/array.rs index e79a4e930..aad7266bc 100644 --- a/crates/typst-library/src/foundations/array.rs +++ b/crates/typst-library/src/foundations/array.rs @@ -301,9 +301,7 @@ impl Array { #[func] pub fn find( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The function to apply to each item. Must return a boolean. searcher: Func, @@ -325,9 +323,7 @@ impl Array { #[func] pub fn position( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The function to apply to each item. Must return a boolean. searcher: Func, @@ -363,8 +359,6 @@ impl Array { /// ``` #[func] pub fn range( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The start of the range (inclusive). #[external] @@ -402,9 +396,7 @@ impl Array { #[func] pub fn filter( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The function to apply to each item. Must return a boolean. test: Func, @@ -427,9 +419,7 @@ impl Array { #[func] pub fn map( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The function to apply to each item. mapper: Func, @@ -481,8 +471,6 @@ impl Array { #[func] pub fn zip( self, - /// The real arguments (the `others` arguments are just for the docs, this - /// function is a bit involved, so we parse the positional arguments manually). args: &mut Args, /// Whether all arrays have to have the same length. /// For example, `{(1, 2).zip((1, 2, 3), exact: true)}` produces an @@ -569,9 +557,7 @@ impl Array { #[func] pub fn fold( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The initial value to start with. init: Value, @@ -631,9 +617,7 @@ impl Array { #[func] pub fn any( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The function to apply to each item. Must return a boolean. test: Func, @@ -651,9 +635,7 @@ impl Array { #[func] pub fn all( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The function to apply to each item. Must return a boolean. test: Func, @@ -831,11 +813,8 @@ impl Array { #[func] pub fn sorted( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, /// If given, applies this function to the elements in the array to /// determine the keys to sort by. @@ -881,9 +860,7 @@ impl Array { #[func(title = "Deduplicate")] pub fn dedup( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// If given, applies this function to the elements in the array to /// determine the keys to deduplicate by. @@ -967,9 +944,7 @@ impl Array { #[func] pub fn reduce( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The reducing function. Must have two parameters: One for the /// accumulated value and one for an item. diff --git a/crates/typst-library/src/foundations/calc.rs b/crates/typst-library/src/foundations/calc.rs index fd4498e07..a8e0eaeb3 100644 --- a/crates/typst-library/src/foundations/calc.rs +++ b/crates/typst-library/src/foundations/calc.rs @@ -97,7 +97,6 @@ cast! { /// ``` #[func(title = "Power")] pub fn pow( - /// The callsite span. span: Span, /// The base of the power. /// @@ -159,7 +158,6 @@ pub fn pow( /// ``` #[func(title = "Exponential")] pub fn exp( - /// The callsite span. span: Span, /// The exponent of the power. exponent: Spanned, @@ -412,7 +410,6 @@ pub fn tanh( /// ``` #[func(title = "Logarithm")] pub fn log( - /// The callsite span. span: Span, /// The number whose logarithm to calculate. Must be strictly positive. value: Spanned, @@ -454,7 +451,6 @@ pub fn log( /// ``` #[func(title = "Natural Logarithm")] pub fn ln( - /// The callsite span. span: Span, /// The number whose logarithm to calculate. Must be strictly positive. value: Spanned, @@ -782,7 +778,6 @@ pub fn round( /// ``` #[func] pub fn clamp( - /// The callsite span. span: Span, /// The number to clamp. value: DecNum, @@ -815,7 +810,6 @@ pub fn clamp( /// ``` #[func(title = "Minimum")] pub fn min( - /// The callsite span. span: Span, /// The sequence of values from which to extract the minimum. /// Must not be empty. @@ -833,7 +827,6 @@ pub fn min( /// ``` #[func(title = "Maximum")] pub fn max( - /// The callsite span. span: Span, /// The sequence of values from which to extract the maximum. /// Must not be empty. @@ -911,7 +904,6 @@ pub fn odd( /// ``` #[func(title = "Remainder")] pub fn rem( - /// The span of the function call. span: Span, /// The dividend of the remainder. dividend: DecNum, @@ -950,7 +942,6 @@ pub fn rem( /// ``` #[func(title = "Euclidean Division")] pub fn div_euclid( - /// The callsite span. span: Span, /// The dividend of the division. dividend: DecNum, @@ -994,7 +985,6 @@ pub fn div_euclid( /// ``` #[func(title = "Euclidean Remainder", keywords = ["modulo", "modulus"])] pub fn rem_euclid( - /// The callsite span. span: Span, /// The dividend of the remainder. dividend: DecNum, @@ -1031,7 +1021,6 @@ pub fn rem_euclid( /// ``` #[func(title = "Quotient")] pub fn quo( - /// The span of the function call. span: Span, /// The dividend of the quotient. dividend: DecNum, diff --git a/crates/typst-library/src/foundations/datetime.rs b/crates/typst-library/src/foundations/datetime.rs index d15cd417a..2fc48a521 100644 --- a/crates/typst-library/src/foundations/datetime.rs +++ b/crates/typst-library/src/foundations/datetime.rs @@ -318,7 +318,6 @@ impl Datetime { /// ``` #[func] pub fn today( - /// The engine. engine: &mut Engine, /// An offset to apply to the current UTC date. If set to `{auto}`, the /// offset will be the local offset. diff --git a/crates/typst-library/src/foundations/func.rs b/crates/typst-library/src/foundations/func.rs index 40c826df9..cb3eba161 100644 --- a/crates/typst-library/src/foundations/func.rs +++ b/crates/typst-library/src/foundations/func.rs @@ -334,8 +334,6 @@ impl Func { #[func] pub fn with( self, - /// The real arguments (the other argument is just for the docs). - /// The docs argument cannot be called `args`. args: &mut Args, /// The arguments to apply to the function. #[external] @@ -361,8 +359,6 @@ impl Func { #[func] pub fn where_( self, - /// The real arguments (the other argument is just for the docs). - /// The docs argument cannot be called `args`. args: &mut Args, /// The fields to filter for. #[variadic] diff --git a/crates/typst-library/src/foundations/mod.rs b/crates/typst-library/src/foundations/mod.rs index d960a666c..2c3730d53 100644 --- a/crates/typst-library/src/foundations/mod.rs +++ b/crates/typst-library/src/foundations/mod.rs @@ -266,7 +266,6 @@ impl assert { /// ``` #[func(title = "Evaluate")] pub fn eval( - /// The engine. engine: &mut Engine, /// A string of Typst code to evaluate. source: Spanned, diff --git a/crates/typst-library/src/foundations/plugin.rs b/crates/typst-library/src/foundations/plugin.rs index adf23a47c..d41261edc 100644 --- a/crates/typst-library/src/foundations/plugin.rs +++ b/crates/typst-library/src/foundations/plugin.rs @@ -152,7 +152,6 @@ impl Plugin { /// Creates a new plugin from a WebAssembly file. #[func(constructor)] pub fn construct( - /// The engine. engine: &mut Engine, /// A path to a WebAssembly file or raw WebAssembly bytes. /// diff --git a/crates/typst-library/src/foundations/str.rs b/crates/typst-library/src/foundations/str.rs index 2e90b3071..551ac04f5 100644 --- a/crates/typst-library/src/foundations/str.rs +++ b/crates/typst-library/src/foundations/str.rs @@ -425,9 +425,7 @@ impl Str { #[func] pub fn replace( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// The pattern to search for. pattern: StrPattern, diff --git a/crates/typst-library/src/foundations/symbol.rs b/crates/typst-library/src/foundations/symbol.rs index 72800f311..3045970de 100644 --- a/crates/typst-library/src/foundations/symbol.rs +++ b/crates/typst-library/src/foundations/symbol.rs @@ -187,7 +187,6 @@ impl Symbol { /// ``` #[func(constructor)] pub fn construct( - /// The callsite span. span: Span, /// The variants of the symbol. /// diff --git a/crates/typst-library/src/foundations/target.rs b/crates/typst-library/src/foundations/target.rs index b743ea1ab..5841552e4 100644 --- a/crates/typst-library/src/foundations/target.rs +++ b/crates/typst-library/src/foundations/target.rs @@ -30,9 +30,6 @@ pub struct TargetElem { /// Returns the current compilation target. #[func(contextual)] -pub fn target( - /// The callsite context. - context: Tracked, -) -> HintedStrResult { +pub fn target(context: Tracked) -> HintedStrResult { Ok(TargetElem::target_in(context.styles()?)) } diff --git a/crates/typst-library/src/introspection/counter.rs b/crates/typst-library/src/introspection/counter.rs index d26a9f9f5..5432df238 100644 --- a/crates/typst-library/src/introspection/counter.rs +++ b/crates/typst-library/src/introspection/counter.rs @@ -428,11 +428,8 @@ impl Counter { #[func(contextual)] pub fn get( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, ) -> SourceResult { let loc = context.location().at(span)?; @@ -444,11 +441,8 @@ impl Counter { #[func(contextual)] pub fn display( self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The call span of the display. span: Span, /// A [numbering pattern or a function]($numbering), which specifies how /// to display the counter. If given a function, that function receives @@ -482,11 +476,8 @@ impl Counter { #[func(contextual)] pub fn at( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, /// The place at which the counter's value should be retrieved. selector: LocatableSelector, @@ -500,11 +491,8 @@ impl Counter { #[func(contextual)] pub fn final_( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, ) -> SourceResult { context.introspect().at(span)?; @@ -528,7 +516,6 @@ impl Counter { #[func] pub fn step( self, - /// The call span of the update. span: Span, /// The depth at which to step the counter. Defaults to `{1}`. #[named] @@ -545,7 +532,6 @@ impl Counter { #[func] pub fn update( self, - /// The call span of the update. span: Span, /// If given an integer or array of integers, sets the counter to that /// value. If given a function, that function receives the previous diff --git a/crates/typst-library/src/introspection/here.rs b/crates/typst-library/src/introspection/here.rs index 9d6133816..510093247 100644 --- a/crates/typst-library/src/introspection/here.rs +++ b/crates/typst-library/src/introspection/here.rs @@ -44,9 +44,6 @@ use crate::introspection::Location; /// ``` /// Refer to the [`selector`] type for more details on before/after selectors. #[func(contextual)] -pub fn here( - /// The callsite context. - context: Tracked, -) -> HintedStrResult { +pub fn here(context: Tracked) -> HintedStrResult { context.location() } diff --git a/crates/typst-library/src/introspection/locate.rs b/crates/typst-library/src/introspection/locate.rs index f6631b021..50f217851 100644 --- a/crates/typst-library/src/introspection/locate.rs +++ b/crates/typst-library/src/introspection/locate.rs @@ -24,9 +24,7 @@ use crate::introspection::Location; /// ``` #[func(contextual)] pub fn locate( - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// A selector that should match exactly one element. This element will be /// located. diff --git a/crates/typst-library/src/introspection/query.rs b/crates/typst-library/src/introspection/query.rs index f616208c5..b742ac010 100644 --- a/crates/typst-library/src/introspection/query.rs +++ b/crates/typst-library/src/introspection/query.rs @@ -136,9 +136,7 @@ use crate::foundations::{func, Array, Context, LocatableSelector, Value}; /// ``` #[func(contextual)] pub fn query( - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// Can be /// - an element function like a `heading` or `figure`, diff --git a/crates/typst-library/src/introspection/state.rs b/crates/typst-library/src/introspection/state.rs index e6ab926bf..cc3f566b5 100644 --- a/crates/typst-library/src/introspection/state.rs +++ b/crates/typst-library/src/introspection/state.rs @@ -289,11 +289,8 @@ impl State { #[func(contextual)] pub fn get( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, ) -> SourceResult { let loc = context.location().at(span)?; @@ -309,11 +306,8 @@ impl State { #[func(contextual)] pub fn at( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, /// The place at which the state's value should be retrieved. selector: LocatableSelector, @@ -326,11 +320,8 @@ impl State { #[func(contextual)] pub fn final_( &self, - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, ) -> SourceResult { context.introspect().at(span)?; @@ -349,7 +340,6 @@ impl State { #[func] pub fn update( self, - /// The span of the `update` call. span: Span, /// If given a non function-value, sets the state to that value. If /// given a function, that function receives the previous state and has diff --git a/crates/typst-library/src/layout/layout.rs b/crates/typst-library/src/layout/layout.rs index 05e4f6d9b..cde3187d3 100644 --- a/crates/typst-library/src/layout/layout.rs +++ b/crates/typst-library/src/layout/layout.rs @@ -54,7 +54,6 @@ use crate::layout::{BlockElem, Size}; /// corresponding page dimension is set to `{auto}`. #[func] pub fn layout( - /// The call span of this function. span: Span, /// A function to call with the outer container's size. Its return value is /// displayed in the document. diff --git a/crates/typst-library/src/layout/measure.rs b/crates/typst-library/src/layout/measure.rs index 0c6071eb0..93c48ad40 100644 --- a/crates/typst-library/src/layout/measure.rs +++ b/crates/typst-library/src/layout/measure.rs @@ -43,11 +43,8 @@ use crate::layout::{Abs, Axes, Length, Region, Size}; /// `height`, both of type [`length`]. #[func(contextual)] pub fn measure( - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, - /// The callsite span. span: Span, /// The width available to layout the content. /// diff --git a/crates/typst-library/src/loading/cbor.rs b/crates/typst-library/src/loading/cbor.rs index 13d551201..2bdeb80ef 100644 --- a/crates/typst-library/src/loading/cbor.rs +++ b/crates/typst-library/src/loading/cbor.rs @@ -19,7 +19,6 @@ use crate::loading::{DataSource, Load}; /// floating point numbers, which may result in an approximative value. #[func(scope, title = "CBOR")] pub fn cbor( - /// The engine. engine: &mut Engine, /// A path to a CBOR file or raw CBOR bytes. /// @@ -40,7 +39,6 @@ impl cbor { /// directly. #[func(title = "Decode CBOR")] pub fn decode( - /// The engine. engine: &mut Engine, /// CBOR data. data: Spanned, diff --git a/crates/typst-library/src/loading/csv.rs b/crates/typst-library/src/loading/csv.rs index 8171c4832..e5dabfaa6 100644 --- a/crates/typst-library/src/loading/csv.rs +++ b/crates/typst-library/src/loading/csv.rs @@ -25,7 +25,6 @@ use crate::loading::{DataSource, Load, Readable}; /// ``` #[func(scope, title = "CSV")] pub fn csv( - /// The engine. engine: &mut Engine, /// Path to a CSV file or raw CSV bytes. /// @@ -102,7 +101,6 @@ impl csv { /// directly. #[func(title = "Decode CSV")] pub fn decode( - /// The engine. engine: &mut Engine, /// CSV data. data: Spanned, diff --git a/crates/typst-library/src/loading/json.rs b/crates/typst-library/src/loading/json.rs index 3128d77da..035c5e4a7 100644 --- a/crates/typst-library/src/loading/json.rs +++ b/crates/typst-library/src/loading/json.rs @@ -50,7 +50,6 @@ use crate::loading::{DataSource, Load, Readable}; /// ``` #[func(scope, title = "JSON")] pub fn json( - /// The engine. engine: &mut Engine, /// Path to a JSON file or raw JSON bytes. /// @@ -71,7 +70,6 @@ impl json { /// directly. #[func(title = "Decode JSON")] pub fn decode( - /// The engine. engine: &mut Engine, /// JSON data. data: Spanned, diff --git a/crates/typst-library/src/loading/read.rs b/crates/typst-library/src/loading/read.rs index bf363f846..32dadc799 100644 --- a/crates/typst-library/src/loading/read.rs +++ b/crates/typst-library/src/loading/read.rs @@ -24,7 +24,6 @@ use crate::World; /// ``` #[func] pub fn read( - /// The engine. engine: &mut Engine, /// Path to a file. /// diff --git a/crates/typst-library/src/loading/toml.rs b/crates/typst-library/src/loading/toml.rs index e3a01cdd5..402207b02 100644 --- a/crates/typst-library/src/loading/toml.rs +++ b/crates/typst-library/src/loading/toml.rs @@ -28,7 +28,6 @@ use crate::loading::{DataSource, Load, Readable}; /// ``` #[func(scope, title = "TOML")] pub fn toml( - /// The engine. engine: &mut Engine, /// A path to a TOML file or raw TOML bytes. /// @@ -50,7 +49,6 @@ impl toml { /// directly. #[func(title = "Decode TOML")] pub fn decode( - /// The engine. engine: &mut Engine, /// TOML data. data: Spanned, diff --git a/crates/typst-library/src/loading/xml.rs b/crates/typst-library/src/loading/xml.rs index 53ec3d93b..ca467c238 100644 --- a/crates/typst-library/src/loading/xml.rs +++ b/crates/typst-library/src/loading/xml.rs @@ -57,7 +57,6 @@ use crate::loading::{DataSource, Load, Readable}; /// ``` #[func(scope, title = "XML")] pub fn xml( - /// The engine. engine: &mut Engine, /// A path to an XML file or raw XML bytes. /// @@ -83,7 +82,6 @@ impl xml { /// directly. #[func(title = "Decode XML")] pub fn decode( - /// The engine. engine: &mut Engine, /// XML data. data: Spanned, diff --git a/crates/typst-library/src/loading/yaml.rs b/crates/typst-library/src/loading/yaml.rs index 2eb26be8f..5767cb640 100644 --- a/crates/typst-library/src/loading/yaml.rs +++ b/crates/typst-library/src/loading/yaml.rs @@ -40,7 +40,6 @@ use crate::loading::{DataSource, Load, Readable}; /// ``` #[func(scope, title = "YAML")] pub fn yaml( - /// The engine. engine: &mut Engine, /// A path to a YAML file or raw YAML bytes. /// @@ -61,7 +60,6 @@ impl yaml { /// directly. #[func(title = "Decode YAML")] pub fn decode( - /// The engine. engine: &mut Engine, /// YAML data. data: Spanned, diff --git a/crates/typst-library/src/math/root.rs b/crates/typst-library/src/math/root.rs index e25c6d423..ad111700b 100644 --- a/crates/typst-library/src/math/root.rs +++ b/crates/typst-library/src/math/root.rs @@ -10,7 +10,6 @@ use crate::math::Mathy; /// ``` #[func(title = "Square Root")] pub fn sqrt( - /// The call span of this function. span: Span, /// The expression to take the square root of. radicand: Content, diff --git a/crates/typst-library/src/model/numbering.rs b/crates/typst-library/src/model/numbering.rs index 4e2fe4579..150506758 100644 --- a/crates/typst-library/src/model/numbering.rs +++ b/crates/typst-library/src/model/numbering.rs @@ -53,9 +53,7 @@ use crate::text::Case; /// ``` #[func] pub fn numbering( - /// The engine. engine: &mut Engine, - /// The callsite context. context: Tracked, /// Defines how the numbering works. /// diff --git a/crates/typst-library/src/visualize/color.rs b/crates/typst-library/src/visualize/color.rs index 8ff8dbdbc..b14312513 100644 --- a/crates/typst-library/src/visualize/color.rs +++ b/crates/typst-library/src/visualize/color.rs @@ -248,8 +248,6 @@ impl Color { /// ``` #[func] pub fn luma( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The lightness component. #[external] @@ -300,8 +298,6 @@ impl Color { /// ``` #[func] pub fn oklab( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The lightness component. #[external] @@ -358,8 +354,6 @@ impl Color { /// ``` #[func] pub fn oklch( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The lightness component. #[external] @@ -420,8 +414,6 @@ impl Color { /// ``` #[func(title = "Linear RGB")] pub fn linear_rgb( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The red component. #[external] @@ -477,8 +469,6 @@ impl Color { /// ``` #[func(title = "RGB")] pub fn rgb( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The red component. #[external] @@ -555,8 +545,6 @@ impl Color { /// ``` #[func(title = "CMYK")] pub fn cmyk( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The cyan component. #[external] @@ -614,8 +602,6 @@ impl Color { /// ``` #[func(title = "HSL")] pub fn hsl( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The hue angle. #[external] @@ -673,8 +659,6 @@ impl Color { /// ``` #[func(title = "HSV")] pub fn hsv( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The hue angle. #[external] @@ -898,7 +882,6 @@ impl Color { #[func] pub fn saturate( self, - /// The call span span: Span, /// The factor to saturate the color by. factor: Ratio, @@ -924,7 +907,6 @@ impl Color { #[func] pub fn desaturate( self, - /// The call span span: Span, /// The factor to desaturate the color by. factor: Ratio, @@ -1001,7 +983,6 @@ impl Color { #[func] pub fn rotate( self, - /// The call span span: Span, /// The angle to rotate the hue by. angle: Angle, diff --git a/crates/typst-library/src/visualize/gradient.rs b/crates/typst-library/src/visualize/gradient.rs index e16e5d88a..431f07dd4 100644 --- a/crates/typst-library/src/visualize/gradient.rs +++ b/crates/typst-library/src/visualize/gradient.rs @@ -200,9 +200,7 @@ impl Gradient { /// ``` #[func(title = "Linear Gradient")] pub fn linear( - /// The args of this function. args: &mut Args, - /// The call site of this function. span: Span, /// The color [stops](#stops) of the gradient. #[variadic] @@ -292,7 +290,6 @@ impl Gradient { /// ``` #[func] fn radial( - /// The call site of this function. span: Span, /// The color [stops](#stops) of the gradient. #[variadic] @@ -407,7 +404,6 @@ impl Gradient { /// ``` #[func] pub fn conic( - /// The call site of this function. span: Span, /// The color [stops](#stops) of the gradient. #[variadic] diff --git a/crates/typst-library/src/visualize/image/mod.rs b/crates/typst-library/src/visualize/image/mod.rs index 0f0602011..77f8426e4 100644 --- a/crates/typst-library/src/visualize/image/mod.rs +++ b/crates/typst-library/src/visualize/image/mod.rs @@ -112,7 +112,6 @@ impl ImageElem { /// ``` #[func(title = "Decode Image")] pub fn decode( - /// The call span of this function. span: Span, /// The data to decode as an image. Can be a string for SVGs. data: Readable, diff --git a/crates/typst-library/src/visualize/polygon.rs b/crates/typst-library/src/visualize/polygon.rs index 465f2c1a7..42b083431 100644 --- a/crates/typst-library/src/visualize/polygon.rs +++ b/crates/typst-library/src/visualize/polygon.rs @@ -67,8 +67,8 @@ impl PolygonElem { /// ``` #[func(title = "Regular Polygon")] pub fn regular( - /// The call span of this function. span: Span, + /// How to fill the polygon. See the general /// [polygon's documentation]($polygon.fill) for more details. #[named] diff --git a/crates/typst-library/src/visualize/stroke.rs b/crates/typst-library/src/visualize/stroke.rs index 97a1535db..a0830cf19 100644 --- a/crates/typst-library/src/visualize/stroke.rs +++ b/crates/typst-library/src/visualize/stroke.rs @@ -97,8 +97,6 @@ impl Stroke { /// ``` #[func(constructor)] pub fn construct( - /// The real arguments (the other arguments are just for the docs, this - /// function is a bit involved, so we parse the arguments manually). args: &mut Args, /// The color or gradient to use for the stroke. diff --git a/crates/typst-library/src/visualize/tiling.rs b/crates/typst-library/src/visualize/tiling.rs index d699d3b6d..98a71f927 100644 --- a/crates/typst-library/src/visualize/tiling.rs +++ b/crates/typst-library/src/visualize/tiling.rs @@ -138,7 +138,6 @@ impl Tiling { #[func(constructor)] pub fn construct( engine: &mut Engine, - /// The callsite span. span: Span, /// The bounding box of each cell of the tiling. #[named]