diff --git a/Cargo.lock b/Cargo.lock index c7dff8ec9..520486716 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2967,6 +2967,7 @@ dependencies = [ "dirs", "ecow", "env_proxy", + "fastrand", "flate2", "fontdb", "native-tls", diff --git a/Cargo.toml b/Cargo.toml index b225a5a0c..24a5168e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ ctrlc = "3.4.1" dirs = "6" ecow = { version = "0.2", features = ["serde"] } env_proxy = "0.4" +fastrand = "2.3" flate2 = "1" fontdb = { version = "0.23", default-features = false } fs_extra = "1.3" diff --git a/crates/typst-ide/src/analyze.rs b/crates/typst-ide/src/analyze.rs index 7ee83e709..c493da81a 100644 --- a/crates/typst-ide/src/analyze.rs +++ b/crates/typst-ide/src/analyze.rs @@ -26,7 +26,7 @@ pub fn analyze_expr( ast::Expr::Str(v) => Value::Str(v.get().into()), _ => { if node.kind() == SyntaxKind::Contextual { - if let Some(child) = node.children().last() { + if let Some(child) = node.children().next_back() { return analyze_expr(world, &child); } } diff --git a/crates/typst-kit/Cargo.toml b/crates/typst-kit/Cargo.toml index 52aa407c3..e59127d71 100644 --- a/crates/typst-kit/Cargo.toml +++ b/crates/typst-kit/Cargo.toml @@ -19,6 +19,7 @@ typst-utils = { workspace = true } dirs = { workspace = true, optional = true } ecow = { workspace = true } env_proxy = { workspace = true, optional = true } +fastrand = { workspace = true, optional = true } flate2 = { workspace = true, optional = true } fontdb = { workspace = true, optional = true } native-tls = { workspace = true, optional = true } @@ -43,7 +44,7 @@ fonts = ["dep:fontdb", "fontdb/memmap", "fontdb/fontconfig"] downloads = ["dep:env_proxy", "dep:native-tls", "dep:ureq", "dep:openssl"] # Add package downloading utilities, implies `downloads` -packages = ["downloads", "dep:dirs", "dep:flate2", "dep:tar"] +packages = ["downloads", "dep:dirs", "dep:flate2", "dep:tar", "dep:fastrand"] # Embeds some fonts into the binary: # - For text: Libertinus Serif, New Computer Modern diff --git a/crates/typst-kit/src/package.rs b/crates/typst-kit/src/package.rs index 172d8740a..584ec83c0 100644 --- a/crates/typst-kit/src/package.rs +++ b/crates/typst-kit/src/package.rs @@ -1,6 +1,7 @@ //! Download and unpack packages and package indices. use std::fs; +use std::io; use std::path::{Path, PathBuf}; use ecow::eco_format; @@ -77,7 +78,8 @@ impl PackageStorage { self.package_path.as_deref() } - /// Make a package available in the on-disk. + /// Makes a package available on-disk and returns the path at which it is + /// located (will be either in the cache or package directory). pub fn prepare_package( &self, spec: &PackageSpec, @@ -100,7 +102,7 @@ impl PackageStorage { // Download from network if it doesn't exist yet. if spec.namespace == DEFAULT_NAMESPACE { - self.download_package(spec, &dir, progress)?; + self.download_package(spec, cache_dir, progress)?; if dir.exists() { return Ok(dir); } @@ -110,7 +112,7 @@ impl PackageStorage { Err(PackageError::NotFound(spec.clone())) } - /// Try to determine the latest version of a package. + /// Tries to determine the latest version of a package. pub fn determine_latest_version( &self, spec: &VersionlessPackageSpec, @@ -143,7 +145,7 @@ impl PackageStorage { } /// Download the package index. The result of this is cached for efficiency. - pub fn download_index(&self) -> StrResult<&[serde_json::Value]> { + fn download_index(&self) -> StrResult<&[serde_json::Value]> { self.index .get_or_try_init(|| { let url = format!("{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/index.json"); @@ -164,10 +166,10 @@ impl PackageStorage { /// /// # Panics /// Panics if the package spec namespace isn't `DEFAULT_NAMESPACE`. - pub fn download_package( + fn download_package( &self, spec: &PackageSpec, - package_dir: &Path, + cache_dir: &Path, progress: &mut dyn Progress, ) -> PackageResult<()> { assert_eq!(spec.namespace, DEFAULT_NAMESPACE); @@ -191,11 +193,52 @@ impl PackageStorage { } }; + // The directory in which the package's version lives. + let base_dir = cache_dir.join(format!("{}/{}", spec.namespace, spec.name)); + + // The place at which the specific package version will live in the end. + let package_dir = base_dir.join(format!("{}", spec.version)); + + // To prevent multiple Typst instances from interferring, we download + // into a temporary directory first and then move this directory to + // its final destination. + // + // In the `rename` function's documentation it is stated: + // > This will not work if the new name is on a different mount point. + // + // By locating the temporary directory directly next to where the + // package directory will live, we are (trying our best) making sure + // that `tempdir` and `package_dir` are on the same mount point. + let tempdir = Tempdir::create(base_dir.join(format!( + ".tmp-{}-{}", + spec.version, + fastrand::u32(..), + ))) + .map_err(|err| error("failed to create temporary package directory", err))?; + + // Decompress the archive into the temporary directory. let decompressed = flate2::read::GzDecoder::new(data.as_slice()); - tar::Archive::new(decompressed).unpack(package_dir).map_err(|err| { - fs::remove_dir_all(package_dir).ok(); - PackageError::MalformedArchive(Some(eco_format!("{err}"))) - }) + tar::Archive::new(decompressed) + .unpack(&tempdir) + .map_err(|err| PackageError::MalformedArchive(Some(eco_format!("{err}"))))?; + + // When trying to move (i.e., `rename`) the directory from one place to + // another and the target/destination directory is empty, then the + // operation will succeed (if it's atomic, or hardware doesn't fail, or + // power doesn't go off, etc.). If however the target directory is not + // empty, i.e., another instance already successfully moved the package, + // then we can safely ignore the `DirectoryNotEmpty` error. + // + // This means that we do not check the integrity of an existing moved + // package, just like we don't check the integrity if the package + // directory already existed in the first place. If situations with + // broken packages still occur even with the rename safeguard, we might + // consider more complex solutions like file locking or checksums. + match fs::rename(&tempdir, &package_dir) { + Ok(()) => Ok(()), + Err(err) if err.kind() == io::ErrorKind::DirectoryNotEmpty => Ok(()), + Err(err) => Err(error("failed to move downloaded package directory", err)), + } } } @@ -207,6 +250,36 @@ struct MinimalPackageInfo { version: PackageVersion, } +/// A temporary directory that is a automatically cleaned up. +struct Tempdir(PathBuf); + +impl Tempdir { + /// Creates a directory at the path and auto-cleans it. + fn create(path: PathBuf) -> io::Result { + std::fs::create_dir_all(&path)?; + Ok(Self(path)) + } +} + +impl Drop for Tempdir { + fn drop(&mut self) { + _ = fs::remove_dir_all(&self.0); + } +} + +impl AsRef for Tempdir { + fn as_ref(&self) -> &Path { + &self.0 + } +} + +/// Enriches an I/O error with a message and turns it into a +/// `PackageError::Other`. +#[cold] +fn error(message: &str, err: io::Error) -> PackageError { + PackageError::Other(Some(eco_format!("{message}: {err}"))) +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/typst-layout/src/flow/compose.rs b/crates/typst-layout/src/flow/compose.rs index 76af8f650..54dc487a3 100644 --- a/crates/typst-layout/src/flow/compose.rs +++ b/crates/typst-layout/src/flow/compose.rs @@ -115,7 +115,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> { let column_height = regions.size.y; let backlog: Vec<_> = std::iter::once(&column_height) .chain(regions.backlog) - .flat_map(|&h| std::iter::repeat(h).take(self.config.columns.count)) + .flat_map(|&h| std::iter::repeat_n(h, self.config.columns.count)) .skip(1) .collect(); diff --git a/crates/typst-layout/src/grid/layouter.rs b/crates/typst-layout/src/grid/layouter.rs index af47ff72f..dc9e2238d 100644 --- a/crates/typst-layout/src/grid/layouter.rs +++ b/crates/typst-layout/src/grid/layouter.rs @@ -1469,7 +1469,7 @@ impl<'a> GridLayouter<'a> { // last height is the one for the current region. rowspan .heights - .extend(std::iter::repeat(Abs::zero()).take(amount_missing_heights)); + .extend(std::iter::repeat_n(Abs::zero(), amount_missing_heights)); // Ensure that, in this region, the rowspan will span at least // this row. diff --git a/crates/typst-layout/src/math/root.rs b/crates/typst-layout/src/math/root.rs index a6b5c03d0..c7f41488e 100644 --- a/crates/typst-layout/src/math/root.rs +++ b/crates/typst-layout/src/math/root.rs @@ -85,14 +85,15 @@ pub fn layout_root( ascent.set_max(shift_up + index.ascent()); } - let radicand_x = sqrt_offset + sqrt.width(); + let sqrt_x = sqrt_offset.max(Abs::zero()); + let radicand_x = sqrt_x + sqrt.width(); let radicand_y = ascent - radicand.ascent(); let width = radicand_x + radicand.width(); let size = Size::new(width, ascent + descent); // The extra "- thickness" comes from the fact that the sqrt is placed // in `push_frame` with respect to its top, not its baseline. - let sqrt_pos = Point::new(sqrt_offset, radicand_y - gap - thickness); + let sqrt_pos = Point::new(sqrt_x, radicand_y - gap - thickness); let line_pos = Point::new(radicand_x, radicand_y - gap - (thickness / 2.0)); let radicand_pos = Point::new(radicand_x, radicand_y); @@ -100,7 +101,8 @@ pub fn layout_root( frame.set_baseline(ascent); if let Some(index) = index { - let index_pos = Point::new(kern_before, ascent - index.ascent() - shift_up); + let index_x = -sqrt_offset.min(Abs::zero()) + kern_before; + let index_pos = Point::new(index_x, ascent - index.ascent() - shift_up); frame.push_frame(index_pos, index); } diff --git a/crates/typst-layout/src/math/stretch.rs b/crates/typst-layout/src/math/stretch.rs index dafa8cbe8..f45035e27 100644 --- a/crates/typst-layout/src/math/stretch.rs +++ b/crates/typst-layout/src/math/stretch.rs @@ -302,6 +302,6 @@ fn assemble( fn parts(assembly: GlyphAssembly, repeat: usize) -> impl Iterator + '_ { assembly.parts.into_iter().flat_map(move |part| { let count = if part.part_flags.extender() { repeat } else { 1 }; - std::iter::repeat(part).take(count) + std::iter::repeat_n(part, count) }) } diff --git a/crates/typst-library/src/foundations/cast.rs b/crates/typst-library/src/foundations/cast.rs index 38f409c67..73645491f 100644 --- a/crates/typst-library/src/foundations/cast.rs +++ b/crates/typst-library/src/foundations/cast.rs @@ -21,7 +21,7 @@ use crate::foundations::{ /// /// Type casting works as follows: /// - [`Reflect for T`](Reflect) describes the possible Typst values for `T` -/// (for documentation and autocomplete). +/// (for documentation and autocomplete). /// - [`IntoValue for T`](IntoValue) is for conversion from `T -> Value` /// (infallible) /// - [`FromValue for T`](FromValue) is for conversion from `Value -> T` diff --git a/crates/typst-library/src/model/bibliography.rs b/crates/typst-library/src/model/bibliography.rs index a391e5804..b11c61789 100644 --- a/crates/typst-library/src/model/bibliography.rs +++ b/crates/typst-library/src/model/bibliography.rs @@ -94,7 +94,7 @@ pub struct BibliographyElem { /// - 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. + /// - An array where each item is one of the above. #[required] #[parse( let sources = args.expect("sources")?; diff --git a/crates/typst-library/src/model/numbering.rs b/crates/typst-library/src/model/numbering.rs index 150506758..ada8a3965 100644 --- a/crates/typst-library/src/model/numbering.rs +++ b/crates/typst-library/src/model/numbering.rs @@ -394,7 +394,7 @@ impl NumberingKind { const SYMBOLS: &[char] = &['*', '†', '‡', '§', '¶', '‖']; let symbol = SYMBOLS[(n - 1) % SYMBOLS.len()]; let amount = ((n - 1) / SYMBOLS.len()) + 1; - std::iter::repeat(symbol).take(amount).collect() + std::iter::repeat_n(symbol, amount).collect() } Self::Hebrew => hebrew_numeral(n), diff --git a/crates/typst-library/src/model/outline.rs b/crates/typst-library/src/model/outline.rs index 7ceb530f8..489c375e6 100644 --- a/crates/typst-library/src/model/outline.rs +++ b/crates/typst-library/src/model/outline.rs @@ -388,7 +388,7 @@ pub struct OutlineEntry { /// space between the entry's body and the page number. When using show /// rules to override outline entries, it is thus recommended to wrap the /// fill in a [`box`] with fractional width, i.e. - /// `{box(width: 1fr, it.fill}`. + /// `{box(width: 1fr, it.fill)}`. /// /// When using [`repeat`], the [`gap`]($repeat.gap) property can be useful /// to tweak the visual weight of the fill. diff --git a/crates/typst-library/src/text/raw.rs b/crates/typst-library/src/text/raw.rs index 1ce8bfc61..d5c07424d 100644 --- a/crates/typst-library/src/text/raw.rs +++ b/crates/typst-library/src/text/raw.rs @@ -188,7 +188,7 @@ pub struct RawElem { /// - 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. + /// - An array where each item is one of the above. /// /// ````example /// #set raw(syntaxes: "SExpressions.sublime-syntax") diff --git a/crates/typst-library/src/visualize/gradient.rs b/crates/typst-library/src/visualize/gradient.rs index 1a723a9f5..d59175a4e 100644 --- a/crates/typst-library/src/visualize/gradient.rs +++ b/crates/typst-library/src/visualize/gradient.rs @@ -574,8 +574,7 @@ impl Gradient { } let n = repetitions.v; - let mut stops = std::iter::repeat(self.stops_ref()) - .take(n) + let mut stops = std::iter::repeat_n(self.stops_ref(), n) .enumerate() .flat_map(|(i, stops)| { let mut stops = stops diff --git a/crates/typst-render/src/shape.rs b/crates/typst-render/src/shape.rs index ba7ed6d89..9b50d5f1f 100644 --- a/crates/typst-render/src/shape.rs +++ b/crates/typst-render/src/shape.rs @@ -69,9 +69,11 @@ pub fn render_shape(canvas: &mut sk::Pixmap, state: State, shape: &Shape) -> Opt let dash = dash.as_ref().and_then(to_sk_dash_pattern); let bbox = shape.geometry.bbox_size(); - let offset_bbox = (!matches!(shape.geometry, Geometry::Line(..))) - .then(|| offset_bounding_box(bbox, *thickness)) - .unwrap_or(bbox); + let offset_bbox = if !matches!(shape.geometry, Geometry::Line(..)) { + offset_bounding_box(bbox, *thickness) + } else { + bbox + }; let fill_transform = (!matches!(shape.geometry, Geometry::Line(..))).then(|| { diff --git a/docs/tutorial/1-writing.md b/docs/tutorial/1-writing.md index 5a9fdd4f7..acc257830 100644 --- a/docs/tutorial/1-writing.md +++ b/docs/tutorial/1-writing.md @@ -172,7 +172,7 @@ nothing else. For example, the image function expects a path to an image file. It would not make sense to pass, e.g., a paragraph of text or another image as the image's path parameter. That's why only strings are allowed here. -On the contrary, strings work wherever content is expected because text is a +In contrast, strings work wherever content is expected because text is a valid kind of content. diff --git a/docs/tutorial/4-template.md b/docs/tutorial/4-template.md index 209fa5546..7542cd6e4 100644 --- a/docs/tutorial/4-template.md +++ b/docs/tutorial/4-template.md @@ -44,7 +44,7 @@ I am #amazed(color: purple)[amazed]! Templates now work by wrapping our whole document in a custom function like `amazed`. But wrapping a whole document in a giant function call would be cumbersome! Instead, we can use an "everything" show rule to achieve the same -with cleaner code. To write such a show rule, put a colon directly behind the +with cleaner code. To write such a show rule, put a colon directly after the show keyword and then provide a function. This function is given the rest of the document as a parameter. The function can then do anything with this content. Since the `amazed` function can be called with a single content argument, we can diff --git a/tests/ref/math-root-frame-size-index.png b/tests/ref/math-root-frame-size-index.png new file mode 100644 index 000000000..41d4df2e9 Binary files /dev/null and b/tests/ref/math-root-frame-size-index.png differ diff --git a/tests/suite/math/root.typ b/tests/suite/math/root.typ index df339fa80..b70b4b9bf 100644 --- a/tests/suite/math/root.typ +++ b/tests/suite/math/root.typ @@ -44,3 +44,9 @@ $ root(2, x) quad $ √2^3 = sqrt(2^3) $ $ √(x+y) quad ∛x quad ∜x $ $ (√2+3) = (sqrt(2)+3) $ + +--- math-root-frame-size-index --- +// Test size of final frame when there is an index. +$ a root(, 3) & a root(., 3) \ + a sqrt(3) & a root(2, 3) \ + a root(#h(-1em), 3) & a root(123, 3) $