Merge branch 'main' into warn-suppression

This commit is contained in:
PgBiel 2024-07-22 11:56:12 -03:00 committed by GitHub
commit c4c7cfc714
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
67 changed files with 757 additions and 342 deletions

1
Cargo.lock generated
View File

@ -2790,7 +2790,6 @@ dependencies = [
"typst-assets",
"typst-macros",
"typst-timing",
"unicode-properties",
"unscanny",
"xmp-writer",
]

View File

@ -114,7 +114,6 @@ typed-arena = "2"
unicode-bidi = "0.3.13"
unicode-ident = "1.0"
unicode-math-class = "0.1"
unicode-properties = "0.1"
unicode-script = "0.5"
unicode-segmentation = "1"
unscanny = "0.1"

View File

@ -10,10 +10,9 @@ use parking_lot::RwLock;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use typst::diag::{bail, Severity, SourceDiagnostic, StrResult, Warned};
use typst::foundations::{Datetime, Smart};
use typst::layout::{Frame, PageRanges};
use typst::layout::{Frame, Page, PageRanges};
use typst::model::Document;
use typst::syntax::{FileId, Source, Span};
use typst::visualize::Color;
use typst::WorldExt;
use crate::args::{
@ -269,7 +268,7 @@ fn export_image(
Output::Stdout => Output::Stdout,
};
export_image_page(command, &page.frame, &output, fmt)?;
export_image_page(command, page, &output, fmt)?;
Ok(())
})
.collect::<Result<Vec<()>, EcoString>>()?;
@ -309,13 +308,13 @@ mod output_template {
/// Export single image.
fn export_image_page(
command: &CompileCommand,
frame: &Frame,
page: &Page,
output: &Output,
fmt: ImageExportFormat,
) -> StrResult<()> {
match fmt {
ImageExportFormat::Png => {
let pixmap = typst_render::render(frame, command.ppi / 72.0, Color::WHITE);
let pixmap = typst_render::render(page, command.ppi / 72.0);
let buf = pixmap
.encode_png()
.map_err(|err| eco_format!("failed to encode PNG file ({err})"))?;
@ -324,7 +323,7 @@ fn export_image_page(
.map_err(|err| eco_format!("failed to write PNG file ({err})"))?;
}
ImageExportFormat::Svg => {
let svg = typst_svg::svg(frame);
let svg = typst_svg::svg(page);
output
.write(svg.as_bytes())
.map_err(|err| eco_format!("failed to write SVG file ({err})"))?;

View File

@ -8,7 +8,7 @@ use std::sync::Arc;
use std::time::{Duration, Instant};
use native_tls::{Certificate, TlsConnector};
use once_cell::sync::Lazy;
use once_cell::sync::OnceCell;
use ureq::Response;
use crate::terminal;
@ -16,13 +16,22 @@ use crate::terminal;
/// Keep track of this many download speed samples.
const SPEED_SAMPLES: usize = 5;
/// Lazily loads a custom CA certificate if present, but if there's an error
/// loading certificate, it just uses the default configuration.
static CERT: Lazy<Option<Certificate>> = Lazy::new(|| {
let path = crate::ARGS.cert.as_ref()?;
let pem = std::fs::read(path).ok()?;
Certificate::from_pem(&pem).ok()
});
/// Load a certificate from the file system if the `--cert` argument or
/// `TYPST_CERT` environment variable is present. The certificate is cached for
/// efficiency.
///
/// - Returns `None` if `--cert` and `TYPST_CERT` are not set.
/// - Returns `Some(Ok(cert))` if the certificate was loaded successfully.
/// - Returns `Some(Err(err))` if an error occurred while loading the certificate.
fn cert() -> Option<Result<&'static Certificate, io::Error>> {
static CERT: OnceCell<Certificate> = OnceCell::new();
crate::ARGS.cert.as_ref().map(|path| {
CERT.get_or_try_init(|| {
let pem = std::fs::read(path)?;
Certificate::from_pem(&pem).map_err(io::Error::other)
})
})
}
/// Download binary data and display its progress.
#[allow(clippy::result_large_err)]
@ -49,8 +58,8 @@ pub fn download(url: &str) -> Result<ureq::Response, ureq::Error> {
}
// Apply a custom CA certificate if present.
if let Some(cert) = &*CERT {
tls.add_root_certificate(cert.clone());
if let Some(cert) = cert() {
tls.add_root_certificate(cert?.clone());
}
// Configure native TLS.

View File

@ -5,8 +5,8 @@ use ecow::{eco_format, EcoString};
use if_chain::if_chain;
use serde::{Deserialize, Serialize};
use typst::foundations::{
fields_on, format_str, mutable_methods_on, repr, AutoValue, CastInfo, Func, Label,
NoneValue, Repr, Scope, StyleChain, Styles, Type, Value,
fields_on, format_str, repr, AutoValue, CastInfo, Func, Label, NoneValue, Repr,
Scope, StyleChain, Styles, Type, Value,
};
use typst::model::Document;
use typst::syntax::{
@ -396,19 +396,6 @@ fn field_access_completions(
}
}
for &(method, args) in mutable_methods_on(value.ty()) {
ctx.completions.push(Completion {
kind: CompletionKind::Func,
label: method.into(),
apply: Some(if args {
eco_format!("{method}(${{}})")
} else {
eco_format!("{method}()${{}}")
}),
detail: None,
})
}
for &field in fields_on(value.ty()) {
// Complete the field name along with its value. Notes:
// 1. No parentheses since function fields cannot currently be called
@ -1394,7 +1381,7 @@ mod tests {
}
#[test]
fn test_whitespace_in_autocomplete() {
fn test_autocomplete_whitespace() {
//Check that extra space before '.' is handled correctly.
test("#() .", 5, &[], &["insert", "remove", "len", "all"]);
test("#{() .}", 6, &["insert", "remove", "len", "all"], &["foo"]);
@ -1404,10 +1391,16 @@ mod tests {
}
#[test]
fn test_before_window_char_boundary() {
fn test_autocomplete_before_window_char_boundary() {
// Check that the `before_window` doesn't slice into invalid byte
// boundaries.
let s = "😀😀 #text(font: \"\")";
test(s, s.len() - 2, &[], &[]);
}
#[test]
fn test_autocomplete_mutable_method() {
let s = "#{ let x = (1, 2, 3); x. }";
test(s, s.len() - 2, &["at", "push", "pop"], &[]);
}
}

View File

@ -29,7 +29,6 @@ pdf-writer = { workspace = true }
subsetter = { workspace = true }
svg2pdf = { workspace = true }
ttf-parser = { workspace = true }
unicode-properties = { workspace = true }
unscanny = { workspace = true }
xmp-writer = { workspace = true }

View File

@ -243,7 +243,7 @@ impl ColorFontMap<()> {
let width =
font.advance(gid).unwrap_or(Em::new(0.0)).get() * font.units_per_em();
let instructions =
content::build(&mut self.resources, &frame, Some(width as f32));
content::build(&mut self.resources, &frame, None, Some(width as f32));
color_font.glyphs.push(ColorGlyph { gid, instructions });
color_font.glyph_indices.insert(gid, index);

View File

@ -16,7 +16,8 @@ use typst::model::Destination;
use typst::text::{color::is_color_glyph, Font, TextItem, TextItemView};
use typst::utils::{Deferred, Numeric, SliceExt};
use typst::visualize::{
FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, Shape,
FillRule, FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem,
Shape,
};
use crate::color_font::ColorFontMap;
@ -36,6 +37,7 @@ use crate::{deflate_deferred, AbsExt, EmExt};
pub fn build(
resources: &mut Resources<()>,
frame: &Frame,
fill: Option<Paint>,
color_glyph_width: Option<f32>,
) -> Encoded {
let size = frame.size();
@ -53,6 +55,11 @@ pub fn build(
.post_concat(Transform::translate(Abs::zero(), size.y)),
);
if let Some(fill) = fill {
let shape = Geometry::Rect(frame.size()).filled(fill);
write_shape(&mut ctx, Point::zero(), &shape);
}
// Encode the frame into the content stream.
write_frame(&mut ctx, frame);
@ -630,11 +637,13 @@ fn write_shape(ctx: &mut Builder, pos: Point, shape: &Shape) {
}
}
match (&shape.fill, stroke) {
(None, None) => unreachable!(),
(Some(_), None) => ctx.content.fill_nonzero(),
(None, Some(_)) => ctx.content.stroke(),
(Some(_), Some(_)) => ctx.content.fill_nonzero_and_stroke(),
match (&shape.fill, &shape.fill_rule, stroke) {
(None, _, None) => unreachable!(),
(Some(_), FillRule::NonZero, None) => ctx.content.fill_nonzero(),
(Some(_), FillRule::EvenOdd, None) => ctx.content.fill_even_odd(),
(None, _, Some(_)) => ctx.content.stroke(),
(Some(_), FillRule::NonZero, Some(_)) => ctx.content.fill_nonzero_and_stroke(),
(Some(_), FillRule::EvenOdd, Some(_)) => ctx.content.fill_even_odd_and_stroke(),
};
}

View File

@ -12,7 +12,6 @@ use subsetter::GlyphRemapper;
use ttf_parser::{name_id, GlyphId, Tag};
use typst::text::Font;
use typst::utils::SliceExt;
use unicode_properties::{GeneralCategory, UnicodeGeneralCategory};
use crate::{deflate, EmExt, PdfChunk, WithGlobalRefs};
@ -226,38 +225,6 @@ pub(crate) fn subset_tag<T: Hash>(glyphs: &T) -> EcoString {
std::str::from_utf8(&letter).unwrap().into()
}
/// For glyphs that have codepoints mapping to them in the font's cmap table, we
/// prefer them over pre-existing text mappings from the document. Only things
/// that don't have a corresponding codepoint (or only a private-use one) like
/// the "Th" in Linux Libertine get the text of their first occurrences in the
/// document instead.
///
/// This function replaces as much copepoints from the document with ones from
/// the cmap table as possible.
pub fn improve_glyph_sets(glyph_sets: &mut HashMap<Font, BTreeMap<u16, EcoString>>) {
for (font, glyph_set) in glyph_sets {
let ttf = font.ttf();
for subtable in ttf.tables().cmap.into_iter().flat_map(|table| table.subtables) {
if !subtable.is_unicode() {
continue;
}
subtable.codepoints(|n| {
let Some(c) = std::char::from_u32(n) else { return };
if c.general_category() == GeneralCategory::PrivateUse {
return;
}
let Some(GlyphId(g)) = ttf.glyph_index(c) else { return };
if glyph_set.contains_key(&g) {
glyph_set.insert(g, c.into());
}
});
}
}
}
/// Create a compressed `/ToUnicode` CMap.
#[comemo::memoize]
#[typst_macros::time(name = "create cmap")]

View File

@ -8,12 +8,12 @@ use pdf_writer::{
};
use typst::foundations::Label;
use typst::introspection::Location;
use typst::layout::{Abs, Frame};
use typst::layout::{Abs, Page};
use typst::model::{Destination, Numbering};
use typst::text::Case;
use crate::Resources;
use crate::{content, AbsExt, PdfChunk, WithDocument, WithRefs, WithResources};
use crate::{font::improve_glyph_sets, Resources};
/// Construct page objects.
#[typst_macros::time(name = "construct pages")]
@ -33,7 +33,7 @@ pub fn traverse_pages(
pages.push(None);
skipped_pages += 1;
} else {
let mut encoded = construct_page(&mut resources, &page.frame);
let mut encoded = construct_page(&mut resources, page);
encoded.label = page
.numbering
.as_ref()
@ -52,17 +52,13 @@ pub fn traverse_pages(
}
}
improve_glyph_sets(&mut resources.glyph_sets);
improve_glyph_sets(&mut resources.color_glyph_sets);
(PdfChunk::new(), (pages, resources))
}
/// Construct a page object.
#[typst_macros::time(name = "construct page")]
fn construct_page(out: &mut Resources<()>, frame: &Frame) -> EncodedPage {
let content = content::build(out, frame, None);
fn construct_page(out: &mut Resources<()>, page: &Page) -> EncodedPage {
let content = content::build(out, &page.frame, page.fill_or_transparent(), None);
EncodedPage { content, label: None }
}

View File

@ -103,7 +103,7 @@ fn register_pattern(
};
// Render the body.
let content = content::build(&mut patterns.resources, pattern.frame(), None);
let content = content::build(&mut patterns.resources, pattern.frame(), None, None);
let pdf_pattern = PdfPattern {
transform,

View File

@ -77,11 +77,16 @@ pub struct Resources<R = Ref> {
pub languages: BTreeMap<Lang, usize>,
/// For each font a mapping from used glyphs to their text representation.
/// May contain multiple chars in case of ligatures or similar things. The
/// same glyph can have a different text representation within one document,
/// then we just save the first one. The resulting strings are used for the
/// PDF's /ToUnicode map for glyphs that don't have an entry in the font's
/// cmap. This is important for copy-paste and searching.
/// This is used for the PDF's /ToUnicode map, and important for copy-paste
/// and searching.
///
/// Note that the text representation may contain multiple chars in case of
/// ligatures or similar things, and it may have no entry in the font's cmap
/// (or only a private-use codepoint), like the “Th” in Linux Libertine.
///
/// A glyph may have multiple entries in the font's cmap, and even the same
/// glyph can have a different text representation within one document.
/// But /ToUnicode does not support that, so we just save the first occurrence.
pub glyph_sets: HashMap<Font, BTreeMap<u16, EcoString>>,
/// Same as `glyph_sets`, but for color fonts.
pub color_glyph_sets: HashMap<Font, BTreeMap<u16, EcoString>>,

View File

@ -7,45 +7,49 @@ mod text;
use tiny_skia as sk;
use typst::layout::{
Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Size, Transform,
Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Page, Point, Size, Transform,
};
use typst::model::Document;
use typst::visualize::Color;
use typst::visualize::{Color, Geometry, Paint};
/// Export a frame into a raster image.
/// Export a page into a raster image.
///
/// This renders the frame at the given number of pixels per point and returns
/// This renders the page at the given number of pixels per point and returns
/// the resulting `tiny-skia` pixel buffer.
#[typst_macros::time(name = "render")]
pub fn render(frame: &Frame, pixel_per_pt: f32, fill: Color) -> sk::Pixmap {
let size = frame.size();
pub fn render(page: &Page, pixel_per_pt: f32) -> sk::Pixmap {
let size = page.frame.size();
let pxw = (pixel_per_pt * size.x.to_f32()).round().max(1.0) as u32;
let pxh = (pixel_per_pt * size.y.to_f32()).round().max(1.0) as u32;
let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap();
canvas.fill(paint::to_sk_color(fill));
let ts = sk::Transform::from_scale(pixel_per_pt, pixel_per_pt);
render_frame(&mut canvas, State::new(size, ts, pixel_per_pt), frame);
let state = State::new(size, ts, pixel_per_pt);
let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap();
if let Some(fill) = page.fill_or_white() {
if let Paint::Solid(color) = fill {
canvas.fill(paint::to_sk_color(color));
} else {
let rect = Geometry::Rect(page.frame.size()).filled(fill);
shape::render_shape(&mut canvas, state, &rect);
}
}
render_frame(&mut canvas, state, &page.frame);
canvas
}
/// Export a document with potentially multiple pages into a single raster image.
///
/// The gap will be added between the individual frames.
pub fn render_merged(
document: &Document,
pixel_per_pt: f32,
frame_fill: Color,
gap: Abs,
gap_fill: Color,
fill: Option<Color>,
) -> sk::Pixmap {
let pixmaps: Vec<_> = document
.pages
.iter()
.map(|page| render(&page.frame, pixel_per_pt, frame_fill))
.collect();
let pixmaps: Vec<_> =
document.pages.iter().map(|page| render(page, pixel_per_pt)).collect();
let gap = (pixel_per_pt * gap.to_f32()).round() as u32;
let pxw = pixmaps.iter().map(sk::Pixmap::width).max().unwrap_or_default();
@ -53,7 +57,9 @@ pub fn render_merged(
+ gap * pixmaps.len().saturating_sub(1) as u32;
let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap();
canvas.fill(paint::to_sk_color(gap_fill));
if let Some(fill) = fill {
canvas.fill(paint::to_sk_color(fill));
}
let mut y = 0;
for pixmap in pixmaps {

View File

@ -1,7 +1,8 @@
use tiny_skia as sk;
use typst::layout::{Abs, Axes, Point, Ratio, Size};
use typst::visualize::{
DashPattern, FixedStroke, Geometry, LineCap, LineJoin, Path, PathItem, Shape,
DashPattern, FillRule, FixedStroke, Geometry, LineCap, LineJoin, Path, PathItem,
Shape,
};
use crate::{paint, AbsExt, State};
@ -51,7 +52,10 @@ pub fn render_shape(canvas: &mut sk::Pixmap, state: State, shape: &Shape) -> Opt
paint.anti_alias = false;
}
let rule = sk::FillRule::default();
let rule = match shape.fill_rule {
FillRule::NonZero => sk::FillRule::Winding,
FillRule::EvenOdd => sk::FillRule::EvenOdd,
};
canvas.fill_path(&path, &paint, rule, ts, state.mask);
}

View File

@ -11,11 +11,11 @@ use std::fmt::{self, Display, Formatter, Write};
use ecow::EcoString;
use ttf_parser::OutlineBuilder;
use typst::layout::{
Abs, Frame, FrameItem, FrameKind, GroupItem, Point, Ratio, Size, Transform,
Abs, Frame, FrameItem, FrameKind, GroupItem, Page, Point, Ratio, Size, Transform,
};
use typst::model::Document;
use typst::utils::hash128;
use typst::visualize::{Gradient, Pattern};
use typst::visualize::{Geometry, Gradient, Pattern};
use xmlwriter::XmlWriter;
use crate::paint::{GradientRef, PatternRef, SVGSubGradient};
@ -23,12 +23,12 @@ use crate::text::RenderedGlyph;
/// Export a frame into a SVG file.
#[typst_macros::time(name = "svg")]
pub fn svg(frame: &Frame) -> String {
pub fn svg(page: &Page) -> String {
let mut renderer = SVGRenderer::new();
renderer.write_header(frame.size());
renderer.write_header(page.frame.size());
let state = State::new(frame.size(), Transform::identity());
renderer.render_frame(state, Transform::identity(), frame);
let state = State::new(page.frame.size(), Transform::identity());
renderer.render_page(state, Transform::identity(), page);
renderer.finalize()
}
@ -57,7 +57,7 @@ pub fn svg_merged(document: &Document, padding: Abs) -> String {
for page in &document.pages {
let ts = Transform::translate(x, y);
let state = State::new(page.frame.size(), Transform::identity());
renderer.render_frame(state, ts, &page.frame);
renderer.render_page(state, ts, page);
y += page.frame.height() + padding;
}
@ -176,6 +176,16 @@ impl SVGRenderer {
self.xml.write_attribute("xmlns:h5", "http://www.w3.org/1999/xhtml");
}
/// Render a page with the given transform.
fn render_page(&mut self, state: State, ts: Transform, page: &Page) {
if let Some(fill) = page.fill_or_white() {
let shape = Geometry::Rect(page.frame.size()).filled(fill);
self.render_shape(state, &shape);
}
self.render_frame(state, ts, &page.frame);
}
/// Render a frame with the given transform.
fn render_frame(&mut self, state: State, ts: Transform, frame: &Frame) {
self.xml.start_element("g");

View File

@ -5,7 +5,7 @@ use ttf_parser::OutlineBuilder;
use typst::foundations::Repr;
use typst::layout::{Angle, Axes, Frame, Quadrant, Ratio, Size, Transform};
use typst::utils::hash128;
use typst::visualize::{Color, Gradient, Paint, Pattern, RatioOrAngle};
use typst::visualize::{Color, FillRule, Gradient, Paint, Pattern, RatioOrAngle};
use xmlwriter::XmlWriter;
use crate::{Id, SVGRenderer, State, SvgMatrix, SvgPathBuilder};
@ -31,7 +31,13 @@ impl SVGRenderer {
}
/// Write a fill attribute.
pub(super) fn write_fill(&mut self, fill: &Paint, size: Size, ts: Transform) {
pub(super) fn write_fill(
&mut self,
fill: &Paint,
fill_rule: FillRule,
size: Size,
ts: Transform,
) {
match fill {
Paint::Solid(color) => self.xml.write_attribute("fill", &color.encode()),
Paint::Gradient(gradient) => {
@ -43,6 +49,10 @@ impl SVGRenderer {
self.xml.write_attribute_fmt("fill", format_args!("url(#{id})"));
}
}
match fill_rule {
FillRule::NonZero => self.xml.write_attribute("fill-rule", "nonzero"),
FillRule::EvenOdd => self.xml.write_attribute("fill-rule", "evenodd"),
}
}
/// Pushes a gradient to the list of gradients to write SVG file.

View File

@ -17,6 +17,7 @@ impl SVGRenderer {
if let Some(paint) = &shape.fill {
self.write_fill(
paint,
shape.fill_rule,
self.shape_fill_size(state, paint, shape),
self.shape_paint_transform(state, paint, shape),
);

View File

@ -6,7 +6,7 @@ use ttf_parser::GlyphId;
use typst::layout::{Abs, Point, Ratio, Size, Transform};
use typst::text::{Font, TextItem};
use typst::utils::hash128;
use typst::visualize::{Image, Paint, RasterFormat, RelativeTo};
use typst::visualize::{FillRule, Image, Paint, RasterFormat, RelativeTo};
use crate::{SVGRenderer, State, SvgMatrix, SvgPathBuilder};
@ -138,6 +138,7 @@ impl SVGRenderer {
self.xml.write_attribute_fmt("x", format_args!("{x_offset}"));
self.write_fill(
&text.fill,
FillRule::default(),
Size::new(Abs::pt(width), Abs::pt(height)),
self.text_paint_transform(state, &text.fill),
);

View File

@ -81,6 +81,10 @@ pub use typst_macros::func;
/// body evaluates to the result of joining all expressions preceding the
/// `return`.
///
/// Functions that don't return any meaningful value return [`none`] instead.
/// The return type of such functions is not explicitly specified in the
/// documentation. (An example of this is [`array.push`]).
///
/// ```example
/// #let alert(body, fill: red) = {
/// set text(white)

View File

@ -3,7 +3,9 @@ use std::num::{NonZeroI64, NonZeroIsize, NonZeroU64, NonZeroUsize, ParseIntError
use ecow::{eco_format, EcoString};
use crate::diag::StrResult;
use crate::foundations::{cast, func, repr, scope, ty, Repr, Str, Value};
use crate::foundations::{
bail, cast, func, repr, scope, ty, Bytes, Cast, Repr, Str, Value,
};
/// A whole number.
///
@ -145,7 +147,6 @@ impl i64 {
#[func(title = "Bitwise Left Shift")]
pub fn bit_lshift(
self,
/// The amount of bits to shift. Must not be negative.
shift: u32,
) -> StrResult<i64> {
@ -168,7 +169,6 @@ impl i64 {
#[func(title = "Bitwise Right Shift")]
pub fn bit_rshift(
self,
/// The amount of bits to shift. Must not be negative.
///
/// Shifts larger than 63 are allowed and will cause the return value to
@ -178,7 +178,6 @@ impl i64 {
/// just applying this operation multiple times. Therefore, the shift will
/// always succeed.
shift: u32,
/// Toggles whether a logical (unsigned) right shift should be performed
/// instead of arithmetic right shift.
/// If this is `true`, negative operands will not preserve their sign bit,
@ -214,6 +213,126 @@ impl i64 {
self >> shift
}
}
/// Converts bytes to an integer.
///
/// ```example
/// #int.from-bytes(bytes((0, 0, 0, 0, 0, 0, 0, 1))) \
/// #int.from-bytes(bytes((1, 0, 0, 0, 0, 0, 0, 0)), endian: "big")
/// ```
#[func]
pub fn from_bytes(
/// The bytes that should be converted to an integer.
///
/// Must be of length at most 8 so that the result fits into a 64-bit
/// signed integer.
bytes: Bytes,
/// The endianness of the conversion.
#[named]
#[default(Endianness::Little)]
endian: Endianness,
/// Whether the bytes should be treated as a signed integer. If this is
/// `{true}` and the most significant bit is set, the resulting number
/// will negative.
#[named]
#[default(true)]
signed: bool,
) -> StrResult<i64> {
let len = bytes.len();
if len == 0 {
return Ok(0);
} else if len > 8 {
bail!("too many bytes to convert to a 64 bit number");
}
// `decimal` will hold the part of the buffer that should be filled with
// the input bytes, `rest` will remain as is or be filled with 0xFF for
// negative numbers if signed is true.
//
// big-endian: `decimal` will be the rightmost bytes of the buffer.
// - little-endian: `decimal` will be the leftmost bytes of the buffer.
let mut buf = [0u8; 8];
let (rest, decimal) = match endian {
Endianness::Big => buf.split_at_mut(8 - len),
Endianness::Little => {
let (first, second) = buf.split_at_mut(len);
(second, first)
}
};
decimal.copy_from_slice(bytes.as_ref());
// Perform sign-extension if necessary.
if signed {
let most_significant_byte = match endian {
Endianness::Big => decimal[0],
Endianness::Little => decimal[len - 1],
};
if most_significant_byte & 0b1000_0000 != 0 {
rest.fill(0xFF);
}
}
Ok(match endian {
Endianness::Big => i64::from_be_bytes(buf),
Endianness::Little => i64::from_le_bytes(buf),
})
}
/// Converts an integer to bytes.
///
/// ```example
/// #array(10000.to-bytes(endian: "big")) \
/// #array(10000.to-bytes(size: 4))
/// ```
#[func]
pub fn to_bytes(
self,
/// The endianness of the conversion.
#[named]
#[default(Endianness::Little)]
endian: Endianness,
/// The size in bytes of the resulting bytes (must be at least zero). If
/// the integer is too large to fit in the specified size, the
/// conversion will truncate the remaining bytes based on the
/// endianness. To keep the same resulting value, if the endianness is
/// big-endian, the truncation will happen at the rightmost bytes.
/// Otherwise, if the endianness is little-endian, the truncation will
/// happen at the leftmost bytes.
///
/// Be aware that if the integer is negative and the size is not enough
/// to make the number fit, when passing the resulting bytes to
/// `int.from-bytes`, the resulting number might be positive, as the
/// most significant bit might not be set to 1.
#[named]
#[default(8)]
size: usize,
) -> Bytes {
let array = match endian {
Endianness::Big => self.to_be_bytes(),
Endianness::Little => self.to_le_bytes(),
};
let mut buf = vec![0u8; size];
match endian {
Endianness::Big => {
// Copy the bytes from the array to the buffer, starting from
// the end of the buffer.
let buf_start = size.saturating_sub(8);
let array_start = 8usize.saturating_sub(size);
buf[buf_start..].copy_from_slice(&array[array_start..])
}
Endianness::Little => {
// Copy the bytes from the array to the buffer, starting from
// the beginning of the buffer.
let end = size.min(8);
buf[..end].copy_from_slice(&array[..end])
}
}
Bytes::from(buf)
}
}
impl Repr for i64 {
@ -222,6 +341,15 @@ impl Repr for i64 {
}
}
/// Represents the byte order used for converting integers to bytes and vice versa.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum Endianness {
/// Big-endian byte order: the highest-value byte is at the beginning of the bytes.
Big,
/// Little-endian byte order: the lowest-value byte is at the beginning of the bytes.
Little,
}
/// A value that can be cast to an integer.
pub struct ToInt(i64);

View File

@ -1,28 +1,9 @@
//! Handles special built-in methods on values.
use crate::diag::{At, SourceResult};
use crate::foundations::{Args, Array, Dict, Str, Type, Value};
use crate::foundations::{Args, Str, Type, Value};
use crate::syntax::Span;
/// List the available methods for a type and whether they take arguments.
pub fn mutable_methods_on(ty: Type) -> &'static [(&'static str, bool)] {
if ty == Type::of::<Array>() {
&[
("first", false),
("last", false),
("at", true),
("pop", false),
("push", true),
("insert", true),
("remove", true),
]
} else if ty == Type::of::<Dict>() {
&[("at", true), ("insert", true), ("remove", true)]
} else {
&[]
}
}
/// Whether a specific method is mutating.
pub(crate) fn is_mutating_method(method: &str) -> bool {
matches!(method, "push" | "pop" | "insert" | "remove")

View File

@ -49,7 +49,7 @@ pub use self::float::*;
pub use self::func::*;
pub use self::int::*;
pub use self::label::*;
pub use self::methods::*;
pub(crate) use self::methods::*;
pub use self::module::*;
pub use self::none::*;
pub use self::plugin::*;

View File

@ -7,7 +7,7 @@ use comemo::{Track, Tracked};
use ecow::{eco_vec, EcoString, EcoVec};
use smallvec::SmallVec;
use crate::diag::{SourceResult, Trace, Tracepoint};
use crate::diag::{warning, SourceResult, Trace, Tracepoint};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, func, ty, Content, Context, Element, Func, NativeElement, Packed, Repr,
@ -33,6 +33,8 @@ use crate::utils::LazyHash;
/// ```
#[func]
pub fn style(
/// The engine.
engine: &mut Engine,
/// The call site span.
span: Span,
/// A function to call with the styles. Its return value is displayed
@ -43,6 +45,11 @@ pub fn style(
/// content that depends on the style context it appears in.
func: Func,
) -> Content {
engine.sink.warn(warning!(
span, "`style` is deprecated";
hint: "use a `context` expression instead"
));
StyleElem::new(func).pack().spanned(span)
}

View File

@ -5,7 +5,7 @@ use comemo::{Track, Tracked, TrackedMut};
use ecow::{eco_format, eco_vec, EcoString, EcoVec};
use smallvec::{smallvec, SmallVec};
use crate::diag::{bail, At, HintedStrResult, SourceResult};
use crate::diag::{bail, warning, At, HintedStrResult, SourceResult};
use crate::engine::{Engine, Route, Sink, Traced};
use crate::foundations::{
cast, elem, func, scope, select_where, ty, Args, Array, Construct, Content, Context,
@ -464,6 +464,11 @@ impl Counter {
if let Ok(loc) = context.location() {
self.display_impl(engine, loc, numbering, both, context.styles().ok())
} else {
engine.sink.warn(warning!(
span, "`counter.display` without context is deprecated";
hint: "use it in a `context` expression instead"
));
Ok(CounterDisplayElem::new(self, numbering, both)
.pack()
.spanned(span)
@ -508,13 +513,19 @@ impl Counter {
context: Tracked<Context>,
/// The callsite span.
span: Span,
/// _Compatibility:_ This argument only exists for compatibility with
/// Typst 0.10 and lower and shouldn't be used anymore.
/// _Compatibility:_ This argument is deprecated. It only exists for
/// compatibility with Typst 0.10 and lower and shouldn't be used
/// anymore.
#[default]
location: Option<Location>,
) -> SourceResult<CounterState> {
if location.is_none() {
context.location().at(span)?;
} else {
engine.sink.warn(warning!(
span, "calling `counter.final` with a location is deprecated";
hint: "try removing the location argument"
));
}
let sequence = self.sequence(engine)?;

View File

@ -1,6 +1,6 @@
use comemo::{Track, Tracked};
use crate::diag::{HintedStrResult, SourceResult};
use crate::diag::{warning, HintedStrResult, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
cast, elem, func, Content, Context, Func, LocatableSelector, NativeElement, Packed,
@ -29,11 +29,12 @@ use crate::syntax::Span;
///
/// # Compatibility
/// In Typst 0.10 and lower, the `locate` function took a closure that made the
/// current location in the document available (like [`here`] does now).
/// Compatibility with the old way will remain for a while to give package
/// authors time to upgrade. To that effect, `locate` detects whether it
/// received a selector or a user-defined function and adjusts its semantics
/// accordingly. This behaviour will be removed in the future.
/// current location in the document available (like [`here`] does now). This
/// usage pattern is deprecated. Compatibility with the old way will remain for
/// a while to give package authors time to upgrade. To that effect, `locate`
/// detects whether it received a selector or a user-defined function and
/// adjusts its semantics accordingly. This behaviour will be removed in the
/// future.
#[func(contextual)]
pub fn locate(
/// The engine.
@ -56,6 +57,11 @@ pub fn locate(
LocateOutput::Location(selector.resolve_unique(engine.introspector, context)?)
}
LocateInput::Func(func) => {
engine.sink.warn(warning!(
span, "`locate` with callback function is deprecated";
hint: "use a `context` expression instead"
));
LocateOutput::Content(LocateElem::new(func).pack().spanned(span))
}
})

View File

@ -1,9 +1,10 @@
use comemo::Tracked;
use crate::diag::HintedStrResult;
use crate::diag::{warning, HintedStrResult};
use crate::engine::Engine;
use crate::foundations::{func, Array, Context, LocatableSelector, Value};
use crate::introspection::Location;
use crate::syntax::Span;
/// Finds elements in the document.
///
@ -141,6 +142,8 @@ pub fn query(
engine: &mut Engine,
/// The callsite context.
context: Tracked<Context>,
/// The span of the `query` call.
span: Span,
/// Can be
/// - an element function like a `heading` or `figure`,
/// - a `{<label>}`,
@ -149,13 +152,18 @@ pub fn query(
///
/// Only [locatable]($location/#locatable) element functions are supported.
target: LocatableSelector,
/// _Compatibility:_ This argument only exists for compatibility with
/// Typst 0.10 and lower and shouldn't be used anymore.
/// _Compatibility:_ This argument is deprecated. It only exists for
/// compatibility with Typst 0.10 and lower and shouldn't be used anymore.
#[default]
location: Option<Location>,
) -> HintedStrResult<Array> {
if location.is_none() {
context.introspect()?;
} else {
engine.sink.warn(warning!(
span, "calling `query` with a location is deprecated";
hint: "try removing the location argument"
));
}
let vec = engine.introspector.query(&target.0);

View File

@ -1,7 +1,7 @@
use comemo::{Track, Tracked, TrackedMut};
use ecow::{eco_format, eco_vec, EcoString, EcoVec};
use crate::diag::{bail, At, SourceResult};
use crate::diag::{bail, warning, At, SourceResult};
use crate::engine::{Engine, Route, Sink, Traced};
use crate::foundations::{
cast, elem, func, scope, select_where, ty, Args, Construct, Content, Context, Func,
@ -325,13 +325,19 @@ impl State {
context: Tracked<Context>,
/// The callsite span.
span: Span,
/// _Compatibility:_ This argument only exists for compatibility with
/// Typst 0.10 and lower and shouldn't be used anymore.
/// _Compatibility:_ This argument is deprecated. It only exists for
/// compatibility with Typst 0.10 and lower and shouldn't be used
/// anymore.
#[default]
location: Option<Location>,
) -> SourceResult<Value> {
if location.is_none() {
context.location().at(span)?;
} else {
engine.sink.warn(warning!(
span, "calling `state.final` with a location is deprecated";
hint: "try removing the location argument"
));
}
let sequence = self.sequence(engine)?;
@ -365,6 +371,8 @@ impl State {
#[func]
pub fn display(
self,
/// The engine.
engine: &mut Engine,
/// The span of the `display` call.
span: Span,
/// A function which receives the value of the state and can return
@ -373,6 +381,11 @@ impl State {
#[default]
func: Option<Func>,
) -> Content {
engine.sink.warn(warning!(
span, "`state.display` is deprecated";
hint: "use `state.get` in a `context` expression instead"
));
StateDisplayElem::new(self, func).pack().spanned(span)
}
}

View File

@ -133,7 +133,7 @@ pub fn line<'a>(
|| (p.justify && breakpoint != Breakpoint::Mandatory);
// Process dashes.
let dash = if breakpoint == Breakpoint::Hyphen || full.ends_with(SHY) {
let dash = if breakpoint.is_hyphen() || full.ends_with(SHY) {
Some(Dash::Soft)
} else if full.ends_with(HYPHEN) {
Some(Dash::Hard)

View File

@ -1,5 +1,6 @@
use std::ops::{Add, Sub};
use az::SaturatingAs;
use icu_properties::maps::{CodePointMapData, CodePointMapDataBorrowed};
use icu_properties::sets::CodePointSetData;
use icu_properties::LineBreak;
@ -21,10 +22,15 @@ use crate::text::{Lang, TextElem};
type Cost = f64;
// Cost parameters.
const DEFAULT_HYPH_COST: Cost = 0.5;
const DEFAULT_RUNT_COST: Cost = 0.5;
const CONSECUTIVE_DASH_COST: Cost = 0.3;
const MAX_COST: Cost = 1_000_000.0;
//
// We choose higher costs than the Knuth-Plass paper (which would be 50) because
// it hyphenates way to eagerly in Typst otherwise. Could be related to the
// ratios coming out differently since Typst doesn't have the concept of glue,
// so things work a bit differently.
const DEFAULT_HYPH_COST: Cost = 135.0;
const DEFAULT_RUNT_COST: Cost = 100.0;
// Other parameters.
const MIN_RATIO: f64 = -1.0;
const MIN_APPROX_RATIO: f64 = -0.5;
const BOUND_EPS: f64 = 1e-3;
@ -65,8 +71,9 @@ pub enum Breakpoint {
Normal,
/// A mandatory breakpoint (after '\n' or at the end of the text).
Mandatory,
/// An opportunity for hyphenating.
Hyphen,
/// An opportunity for hyphenating and how many chars are before/after it
/// in the word.
Hyphen(u8, u8),
}
impl Breakpoint {
@ -95,9 +102,14 @@ impl Breakpoint {
}
// Trim nothing further.
Self::Hyphen => line,
Self::Hyphen(..) => line,
}
}
/// Whether this is a hyphen breakpoint.
pub fn is_hyphen(self) -> bool {
matches!(self, Self::Hyphen(..))
}
}
/// Breaks the paragraph into lines.
@ -254,7 +266,6 @@ fn linebreak_optimized_bounded<'a>(
width,
&pred.line,
&attempt,
end,
breakpoint,
unbreakable,
);
@ -374,8 +385,6 @@ fn linebreak_optimized_approximate(
let mut prev_end = 0;
breakpoints(p, |end, breakpoint| {
let at_end = end == p.text.len();
// Find the optimal predecessor.
let mut best: Option<Entry> = None;
for (pred_index, pred) in table.iter().enumerate().skip(active) {
@ -384,13 +393,12 @@ fn linebreak_optimized_approximate(
// Whether the line is justified. This is not 100% accurate w.r.t
// to line()'s behaviour, but good enough.
let justify = p.justify && !at_end && breakpoint != Breakpoint::Mandatory;
let justify = p.justify && breakpoint != Breakpoint::Mandatory;
// We don't really know whether the line naturally ends with a dash
// here, so we can miss that case, but it's ok, since all of this
// just an estimate.
let consecutive_dash =
pred.breakpoint == Breakpoint::Hyphen && breakpoint == Breakpoint::Hyphen;
let consecutive_dash = pred.breakpoint.is_hyphen() && breakpoint.is_hyphen();
// Estimate how much the line's spaces would need to be stretched to
// make it the desired width. We trim at the end to not take into
@ -401,7 +409,7 @@ fn linebreak_optimized_approximate(
p,
width,
estimates.widths.estimate(start..trimmed_end)
+ if breakpoint == Breakpoint::Hyphen {
+ if breakpoint.is_hyphen() {
metrics.approx_hyphen_width
} else {
Abs::zero()
@ -416,7 +424,6 @@ fn linebreak_optimized_approximate(
metrics,
breakpoint,
line_ratio,
at_end,
justify,
unbreakable,
consecutive_dash,
@ -474,17 +481,8 @@ fn linebreak_optimized_approximate(
let Entry { end, breakpoint, unbreakable, .. } = table[idx];
let attempt = line(engine, p, start..end, breakpoint, Some(&pred));
let (_, line_cost) = ratio_and_cost(
p,
metrics,
width,
&pred,
&attempt,
end,
breakpoint,
unbreakable,
);
let (_, line_cost) =
ratio_and_cost(p, metrics, width, &pred, &attempt, breakpoint, unbreakable);
pred = attempt;
start = end;
@ -502,7 +500,6 @@ fn ratio_and_cost(
available_width: Abs,
pred: &Line,
attempt: &Line,
end: usize,
breakpoint: Breakpoint,
unbreakable: bool,
) -> (f64, Cost) {
@ -519,7 +516,6 @@ fn ratio_and_cost(
metrics,
breakpoint,
ratio,
end == p.text.len(),
attempt.justify,
unbreakable,
pred.dash.is_some() && attempt.dash.is_some(),
@ -569,57 +565,64 @@ fn raw_ratio(
}
/// Compute the cost of a line given raw metrics.
#[allow(clippy::too_many_arguments)]
///
/// This mostly follows the formula in the Knuth-Plass paper, but there are some
/// adjustments.
fn raw_cost(
metrics: &CostMetrics,
breakpoint: Breakpoint,
ratio: f64,
at_end: bool,
justify: bool,
unbreakable: bool,
consecutive_dash: bool,
approx: bool,
) -> Cost {
// Determine the cost of the line.
let mut cost = if ratio < metrics.min_ratio(approx) {
// Determine the stretch/shrink cost of the line.
let badness = if ratio < metrics.min_ratio(approx) {
// Overfull line always has maximum cost.
MAX_COST
} else if breakpoint == Breakpoint::Mandatory || at_end {
// - If ratio < 0, we always need to shrink the line (even the last one).
// - If ratio > 0, we need to stretch the line only when it is justified
// (last line is not justified by default even if `p.justify` is true).
if ratio < 0.0 || (ratio > 0.0 && justify) {
ratio.powi(3).abs()
1_000_000.0
} else if justify || ratio < 0.0 {
// If the line shall be justified or needs shrinking, it has normal
// badness with cost 100|ratio|^3. We limit the ratio to 10 as to not
// get to close to our maximum cost.
100.0 * ratio.abs().min(10.0).powi(3)
} else {
// If the line shouldn't be justified and doesn't need shrink, we don't
// pay any cost.
0.0
}
} else {
// Normal line with cost of |ratio^3|.
ratio.powi(3).abs()
};
// Penalize runts (lone words in the last line).
if unbreakable && at_end {
cost += metrics.runt_cost;
// Compute penalties.
let mut penalty = 0.0;
// Penalize runts (lone words before a mandatory break / at the end).
if unbreakable && breakpoint == Breakpoint::Mandatory {
penalty += metrics.runt_cost;
}
// Penalize hyphenation.
if breakpoint == Breakpoint::Hyphen {
cost += metrics.hyph_cost;
if let Breakpoint::Hyphen(l, r) = breakpoint {
// We penalize hyphenations close to the edges of the word (< LIMIT
// chars) extra. For each step of distance from the limit, we add 15%
// to the cost.
const LIMIT: u8 = 5;
let steps = LIMIT.saturating_sub(l) + LIMIT.saturating_sub(r);
let extra = 0.15 * steps as f64;
penalty += (1.0 + extra) * metrics.hyph_cost;
}
// In the Knuth paper, cost = (1 + 100|r|^3 + p)^2 + a,
// where r is the ratio, p=50 is the penalty, and a=3000 is
// consecutive the penalty. We divide the whole formula by 10,
// resulting (0.01 + |r|^3 + p)^2 + a, where p=0.5 and a=0.3
let mut cost = (0.01 + cost).powi(2);
// Penalize two consecutive dashes (not necessarily hyphens) extra.
// Penalize two consecutive dashes extra (not necessarily hyphens).
// Knuth-Plass does this separately after the squaring, with a higher cost,
// but I couldn't find any explanation as to why.
if consecutive_dash {
cost += CONSECUTIVE_DASH_COST;
penalty += metrics.hyph_cost;
}
cost
// From the Knuth-Plass Paper: $ (1 + beta_j + pi_j)^2 $.
//
// We add one to minimize the number of lines when everything else is more
// or less equal.
(1.0 + badness + penalty).powi(2)
}
/// Calls `f` for all possible points in the text where lines can broken.
@ -655,9 +658,9 @@ fn breakpoints(p: &Preparation, mut f: impl FnMut(usize, Breakpoint)) {
let (head, tail) = text.split_at(last);
if head.ends_with("://") || tail.starts_with("www.") {
let (link, _) = link_prefix(tail);
let end = last + link.len();
linebreak_link(link, |i| f(last + i, Breakpoint::Normal));
while iter.peek().is_some_and(|&p| p < end) {
last += link.len();
while iter.peek().is_some_and(|&p| p < last) {
iter.next();
}
}
@ -687,19 +690,17 @@ fn breakpoints(p: &Preparation, mut f: impl FnMut(usize, Breakpoint)) {
};
// Hyphenate between the last and current breakpoint.
if hyphenate {
let mut offset = last;
if hyphenate && last < point {
for segment in text[last..point].split_word_bounds() {
if !segment.is_empty() && segment.chars().all(char::is_alphabetic) {
hyphenations(p, &lb, offset, segment, &mut f);
hyphenations(p, &lb, last, segment, &mut f);
}
offset += segment.len();
last += segment.len();
}
}
// Call `f` for the UAX #14 break opportunity.
f(point, breakpoint);
last = point;
}
}
@ -713,10 +714,13 @@ fn hyphenations(
mut f: impl FnMut(usize, Breakpoint),
) {
let Some(lang) = lang_at(p, offset) else { return };
let count = word.chars().count();
let end = offset + word.len();
let mut chars = 0;
for syllable in hypher::hyphenate(word, lang) {
offset += syllable.len();
chars += syllable.chars().count();
// Don't hyphenate after the final syllable.
if offset == end {
@ -737,8 +741,12 @@ fn hyphenations(
continue;
}
// Determine the number of codepoints before and after the hyphenation.
let l = chars.saturating_as::<u8>();
let r = (count - chars).saturating_as::<u8>();
// Call `f` for the word-internal hyphenation opportunity.
f(offset, Breakpoint::Hyphen);
f(offset, Breakpoint::Hyphen(l, r));
}
}
@ -827,9 +835,9 @@ fn lang_at(p: &Preparation, offset: usize) -> Option<hypher::Lang> {
struct CostMetrics {
min_ratio: f64,
min_approx_ratio: f64,
approx_hyphen_width: Abs,
hyph_cost: Cost,
runt_cost: Cost,
approx_hyphen_width: Abs,
}
impl CostMetrics {
@ -839,10 +847,11 @@ impl CostMetrics {
// When justifying, we may stretch spaces below their natural width.
min_ratio: if p.justify { MIN_RATIO } else { 0.0 },
min_approx_ratio: if p.justify { MIN_APPROX_RATIO } else { 0.0 },
hyph_cost: DEFAULT_HYPH_COST * p.costs.hyphenation().get(),
runt_cost: DEFAULT_RUNT_COST * p.costs.runt().get(),
// Approximate hyphen width for estimates.
approx_hyphen_width: Em::new(0.33).at(p.size),
// Costs.
hyph_cost: DEFAULT_HYPH_COST * p.costs.hyphenation().get(),
runt_cost: DEFAULT_RUNT_COST * p.costs.runt().get(),
}
}

View File

@ -1,6 +1,6 @@
use comemo::Tracked;
use crate::diag::{At, SourceResult};
use crate::diag::{warning, At, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
dict, func, Content, Context, Dict, Resolve, Smart, StyleChain, Styles,
@ -76,13 +76,19 @@ pub fn measure(
height: Smart<Length>,
/// The content whose size to measure.
content: Content,
/// _Compatibility:_ This argument only exists for compatibility with
/// Typst 0.10 and lower and shouldn't be used anymore.
/// _Compatibility:_ This argument is deprecated. It only exists for
/// compatibility with Typst 0.10 and lower and shouldn't be used anymore.
#[default]
styles: Option<Styles>,
) -> SourceResult<Dict> {
let styles = match &styles {
Some(styles) => StyleChain::new(styles),
Some(styles) => {
engine.sink.warn(warning!(
span, "calling `measure` with a styles argument is deprecated";
hint: "try removing the styles argument"
));
StyleChain::new(styles)
}
None => context.styles().at(span)?,
};

View File

@ -24,7 +24,7 @@ use crate::layout::{
use crate::model::Numbering;
use crate::text::TextElem;
use crate::utils::{NonZeroExt, Numeric, Scalar};
use crate::visualize::Paint;
use crate::visualize::{Color, Paint};
/// Layouts its child onto one or multiple pages.
///
@ -178,12 +178,20 @@ pub struct PageElem {
#[default(NonZeroUsize::ONE)]
pub columns: NonZeroUsize,
/// The page's background color.
/// The page's background fill.
///
/// This instructs the printer to color the complete page with the given
/// color. If you are considering larger production runs, it may be more
/// environmentally friendly and cost-effective to source pre-dyed pages and
/// not set this property.
/// Setting this to something non-transparent instructs the printer to color
/// the complete page. If you are considering larger production runs, it may
/// be more environmentally friendly and cost-effective to source pre-dyed
/// pages and not set this property.
///
/// When set to `{none}`, the background becomes transparent. Note that PDF
/// pages will still appear with a (usually white) background in viewers,
/// but they are conceptually transparent. (If you print them, no color is
/// used for the background.)
///
/// The default of `{auto}` results in `{none}` for PDF output, and
/// `{white}` for PNG and SVG.
///
/// ```example
/// #set page(fill: rgb("444352"))
@ -191,7 +199,7 @@ pub struct PageElem {
/// *Dark mode enabled.*
/// ```
#[borrowed]
pub fill: Option<Paint>,
pub fill: Smart<Option<Paint>>,
/// How to [number]($numbering) the pages.
///
@ -555,13 +563,10 @@ impl PageLayout<'_> {
}
}
if let Some(fill) = fill {
frame.fill(fill.clone());
}
page_counter.visit(engine, &frame)?;
pages.push(Page {
frame,
fill: fill.clone(),
numbering: numbering.clone(),
number: page_counter.logical(),
});
@ -578,6 +583,15 @@ impl PageLayout<'_> {
pub struct Page {
/// The frame that defines the page.
pub frame: Frame,
/// How the page is filled.
///
/// - When `None`, the background is transparent.
/// - When `Auto`, the background is transparent for PDF and white
/// for raster and SVG targets.
///
/// Exporters should access the resolved value of this property through
/// `fill_or_transparent()` or `fill_or_white()`.
pub fill: Smart<Option<Paint>>,
/// The page's numbering.
pub numbering: Option<Numbering>,
/// The logical page number (controlled by `counter(page)` and may thus not
@ -585,6 +599,22 @@ pub struct Page {
pub number: usize,
}
impl Page {
/// Get the configured background or `None` if it is `Auto`.
///
/// This is used in PDF export.
pub fn fill_or_transparent(&self) -> Option<Paint> {
self.fill.clone().unwrap_or(None)
}
/// Get the configured background or white if it is `Auto`.
///
/// This is used in raster and SVG export.
pub fn fill_or_white(&self) -> Option<Paint> {
self.fill.clone().unwrap_or_else(|| Some(Color::WHITE.into()))
}
}
/// Specification of the page's margins.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Margin {

View File

@ -70,7 +70,7 @@ impl Ratio {
impl Debug for Ratio {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{:?}%", self.get())
write!(f, "{:?}%", self.get() * 100.0)
}
}

View File

@ -1,13 +1,18 @@
use crate::diag::SourceResult;
use std::ops::Div;
use once_cell::unsync::Lazy;
use crate::diag::{bail, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
elem, Content, NativeElement, Packed, Resolve, Show, StyleChain,
cast, elem, Content, NativeElement, Packed, Resolve, Show, Smart, StyleChain,
};
use crate::introspection::Locator;
use crate::layout::{
Abs, Alignment, Angle, Axes, BlockElem, FixedAlignment, Frame, HAlignment, Length,
Point, Ratio, Region, Regions, Rel, Size, VAlignment,
};
use crate::utils::Numeric;
/// Moves content without affecting layout.
///
@ -149,12 +154,11 @@ fn layout_rotate(
let align = elem.origin(styles).resolve(styles);
// Compute the new region's approximate size.
let size = region
.size
.to_point()
.transform_inf(Transform::rotate(angle))
.map(Abs::abs)
.to_size();
let size = if region.size.is_finite() {
compute_bounding_box(region.size, Transform::rotate(-angle)).1
} else {
Size::splat(Abs::inf())
};
measure_and_layout(
engine,
@ -188,15 +192,15 @@ pub struct ScaleElem {
let all = args.find()?;
args.named("x")?.or(all)
)]
#[default(Ratio::one())]
pub x: Ratio,
#[default(Smart::Custom(ScaleAmount::Ratio(Ratio::one())))]
pub x: Smart<ScaleAmount>,
/// The vertical scaling factor.
///
/// The body will be mirrored vertically if the parameter is negative.
#[parse(args.named("y")?.or(all))]
#[default(Ratio::one())]
pub y: Ratio,
#[default(Smart::Custom(ScaleAmount::Ratio(Ratio::one())))]
pub y: Smart<ScaleAmount>,
/// The origin of the transformation.
///
@ -242,12 +246,12 @@ fn layout_scale(
styles: StyleChain,
region: Region,
) -> SourceResult<Frame> {
let sx = elem.x(styles);
let sy = elem.y(styles);
let align = elem.origin(styles).resolve(styles);
// Compute the new region's approximate size.
let size = region.size.zip_map(Axes::new(sx, sy), |r, s| s.of(r)).map(Abs::abs);
let scale = elem.resolve_scale(engine, locator.relayout(), region.size, styles)?;
let size = region
.size
.zip_map(scale, |r, s| if r.is_finite() { Ratio::new(1.0 / s).of(r) } else { r })
.map(Abs::abs);
measure_and_layout(
engine,
@ -256,12 +260,84 @@ fn layout_scale(
size,
styles,
elem.body(),
Transform::scale(sx, sy),
align,
Transform::scale(scale.x, scale.y),
elem.origin(styles).resolve(styles),
elem.reflow(styles),
)
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
enum ScaleAmount {
Ratio(Ratio),
Length(Length),
}
impl Packed<ScaleElem> {
/// Resolves scale parameters, preserving aspect ratio if one of the scales is set to `auto`.
fn resolve_scale(
&self,
engine: &mut Engine,
locator: Locator,
container: Size,
styles: StyleChain,
) -> SourceResult<Axes<Ratio>> {
fn resolve_axis(
axis: Smart<ScaleAmount>,
body: impl Fn() -> SourceResult<Abs>,
styles: StyleChain,
) -> SourceResult<Smart<Ratio>> {
Ok(match axis {
Smart::Auto => Smart::Auto,
Smart::Custom(amt) => Smart::Custom(match amt {
ScaleAmount::Ratio(ratio) => ratio,
ScaleAmount::Length(length) => {
let length = length.resolve(styles);
Ratio::new(length.div(body()?))
}
}),
})
}
let size = Lazy::new(|| {
let pod = Regions::one(container, Axes::splat(false));
let frame = self.body().layout(engine, locator, styles, pod)?.into_frame();
SourceResult::Ok(frame.size())
});
let x = resolve_axis(
self.x(styles),
|| size.as_ref().map(|size| size.x).map_err(Clone::clone),
styles,
)?;
let y = resolve_axis(
self.y(styles),
|| size.as_ref().map(|size| size.y).map_err(Clone::clone),
styles,
)?;
match (x, y) {
(Smart::Auto, Smart::Auto) => {
bail!(self.span(), "x and y cannot both be auto")
}
(Smart::Custom(x), Smart::Custom(y)) => Ok(Axes::new(x, y)),
(Smart::Auto, Smart::Custom(v)) | (Smart::Custom(v), Smart::Auto) => {
Ok(Axes::splat(v))
}
}
}
}
cast! {
ScaleAmount,
self => match self {
ScaleAmount::Ratio(ratio) => ratio.into_value(),
ScaleAmount::Length(length) => length.into_value(),
},
ratio: Ratio => ScaleAmount::Ratio(ratio),
length: Length => ScaleAmount::Length(length),
}
/// A scale-skew-translate transformation.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Transform {
@ -416,7 +492,7 @@ fn measure_and_layout(
.pre_concat(Transform::translate(-x, -y));
// Compute the bounding box and offset and wrap in a new frame.
let (offset, size) = compute_bounding_box(&frame, ts);
let (offset, size) = compute_bounding_box(frame.size(), ts);
frame.transform(ts);
frame.translate(offset);
frame.set_size(size);
@ -439,20 +515,20 @@ fn measure_and_layout(
}
}
/// Computes the bounding box and offset of a transformed frame.
fn compute_bounding_box(frame: &Frame, ts: Transform) -> (Point, Size) {
/// Computes the bounding box and offset of a transformed area.
fn compute_bounding_box(size: Size, ts: Transform) -> (Point, Size) {
let top_left = Point::zero().transform_inf(ts);
let top_right = Point::new(frame.width(), Abs::zero()).transform_inf(ts);
let bottom_left = Point::new(Abs::zero(), frame.height()).transform_inf(ts);
let bottom_right = Point::new(frame.width(), frame.height()).transform_inf(ts);
let top_right = Point::with_x(size.x).transform_inf(ts);
let bottom_left = Point::with_y(size.y).transform_inf(ts);
let bottom_right = size.to_point().transform_inf(ts);
// We first compute the new bounding box of the rotated frame.
// We first compute the new bounding box of the rotated area.
let min_x = top_left.x.min(top_right.x).min(bottom_left.x).min(bottom_right.x);
let min_y = top_left.y.min(top_right.y).min(bottom_left.y).min(bottom_right.y);
let max_x = top_left.x.max(top_right.x).max(bottom_left.x).max(bottom_right.x);
let max_y = top_left.y.max(top_right.y).max(bottom_left.y).max(bottom_right.y);
// Then we compute the new size of the frame.
// Then we compute the new size of the area.
let width = max_x - min_x;
let height = max_y - min_y;

View File

@ -18,7 +18,7 @@ use crate::symbols::Symbol;
use crate::syntax::{Span, Spanned};
use crate::text::TextElem;
use crate::utils::Numeric;
use crate::visualize::{FixedStroke, Geometry, LineCap, Shape, Stroke};
use crate::visualize::{FillRule, FixedStroke, Geometry, LineCap, Shape, Stroke};
use super::delimiter_alignment;
@ -458,6 +458,12 @@ fn layout_mat_body(
gap: Axes<Rel<Abs>>,
span: Span,
) -> SourceResult<Frame> {
let ncols = rows.first().map_or(0, |row| row.len());
let nrows = rows.len();
if ncols == 0 || nrows == 0 {
return Ok(Frame::soft(Size::zero()));
}
let gap = gap.zip_map(ctx.regions.base(), Rel::relative_to);
let half_gap = gap * 0.5;
@ -483,12 +489,6 @@ fn layout_mat_body(
_ => (AugmentOffsets::default(), AugmentOffsets::default(), default_stroke),
};
let ncols = rows.first().map_or(0, |row| row.len());
let nrows = rows.len();
if ncols == 0 || nrows == 0 {
return Ok(Frame::soft(Size::zero()));
}
// Before the full matrix body can be laid out, the
// individual cells must first be independently laid out
// so we can ensure alignment across rows and columns.
@ -597,6 +597,7 @@ fn line_item(length: Abs, vertical: bool, stroke: FixedStroke, span: Span) -> Fr
Shape {
geometry: line_geom,
fill: None,
fill_rule: FillRule::default(),
stroke: Some(stroke),
},
span,

View File

@ -39,7 +39,13 @@ fn stretch_glyph(
short_fall: Abs,
horizontal: bool,
) -> VariantFragment {
// If the base glyph is good enough, use it.
let advance = if horizontal { base.width } else { base.height() };
let short_target = target - short_fall;
if short_target <= advance {
return base.into_variant();
}
let mut min_overlap = Abs::zero();
let construction = ctx
.table
@ -55,12 +61,6 @@ fn stretch_glyph(
})
.unwrap_or(GlyphConstruction { assembly: None, variants: LazyArray16::new(&[]) });
// If the base glyph is good enough, use it.
let advance = if horizontal { base.width } else { base.height() };
if short_target <= advance {
return base.into_variant();
}
// Search for a pre-made variant with a good advance.
let mut best_id = base.id;
let mut best_advance = base.width;

View File

@ -513,11 +513,6 @@ pub struct TextElem {
/// default of `auto`, prevents them. More nuanced cost specification for
/// these modifications is planned for the future.)
///
/// The default costs are an acceptable balance, but some may find that it
/// hyphenates or avoids runs too eagerly, breaking the flow of dense prose.
/// A cost of 600% (six times the normal cost) may work better for such
/// contexts.
///
/// ```example
/// #set text(hyphenate: true, size: 11.4pt)
/// #set par(justify: true)

View File

@ -10,7 +10,7 @@ use crate::introspection::Locator;
use crate::layout::{
Abs, Axes, BlockElem, Frame, FrameItem, Length, Point, Region, Rel, Size,
};
use crate::visualize::{FixedStroke, Geometry, Paint, Shape, Stroke};
use crate::visualize::{FillRule, FixedStroke, Geometry, Paint, Shape, Stroke};
use PathVertex::{AllControlPoints, MirroredControlPoint, Vertex};
@ -33,11 +33,12 @@ pub struct PathElem {
///
/// When setting a fill, the default stroke disappears. To create a
/// rectangle with both fill and stroke, you have to configure both.
///
/// Currently all paths are filled according to the [non-zero winding
/// rule](https://en.wikipedia.org/wiki/Nonzero-rule).
pub fill: Option<Paint>,
/// The rule used to fill the path.
#[default]
pub fill_rule: FillRule,
/// How to [stroke] the path. This can be:
///
/// Can be set to `{none}` to disable the stroke or to `{auto}` for a
@ -147,6 +148,7 @@ fn layout_path(
// Prepare fill and stroke.
let fill = elem.fill(styles);
let fill_rule = elem.fill_rule(styles);
let stroke = match elem.stroke(styles) {
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
Smart::Auto => None,
@ -154,7 +156,12 @@ fn layout_path(
};
let mut frame = Frame::soft(size);
let shape = Shape { geometry: Geometry::Path(path), stroke, fill };
let shape = Shape {
geometry: Geometry::Path(path),
stroke,
fill,
fill_rule,
};
frame.push(Point::zero(), FrameItem::Shape(shape, elem.span()));
Ok(frame)
}

View File

@ -9,7 +9,7 @@ use crate::introspection::Locator;
use crate::layout::{Axes, BlockElem, Em, Frame, FrameItem, Length, Point, Region, Rel};
use crate::syntax::Span;
use crate::utils::Numeric;
use crate::visualize::{FixedStroke, Geometry, Paint, Path, Shape, Stroke};
use crate::visualize::{FillRule, FixedStroke, Geometry, Paint, Path, Shape, Stroke};
/// A closed polygon.
///
@ -32,11 +32,12 @@ pub struct PolygonElem {
///
/// When setting a fill, the default stroke disappears. To create a
/// rectangle with both fill and stroke, you have to configure both.
///
/// Currently all polygons are filled according to the
/// [non-zero winding rule](https://en.wikipedia.org/wiki/Nonzero-rule).
pub fill: Option<Paint>,
/// The rule used to fill the polygon.
#[default]
pub fill_rule: FillRule,
/// How to [stroke] the polygon. This can be:
///
/// Can be set to `{none}` to disable the stroke or to `{auto}` for a
@ -161,6 +162,7 @@ fn layout_polygon(
// Prepare fill and stroke.
let fill = elem.fill(styles);
let fill_rule = elem.fill_rule(styles);
let stroke = match elem.stroke(styles) {
Smart::Auto if fill.is_none() => Some(FixedStroke::default()),
Smart::Auto => None,
@ -175,7 +177,12 @@ fn layout_polygon(
}
path.close_path();
let shape = Shape { geometry: Geometry::Path(path), stroke, fill };
let shape = Shape {
geometry: Geometry::Path(path),
stroke,
fill,
fill_rule,
};
frame.push(Point::zero(), FrameItem::Shape(shape, elem.span()));
Ok(frame)
}

View File

@ -2,7 +2,9 @@ use std::f64::consts::SQRT_2;
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{elem, Content, NativeElement, Packed, Show, Smart, StyleChain};
use crate::foundations::{
elem, Cast, Content, NativeElement, Packed, Show, Smart, StyleChain,
};
use crate::introspection::Locator;
use crate::layout::{
Abs, Axes, BlockElem, Corner, Corners, Frame, FrameItem, Length, Point, Ratio,
@ -583,10 +585,22 @@ pub struct Shape {
pub geometry: Geometry,
/// The shape's background fill.
pub fill: Option<Paint>,
/// The shape's fill rule.
pub fill_rule: FillRule,
/// The shape's border stroke.
pub stroke: Option<FixedStroke>,
}
/// A path filling rule.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum FillRule {
/// Specifies that "inside" is computed by a non-zero sum of signed edge crossings.
#[default]
NonZero,
/// Specifies that "inside" is computed by an odd number of edge crossings.
EvenOdd,
}
/// A shape's geometry.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum Geometry {
@ -601,12 +615,22 @@ pub enum Geometry {
impl Geometry {
/// Fill the geometry without a stroke.
pub fn filled(self, fill: Paint) -> Shape {
Shape { geometry: self, fill: Some(fill), stroke: None }
Shape {
geometry: self,
fill: Some(fill),
fill_rule: FillRule::default(),
stroke: None,
}
}
/// Stroke the geometry without a fill.
pub fn stroked(self, stroke: FixedStroke) -> Shape {
Shape { geometry: self, fill: None, stroke: Some(stroke) }
Shape {
geometry: self,
fill: None,
fill_rule: FillRule::default(),
stroke: Some(stroke),
}
}
/// The bounding box of the geometry.
@ -641,7 +665,12 @@ pub(crate) fn ellipse(
path.cubic_to(point(rx, my), point(mx, ry), point(z, ry));
path.cubic_to(point(-mx, ry), point(-rx, my), point(-rx, z));
Shape { geometry: Geometry::Path(path), stroke, fill }
Shape {
geometry: Geometry::Path(path),
stroke,
fill,
fill_rule: FillRule::default(),
}
}
/// Creates a new rectangle as a path.
@ -704,7 +733,12 @@ fn simple_rect(
fill: Option<Paint>,
stroke: Option<FixedStroke>,
) -> Vec<Shape> {
vec![Shape { geometry: Geometry::Rect(size), fill, stroke }]
vec![Shape {
geometry: Geometry::Rect(size),
fill,
stroke,
fill_rule: FillRule::default(),
}]
}
fn corners_control_points(
@ -779,6 +813,7 @@ fn segmented_rect(
res.push(Shape {
geometry: Geometry::Path(path),
fill: Some(fill),
fill_rule: FillRule::default(),
stroke: None,
});
stroke_insert += 1;
@ -916,6 +951,7 @@ fn stroke_segment(
geometry: Geometry::Path(path),
stroke: Some(stroke),
fill: None,
fill_rule: FillRule::default(),
}
}
@ -1014,6 +1050,7 @@ fn fill_segment(
geometry: Geometry::Path(path),
stroke: None,
fill: Some(stroke.paint.clone()),
fill_rule: FillRule::default(),
}
}

View File

@ -3,7 +3,6 @@ use std::path::{Path, PathBuf};
use clap::Parser;
use typst::model::Document;
use typst::visualize::Color;
use typst_docs::{provide, Html, Resolver};
use typst_render::render;
@ -35,8 +34,8 @@ impl<'a> Resolver for CliResolver<'a> {
);
}
let frame = &document.pages.first().expect("page 0").frame;
let pixmap = render(frame, 2.0, Color::WHITE);
let page = document.pages.first().expect("page 0");
let pixmap = render(page, 2.0);
let filename = format!("{hash:x}.png");
let path = self.assets_dir.join(&filename);
fs::create_dir_all(path.parent().expect("parent")).expect("create dir");

6
flake.lock generated
View File

@ -39,11 +39,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1706371002,
"narHash": "sha256-dwuorKimqSYgyu8Cw6ncKhyQjUDOyuXoxDTVmAXq88s=",
"lastModified": 1720957393,
"narHash": "sha256-oedh2RwpjEa+TNxhg5Je9Ch6d3W1NKi7DbRO1ziHemA=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "c002c6aa977ad22c60398daaa9be52f2203d0006",
"rev": "693bc46d169f5af9c992095736e82c3488bf7dbb",
"type": "github"
},
"original": {

View File

@ -6,7 +6,6 @@ use typst::foundations::{Bytes, Datetime};
use typst::syntax::{FileId, Source};
use typst::text::{Font, FontBook};
use typst::utils::LazyHash;
use typst::visualize::Color;
use typst::{Library, World};
struct FuzzWorld {
@ -68,7 +67,7 @@ fuzz_target!(|text: &str| {
let world = FuzzWorld::new(text);
if let Ok(document) = typst::compile(&world).output {
if let Some(page) = document.pages.first() {
std::hint::black_box(typst_render::render(&page.frame, 1.0, Color::WHITE));
std::hint::black_box(typst_render::render(page, 1.0));
}
}
comemo::evict(10);

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 423 B

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -359,13 +359,8 @@ fn render(document: &Document, pixel_per_pt: f32) -> sk::Pixmap {
}
let gap = Abs::pt(1.0);
let mut pixmap = typst_render::render_merged(
document,
pixel_per_pt,
Color::WHITE,
gap,
Color::BLACK,
);
let mut pixmap =
typst_render::render_merged(document, pixel_per_pt, gap, Some(Color::BLACK));
let gap = (pixel_per_pt * gap.to_pt() as f32).round();

View File

@ -43,10 +43,16 @@
#s.update(x =>
eval(expr.replace("x", str(x)))
)
// Warning: 17-28 `state.display` is deprecated
// Hint: 17-28 use `state.get` in a `context` expression instead
New value is #s.display().
]
// Warning: 1:2-6:3 `locate` with callback function is deprecated
// Hint: 1:2-6:3 use a `context` expression instead
#locate(loc => {
// Warning: 14-32 calling `query` with a location is deprecated
// Hint: 14-32 try removing the location argument
let elem = query(<here>, loc).first()
test(s.at(elem.location()), 13)
})
@ -58,8 +64,15 @@
#compute("x - 5")
--- context-compatibility-styling ---
// Warning: 2-53 `style` is deprecated
// Hint: 2-53 use a `context` expression instead
// Warning: 18-39 calling `measure` with a styles argument is deprecated
// Hint: 18-39 try removing the styles argument
#style(styles => measure([it], styles).width < 20pt)
--- context-compatibility-counter-display ---
#counter(heading).update(10)
// Warning: 2-44 `counter.display` without context is deprecated
// Hint: 2-44 use it in a `context` expression instead
#counter(heading).display(n => test(n, 10))

View File

@ -38,6 +38,24 @@
#test(int(10.0).signum(), 1)
#test(int(-10.0).signum(), -1)
--- int-from-and-to-bytes ---
// Test `int.from-bytes` and `int.to-bytes`.
#test(int.from-bytes(bytes(())), 0)
#test(int.from-bytes(bytes((1, 0, 0, 0, 0, 0, 0, 0)), endian: "little", signed: true), 1)
#test(int.from-bytes(bytes((1, 0, 0, 0, 0, 0, 0, 0)), endian: "big", signed: true), 72057594037927936)
#test(int.from-bytes(bytes((1, 0, 0, 0, 0, 0, 0, 0)), endian: "little", signed: false), 1)
#test(int.from-bytes(bytes((255,)), endian: "big", signed: true), -1)
#test(int.from-bytes(bytes((255,)), endian: "big", signed: false), 255)
#test(int.from-bytes((-1000).to-bytes(endian: "big", size: 5), endian: "big", signed: true), -1000)
#test(int.from-bytes((-1000).to-bytes(endian: "little", size: 5), endian: "little", signed: true), -1000)
#test(int.from-bytes(1000.to-bytes(endian: "big", size: 5), endian: "big", signed: true), 1000)
#test(int.from-bytes(1000.to-bytes(endian: "little", size: 5), endian: "little", signed: true), 1000)
#test(int.from-bytes(1000.to-bytes(endian: "little", size: 5), endian: "little", signed: false), 1000)
--- int-from-and-to-bytes-too-many ---
// Error: 2-34 too many bytes to convert to a 64 bit number
#int.from-bytes(bytes((0,) * 16))
--- int-repr ---
// Test the `repr` function with integers.
#repr(12) \

View File

@ -79,7 +79,7 @@ At Beta, it was #context {
--- issue-2480-counter-reset ---
#let q = counter("question")
#let step-show = q.step() + q.display("1")
#let step-show = q.step() + context q.display("1")
#let g = grid(step-show, step-show, gutter: 2pt)
#g

View File

@ -282,17 +282,17 @@
rows: (auto, 2em, auto, auto),
table.header(
[eeec],
table.cell(rowspan: 2, count.step() + count.display()),
table.cell(rowspan: 2, count.step() + context count.display()),
),
[d],
block(width: 5em, fill: yellow, lorem(7)),
[d],
table.footer(
[eeec],
table.cell(rowspan: 2, count.step() + count.display()),
table.cell(rowspan: 2, count.step() + context count.display()),
)
)
#count.display()
#context count.display()
--- grid-nested-with-footers ---
// Nested table with footer should repeat both footers

View File

@ -265,13 +265,13 @@
rows: (auto, 2em, auto, auto),
table.header(
[eeec],
table.cell(rowspan: 2, count.step() + count.display()),
table.cell(rowspan: 2, count.step() + context count.display()),
),
[d],
block(width: 5em, fill: yellow, lorem(15)),
[d]
)
#count.display()
#context count.display()
--- grid-header-expand ---
// Ensure header expands to fit cell placed in it after its declaration

View File

@ -163,7 +163,7 @@
#let count = counter("count")
#show grid.cell: it => {
count.step()
count.display()
context count.display()
}
#grid(

View File

@ -107,3 +107,11 @@ For info see #link("https://myhost.tld").
#set page(width: 50pt, height: auto)
#h(99%) 🏳️‍🌈
🏳️‍🌈
--- issue-hyphenate-in-link ---
#set par(justify: true)
// The `linebreak()` function accidentally generated out-of-order breakpoints
// for links because it now splits on word boundaries. We avoid the link markup
// syntax because it's show rule interferes.
#"http://creativecommons.org/licenses/by-nc-sa/4.0/"

View File

@ -66,7 +66,13 @@
// Test page fill.
#set page(width: 80pt, height: 40pt, fill: eastern)
#text(15pt, font: "Roboto", fill: white, smallcaps[Typst])
#page(width: 40pt, fill: none, margin: (top: 10pt, rest: auto))[Hi]
#page(width: 40pt, fill: auto, margin: (top: 10pt, rest: auto))[Hi]
--- page-fill-none ---
// Test disabling page fill.
// The PNG is filled with black anyway due to the test runner.
#set page(fill: none)
#rect(fill: green)
--- page-margin-uniform ---
// Set all margins at once.

View File

@ -34,7 +34,7 @@ Totally #h() ignored
h(1em)
counter(heading).update(4)
[Hello ]
counter(heading).display()
context counter(heading).display()
}
--- trim-weak-space-line-beginning ---

View File

@ -74,7 +74,7 @@ Hello #rotated[World]!\
Hello #rotated[World]!
--- transform-scale ---
// Test that scaling impact layout.
// Test that scaling impacts layout.
#set page(width: 200pt)
#set text(size: 32pt)
#let scaled(body) = box(scale(
@ -104,3 +104,14 @@ Hello #scaled[World]!\
#set scale(reflow: true)
Hello #scaled[World]!
--- transform-scale-abs-and-auto ---
// Test scaling by absolute lengths and auto.
#set page(width: 200pt, height: 200pt)
#let cylinder = image("/assets/images/cylinder.svg")
#cylinder
#scale(x: 100pt, y: 50pt, reflow: true, cylinder)
#scale(x: auto, y: 50pt, reflow: true, cylinder)
#scale(x: 100pt, y: auto, reflow: true, cylinder)
#scale(x: 150%, y: auto, reflow: true, cylinder)

View File

@ -12,8 +12,13 @@ fi
--- fold-vec-order-meta ---
#let c = counter("mycounter")
#c.update(1)
// Warning: 1:2-7:3 `locate` with callback function is deprecated
// Hint: 1:2-7:3 use a `context` expression instead
#locate(loc => [
#c.update(2)
#c.at(loc) \
// Warning: 12-36 `locate` with callback function is deprecated
// Hint: 12-36 use a `context` expression instead
Second: #locate(loc => c.at(loc))
])

View File

@ -1,10 +1,10 @@
// Test paths.
--- path ---
#set page(height: 200pt, width: 200pt)
#set page(height: 300pt, width: 200pt)
#table(
columns: (1fr, 1fr),
rows: (1fr, 1fr),
rows: (1fr, 1fr, 1fr),
align: center + horizon,
path(
fill: red,
@ -37,6 +37,26 @@
(30pt, 30pt),
(15pt, 0pt),
),
path(
fill: red,
fill-rule: "non-zero",
closed: true,
(25pt, 0pt),
(10pt, 50pt),
(50pt, 20pt),
(0pt, 20pt),
(40pt, 50pt),
),
path(
fill: red,
fill-rule: "even-odd",
closed: true,
(25pt, 0pt),
(10pt, 50pt),
(50pt, 20pt),
(0pt, 20pt),
(40pt, 50pt),
),
)
--- path-bad-vertex ---

View File

@ -21,24 +21,30 @@
--- pattern-relative-self ---
// Test with relative set to `"self"`
#let pat(..args) = pattern(size: (30pt, 30pt), ..args)[
#set line(stroke: green)
#place(top + left, line(start: (0%, 0%), end: (100%, 100%), stroke: 1pt))
#place(top + left, line(start: (0%, 100%), end: (100%, 0%), stroke: 1pt))
]
#set page(fill: pat(), width: 100pt, height: 100pt)
#rect(fill: pat(relative: "self"), width: 100%, height: 100%, stroke: 1pt)
#rect(
width: 100%,
height: 100%,
fill: pat(relative: "self"),
stroke: 1pt + green,
)
--- pattern-relative-parent ---
// Test with relative set to `"parent"`
#let pat(..args) = pattern(size: (30pt, 30pt), ..args)[
#let pat(fill, ..args) = pattern(size: (30pt, 30pt), ..args)[
#rect(width: 100%, height: 100%, fill: fill, stroke: none)
#place(top + left, line(start: (0%, 0%), end: (100%, 100%), stroke: 1pt))
#place(top + left, line(start: (0%, 100%), end: (100%, 0%), stroke: 1pt))
]
#set page(fill: pat(), width: 100pt, height: 100pt)
#set page(fill: pat(white), width: 100pt, height: 100pt)
#rect(fill: pat(relative: "parent"), width: 100%, height: 100%, stroke: 1pt)
#rect(fill: pat(none, relative: "parent"), width: 100%, height: 100%, stroke: 1pt)
--- pattern-small ---
// Tests small patterns for pixel accuracy.

View File

@ -27,6 +27,8 @@
// Self-intersections
#polygon((0pt, 10pt), (30pt, 20pt), (0pt, 30pt), (20pt, 0pt), (20pt, 35pt))
#polygon(fill-rule: "non-zero", (0pt, 10pt), (30pt, 20pt), (0pt, 30pt), (20pt, 0pt), (20pt, 35pt))
#polygon(fill-rule: "even-odd", (0pt, 10pt), (30pt, 20pt), (0pt, 30pt), (20pt, 0pt), (20pt, 35pt))
// Regular polygon; should have equal side lengths
#for k in range(3, 9) {polygon.regular(size: 30pt, vertices: k,)}