Merge branch 'main' into warn-suppression
1
Cargo.lock
generated
@ -2790,7 +2790,6 @@ dependencies = [
|
||||
"typst-assets",
|
||||
"typst-macros",
|
||||
"typst-timing",
|
||||
"unicode-properties",
|
||||
"unscanny",
|
||||
"xmp-writer",
|
||||
]
|
||||
|
@ -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"
|
||||
|
@ -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})"))?;
|
||||
|
@ -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.
|
||||
|
@ -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"], &[]);
|
||||
}
|
||||
}
|
||||
|
@ -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 }
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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")]
|
||||
|
@ -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 }
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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>>,
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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.
|
||||
|
@ -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),
|
||||
);
|
||||
|
@ -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),
|
||||
);
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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::*;
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)?;
|
||||
|
@ -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))
|
||||
}
|
||||
})
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)?,
|
||||
};
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
@ -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": {
|
||||
|
@ -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);
|
||||
|
BIN
tests/ref/issue-hyphenate-in-link.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 6.4 KiB |
BIN
tests/ref/page-fill-none.png
Normal file
After Width: | Height: | Size: 97 B |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 423 B After Width: | Height: | Size: 474 B |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.6 KiB |
BIN
tests/ref/transform-scale-abs-and-auto.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
@ -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();
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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) \
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -163,7 +163,7 @@
|
||||
#let count = counter("count")
|
||||
#show grid.cell: it => {
|
||||
count.step()
|
||||
count.display()
|
||||
context count.display()
|
||||
}
|
||||
|
||||
#grid(
|
||||
|
@ -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/"
|
||||
|
@ -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.
|
||||
|
@ -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 ---
|
||||
|
@ -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)
|
||||
|
@ -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))
|
||||
])
|
||||
|
@ -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 ---
|
||||
|
@ -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.
|
||||
|
@ -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,)}
|
||||
|