Merge branch 'main' into krilla-port

This commit is contained in:
Laurenz Stampfl 2025-03-18 17:27:08 +01:00
commit 8b63d09c24
20 changed files with 115 additions and 30 deletions

1
Cargo.lock generated
View File

@ -2967,6 +2967,7 @@ dependencies = [
"dirs", "dirs",
"ecow", "ecow",
"env_proxy", "env_proxy",
"fastrand",
"flate2", "flate2",
"fontdb", "fontdb",
"native-tls", "native-tls",

View File

@ -55,6 +55,7 @@ ctrlc = "3.4.1"
dirs = "6" dirs = "6"
ecow = { version = "0.2", features = ["serde"] } ecow = { version = "0.2", features = ["serde"] }
env_proxy = "0.4" env_proxy = "0.4"
fastrand = "2.3"
flate2 = "1" flate2 = "1"
fontdb = { version = "0.23", default-features = false } fontdb = { version = "0.23", default-features = false }
fs_extra = "1.3" fs_extra = "1.3"

View File

@ -26,7 +26,7 @@ pub fn analyze_expr(
ast::Expr::Str(v) => Value::Str(v.get().into()), ast::Expr::Str(v) => Value::Str(v.get().into()),
_ => { _ => {
if node.kind() == SyntaxKind::Contextual { 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); return analyze_expr(world, &child);
} }
} }

View File

@ -19,6 +19,7 @@ typst-utils = { workspace = true }
dirs = { workspace = true, optional = true } dirs = { workspace = true, optional = true }
ecow = { workspace = true } ecow = { workspace = true }
env_proxy = { workspace = true, optional = true } env_proxy = { workspace = true, optional = true }
fastrand = { workspace = true, optional = true }
flate2 = { workspace = true, optional = true } flate2 = { workspace = true, optional = true }
fontdb = { workspace = true, optional = true } fontdb = { workspace = true, optional = true }
native-tls = { 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"] downloads = ["dep:env_proxy", "dep:native-tls", "dep:ureq", "dep:openssl"]
# Add package downloading utilities, implies `downloads` # 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: # Embeds some fonts into the binary:
# - For text: Libertinus Serif, New Computer Modern # - For text: Libertinus Serif, New Computer Modern

View File

@ -1,6 +1,7 @@
//! Download and unpack packages and package indices. //! Download and unpack packages and package indices.
use std::fs; use std::fs;
use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use ecow::eco_format; use ecow::eco_format;
@ -77,7 +78,8 @@ impl PackageStorage {
self.package_path.as_deref() 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( pub fn prepare_package(
&self, &self,
spec: &PackageSpec, spec: &PackageSpec,
@ -100,7 +102,7 @@ impl PackageStorage {
// Download from network if it doesn't exist yet. // Download from network if it doesn't exist yet.
if spec.namespace == DEFAULT_NAMESPACE { if spec.namespace == DEFAULT_NAMESPACE {
self.download_package(spec, &dir, progress)?; self.download_package(spec, cache_dir, progress)?;
if dir.exists() { if dir.exists() {
return Ok(dir); return Ok(dir);
} }
@ -110,7 +112,7 @@ impl PackageStorage {
Err(PackageError::NotFound(spec.clone())) 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( pub fn determine_latest_version(
&self, &self,
spec: &VersionlessPackageSpec, spec: &VersionlessPackageSpec,
@ -143,7 +145,7 @@ impl PackageStorage {
} }
/// Download the package index. The result of this is cached for efficiency. /// 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 self.index
.get_or_try_init(|| { .get_or_try_init(|| {
let url = format!("{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/index.json"); let url = format!("{DEFAULT_REGISTRY}/{DEFAULT_NAMESPACE}/index.json");
@ -164,10 +166,10 @@ impl PackageStorage {
/// ///
/// # Panics /// # Panics
/// Panics if the package spec namespace isn't `DEFAULT_NAMESPACE`. /// Panics if the package spec namespace isn't `DEFAULT_NAMESPACE`.
pub fn download_package( fn download_package(
&self, &self,
spec: &PackageSpec, spec: &PackageSpec,
package_dir: &Path, cache_dir: &Path,
progress: &mut dyn Progress, progress: &mut dyn Progress,
) -> PackageResult<()> { ) -> PackageResult<()> {
assert_eq!(spec.namespace, DEFAULT_NAMESPACE); 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()); let decompressed = flate2::read::GzDecoder::new(data.as_slice());
tar::Archive::new(decompressed).unpack(package_dir).map_err(|err| { tar::Archive::new(decompressed)
fs::remove_dir_all(package_dir).ok(); .unpack(&tempdir)
PackageError::MalformedArchive(Some(eco_format!("{err}"))) .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, 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<Self> {
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<Path> 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -115,7 +115,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
let column_height = regions.size.y; let column_height = regions.size.y;
let backlog: Vec<_> = std::iter::once(&column_height) let backlog: Vec<_> = std::iter::once(&column_height)
.chain(regions.backlog) .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) .skip(1)
.collect(); .collect();

View File

@ -1469,7 +1469,7 @@ impl<'a> GridLayouter<'a> {
// last height is the one for the current region. // last height is the one for the current region.
rowspan rowspan
.heights .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 // Ensure that, in this region, the rowspan will span at least
// this row. // this row.

View File

@ -85,14 +85,15 @@ pub fn layout_root(
ascent.set_max(shift_up + index.ascent()); 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 radicand_y = ascent - radicand.ascent();
let width = radicand_x + radicand.width(); let width = radicand_x + radicand.width();
let size = Size::new(width, ascent + descent); let size = Size::new(width, ascent + descent);
// The extra "- thickness" comes from the fact that the sqrt is placed // The extra "- thickness" comes from the fact that the sqrt is placed
// in `push_frame` with respect to its top, not its baseline. // 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 line_pos = Point::new(radicand_x, radicand_y - gap - (thickness / 2.0));
let radicand_pos = Point::new(radicand_x, radicand_y); let radicand_pos = Point::new(radicand_x, radicand_y);
@ -100,7 +101,8 @@ pub fn layout_root(
frame.set_baseline(ascent); frame.set_baseline(ascent);
if let Some(index) = index { 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); frame.push_frame(index_pos, index);
} }

View File

@ -302,6 +302,6 @@ fn assemble(
fn parts(assembly: GlyphAssembly, repeat: usize) -> impl Iterator<Item = GlyphPart> + '_ { fn parts(assembly: GlyphAssembly, repeat: usize) -> impl Iterator<Item = GlyphPart> + '_ {
assembly.parts.into_iter().flat_map(move |part| { assembly.parts.into_iter().flat_map(move |part| {
let count = if part.part_flags.extender() { repeat } else { 1 }; let count = if part.part_flags.extender() { repeat } else { 1 };
std::iter::repeat(part).take(count) std::iter::repeat_n(part, count)
}) })
} }

View File

@ -94,7 +94,7 @@ pub struct BibliographyElem {
/// - A path string to load a bibliography file from the given path. For /// - A path string to load a bibliography file from the given path. For
/// more details about paths, see the [Paths section]($syntax/#paths). /// more details about paths, see the [Paths section]($syntax/#paths).
/// - Raw bytes from which the bibliography should be decoded. /// - 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] #[required]
#[parse( #[parse(
let sources = args.expect("sources")?; let sources = args.expect("sources")?;

View File

@ -394,7 +394,7 @@ impl NumberingKind {
const SYMBOLS: &[char] = &['*', '†', '‡', '§', '¶', '‖']; const SYMBOLS: &[char] = &['*', '†', '‡', '§', '¶', '‖'];
let symbol = SYMBOLS[(n - 1) % SYMBOLS.len()]; let symbol = SYMBOLS[(n - 1) % SYMBOLS.len()];
let amount = ((n - 1) / SYMBOLS.len()) + 1; 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), Self::Hebrew => hebrew_numeral(n),

View File

@ -388,7 +388,7 @@ pub struct OutlineEntry {
/// space between the entry's body and the page number. When using show /// 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 /// rules to override outline entries, it is thus recommended to wrap the
/// fill in a [`box`] with fractional width, i.e. /// 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 /// When using [`repeat`], the [`gap`]($repeat.gap) property can be useful
/// to tweak the visual weight of the fill. /// to tweak the visual weight of the fill.

View File

@ -188,7 +188,7 @@ pub struct RawElem {
/// - A path string to load a syntax file from the given path. For more /// - A path string to load a syntax file from the given path. For more
/// details about paths, see the [Paths section]($syntax/#paths). /// details about paths, see the [Paths section]($syntax/#paths).
/// - Raw bytes from which the syntax should be decoded. /// - 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 /// ````example
/// #set raw(syntaxes: "SExpressions.sublime-syntax") /// #set raw(syntaxes: "SExpressions.sublime-syntax")

View File

@ -574,8 +574,7 @@ impl Gradient {
} }
let n = repetitions.v; let n = repetitions.v;
let mut stops = std::iter::repeat(self.stops_ref()) let mut stops = std::iter::repeat_n(self.stops_ref(), n)
.take(n)
.enumerate() .enumerate()
.flat_map(|(i, stops)| { .flat_map(|(i, stops)| {
let mut stops = stops let mut stops = stops

View File

@ -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 dash = dash.as_ref().and_then(to_sk_dash_pattern);
let bbox = shape.geometry.bbox_size(); let bbox = shape.geometry.bbox_size();
let offset_bbox = (!matches!(shape.geometry, Geometry::Line(..))) let offset_bbox = if !matches!(shape.geometry, Geometry::Line(..)) {
.then(|| offset_bounding_box(bbox, *thickness)) offset_bounding_box(bbox, *thickness)
.unwrap_or(bbox); } else {
bbox
};
let fill_transform = let fill_transform =
(!matches!(shape.geometry, Geometry::Line(..))).then(|| { (!matches!(shape.geometry, Geometry::Line(..))).then(|| {

View File

@ -172,7 +172,7 @@ nothing else.
For example, the image function expects a path to an image file. 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 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. 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. valid kind of content.
</div> </div>

View File

@ -44,7 +44,7 @@ I am #amazed(color: purple)[amazed]!
Templates now work by wrapping our whole document in a custom function like 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 `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 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 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. 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 Since the `amazed` function can be called with a single content argument, we can

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 B

View File

@ -44,3 +44,9 @@ $ root(2, x) quad
$ 2^3 = sqrt(2^3) $ $ 2^3 = sqrt(2^3) $
$ (x+y) quad x quad x $ $ (x+y) quad x quad x $
$ (2+3) = (sqrt(2)+3) $ $ (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) $