Merge branch 'main' into issue5490
7
Cargo.lock
generated
@ -890,9 +890,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.0"
|
||||
version = "0.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
|
||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||
|
||||
[[package]]
|
||||
name = "hayagriva"
|
||||
@ -1169,7 +1169,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.0",
|
||||
"hashbrown 0.15.2",
|
||||
"rayon",
|
||||
"serde",
|
||||
]
|
||||
@ -2931,6 +2931,7 @@ dependencies = [
|
||||
"qcms",
|
||||
"rayon",
|
||||
"regex",
|
||||
"regex-syntax",
|
||||
"roxmltree",
|
||||
"rust_decimal",
|
||||
"rustybuzz",
|
||||
|
@ -94,6 +94,7 @@ qcms = "0.3.0"
|
||||
quote = "1"
|
||||
rayon = "1.7.0"
|
||||
regex = "1"
|
||||
regex-syntax = "0.8"
|
||||
resvg = { version = "0.43", default-features = false, features = ["raster-images"] }
|
||||
roxmltree = "0.20"
|
||||
rust_decimal = { version = "1.36.0", default-features = false, features = ["maths"] }
|
||||
|
@ -17,7 +17,7 @@ use typst::html::HtmlDocument;
|
||||
use typst::layout::{Frame, Page, PageRanges, PagedDocument};
|
||||
use typst::syntax::{FileId, Source, Span};
|
||||
use typst::WorldExt;
|
||||
use typst_pdf::{PdfOptions, PdfStandards};
|
||||
use typst_pdf::{PdfOptions, PdfStandards, Timestamp};
|
||||
|
||||
use crate::args::{
|
||||
CompileArgs, CompileCommand, DiagnosticFormat, Input, Output, OutputFormat,
|
||||
@ -55,7 +55,7 @@ pub struct CompileConfig {
|
||||
pub output_format: OutputFormat,
|
||||
/// Which pages to export.
|
||||
pub pages: Option<PageRanges>,
|
||||
/// The document's creation date formatted as a UNIX timestamp.
|
||||
/// The document's creation date formatted as a UNIX timestamp, with UTC suffix.
|
||||
pub creation_timestamp: Option<DateTime<Utc>>,
|
||||
/// The format to emit diagnostics in.
|
||||
pub diagnostic_format: DiagnosticFormat,
|
||||
@ -271,11 +271,23 @@ fn export_paged(document: &PagedDocument, config: &CompileConfig) -> SourceResul
|
||||
|
||||
/// Export to a PDF.
|
||||
fn export_pdf(document: &PagedDocument, config: &CompileConfig) -> SourceResult<()> {
|
||||
// If the timestamp is provided through the CLI, use UTC suffix,
|
||||
// else, use the current local time and timezone.
|
||||
let timestamp = match config.creation_timestamp {
|
||||
Some(timestamp) => convert_datetime(timestamp).map(Timestamp::new_utc),
|
||||
None => {
|
||||
let local_datetime = chrono::Local::now();
|
||||
convert_datetime(local_datetime).and_then(|datetime| {
|
||||
Timestamp::new_local(
|
||||
datetime,
|
||||
local_datetime.offset().local_minus_utc() / 60,
|
||||
)
|
||||
})
|
||||
}
|
||||
};
|
||||
let options = PdfOptions {
|
||||
ident: Smart::Auto,
|
||||
timestamp: convert_datetime(
|
||||
config.creation_timestamp.unwrap_or_else(chrono::Utc::now),
|
||||
),
|
||||
timestamp,
|
||||
page_ranges: config.pages.clone(),
|
||||
standards: config.pdf_standards.clone(),
|
||||
};
|
||||
@ -289,7 +301,9 @@ fn export_pdf(document: &PagedDocument, config: &CompileConfig) -> SourceResult<
|
||||
}
|
||||
|
||||
/// Convert [`chrono::DateTime`] to [`Datetime`]
|
||||
fn convert_datetime(date_time: chrono::DateTime<chrono::Utc>) -> Option<Datetime> {
|
||||
fn convert_datetime<Tz: chrono::TimeZone>(
|
||||
date_time: chrono::DateTime<Tz>,
|
||||
) -> Option<Datetime> {
|
||||
Datetime::from_ymd_hms(
|
||||
date_time.year(),
|
||||
date_time.month().try_into().ok()?,
|
||||
|
@ -593,14 +593,8 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[track_caller]
|
||||
fn test(text: &str, result: &[&str]) {
|
||||
let mut scopes = Scopes::new(None);
|
||||
scopes.top.define("f", 0);
|
||||
scopes.top.define("x", 0);
|
||||
scopes.top.define("y", 0);
|
||||
scopes.top.define("z", 0);
|
||||
|
||||
let mut visitor = CapturesVisitor::new(Some(&scopes), Capturer::Function);
|
||||
fn test(scopes: &Scopes, text: &str, result: &[&str]) {
|
||||
let mut visitor = CapturesVisitor::new(Some(scopes), Capturer::Function);
|
||||
let root = parse(text);
|
||||
visitor.visit(&root);
|
||||
|
||||
@ -613,44 +607,95 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_captures() {
|
||||
let mut scopes = Scopes::new(None);
|
||||
scopes.top.define("f", 0);
|
||||
scopes.top.define("x", 0);
|
||||
scopes.top.define("y", 0);
|
||||
scopes.top.define("z", 0);
|
||||
let s = &scopes;
|
||||
|
||||
// Let binding and function definition.
|
||||
test("#let x = x", &["x"]);
|
||||
test("#let x; #(x + y)", &["y"]);
|
||||
test("#let f(x, y) = x + y", &[]);
|
||||
test("#let f(x, y) = f", &[]);
|
||||
test("#let f = (x, y) => f", &["f"]);
|
||||
test(s, "#let x = x", &["x"]);
|
||||
test(s, "#let x; #(x + y)", &["y"]);
|
||||
test(s, "#let f(x, y) = x + y", &[]);
|
||||
test(s, "#let f(x, y) = f", &[]);
|
||||
test(s, "#let f = (x, y) => f", &["f"]);
|
||||
|
||||
// Closure with different kinds of params.
|
||||
test("#((x, y) => x + z)", &["z"]);
|
||||
test("#((x: y, z) => x + z)", &["y"]);
|
||||
test("#((..x) => x + y)", &["y"]);
|
||||
test("#((x, y: x + z) => x + y)", &["x", "z"]);
|
||||
test("#{x => x; x}", &["x"]);
|
||||
test(s, "#((x, y) => x + z)", &["z"]);
|
||||
test(s, "#((x: y, z) => x + z)", &["y"]);
|
||||
test(s, "#((..x) => x + y)", &["y"]);
|
||||
test(s, "#((x, y: x + z) => x + y)", &["x", "z"]);
|
||||
test(s, "#{x => x; x}", &["x"]);
|
||||
|
||||
// Show rule.
|
||||
test("#show y: x => x", &["y"]);
|
||||
test("#show y: x => x + z", &["y", "z"]);
|
||||
test("#show x: x => x", &["x"]);
|
||||
test(s, "#show y: x => x", &["y"]);
|
||||
test(s, "#show y: x => x + z", &["y", "z"]);
|
||||
test(s, "#show x: x => x", &["x"]);
|
||||
|
||||
// For loop.
|
||||
test("#for x in y { x + z }", &["y", "z"]);
|
||||
test("#for (x, y) in y { x + y }", &["y"]);
|
||||
test("#for x in y {} #x", &["x", "y"]);
|
||||
test(s, "#for x in y { x + z }", &["y", "z"]);
|
||||
test(s, "#for (x, y) in y { x + y }", &["y"]);
|
||||
test(s, "#for x in y {} #x", &["x", "y"]);
|
||||
|
||||
// Import.
|
||||
test("#import z: x, y", &["z"]);
|
||||
test("#import x + y: x, y, z", &["x", "y"]);
|
||||
test(s, "#import z: x, y", &["z"]);
|
||||
test(s, "#import x + y: x, y, z", &["x", "y"]);
|
||||
|
||||
// Blocks.
|
||||
test("#{ let x = 1; { let y = 2; y }; x + y }", &["y"]);
|
||||
test("#[#let x = 1]#x", &["x"]);
|
||||
test(s, "#{ let x = 1; { let y = 2; y }; x + y }", &["y"]);
|
||||
test(s, "#[#let x = 1]#x", &["x"]);
|
||||
|
||||
// Field access.
|
||||
test("#foo(body: 1)", &[]);
|
||||
test("#(body: 1)", &[]);
|
||||
test("#(body = 1)", &[]);
|
||||
test("#(body += y)", &["y"]);
|
||||
test("#{ (body, a) = (y, 1) }", &["y"]);
|
||||
test("#(x.at(y) = 5)", &["x", "y"])
|
||||
test(s, "#x.y.f(z)", &["x", "z"]);
|
||||
|
||||
// Parenthesized expressions.
|
||||
test(s, "#f(x: 1)", &["f"]);
|
||||
test(s, "#(x: 1)", &[]);
|
||||
test(s, "#(x = 1)", &["x"]);
|
||||
test(s, "#(x += y)", &["x", "y"]);
|
||||
test(s, "#{ (x, z) = (y, 1) }", &["x", "y", "z"]);
|
||||
test(s, "#(x.at(y) = 5)", &["x", "y"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_captures_in_math() {
|
||||
let mut scopes = Scopes::new(None);
|
||||
scopes.top.define("f", 0);
|
||||
scopes.top.define("x", 0);
|
||||
scopes.top.define("y", 0);
|
||||
scopes.top.define("z", 0);
|
||||
// Multi-letter variables are required for math.
|
||||
scopes.top.define("foo", 0);
|
||||
scopes.top.define("bar", 0);
|
||||
scopes.top.define("x-bar", 0);
|
||||
scopes.top.define("x_bar", 0);
|
||||
let s = &scopes;
|
||||
|
||||
// Basic math identifier differences.
|
||||
test(s, "$ x f(z) $", &[]); // single letters not captured.
|
||||
test(s, "$ #x #f(z) $", &["f", "x", "z"]);
|
||||
test(s, "$ foo f(bar) $", &["bar", "foo"]);
|
||||
test(s, "$ #foo[#$bar$] $", &["bar", "foo"]);
|
||||
test(s, "$ #let foo = x; foo $", &["x"]);
|
||||
|
||||
// Math idents don't have dashes/underscores
|
||||
test(s, "$ x-y x_y foo-x x_bar $", &["bar", "foo"]);
|
||||
test(s, "$ #x-bar #x_bar $", &["x-bar", "x_bar"]);
|
||||
|
||||
// Named-params.
|
||||
test(s, "$ foo(bar: y) $", &["foo"]);
|
||||
// This should be updated when we improve named-param parsing:
|
||||
test(s, "$ foo(x-y: 1, bar-z: 2) $", &["bar", "foo"]);
|
||||
|
||||
// Field access in math.
|
||||
test(s, "$ foo.bar $", &["foo"]);
|
||||
test(s, "$ foo.x $", &["foo"]);
|
||||
test(s, "$ x.foo $", &["foo"]);
|
||||
test(s, "$ foo . bar $", &["bar", "foo"]);
|
||||
test(s, "$ foo.x.y.bar(z) $", &["foo"]);
|
||||
test(s, "$ foo.x-bar $", &["bar", "foo"]);
|
||||
test(s, "$ foo.x_bar $", &["bar", "foo"]);
|
||||
test(s, "$ #x_bar.x-bar $", &["x_bar"]);
|
||||
}
|
||||
}
|
||||
|
@ -8,14 +8,30 @@ use typst_syntax::Span;
|
||||
|
||||
/// Encodes an HTML document into a string.
|
||||
pub fn html(document: &HtmlDocument) -> SourceResult<String> {
|
||||
let mut w = Writer { buf: String::new() };
|
||||
let mut w = Writer { pretty: true, ..Writer::default() };
|
||||
w.buf.push_str("<!DOCTYPE html>");
|
||||
write_indent(&mut w);
|
||||
write_element(&mut w, &document.root)?;
|
||||
Ok(w.buf)
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Writer {
|
||||
buf: String,
|
||||
/// current indentation level
|
||||
level: usize,
|
||||
/// pretty printing enabled?
|
||||
pretty: bool,
|
||||
}
|
||||
|
||||
/// Write a newline and indent, if pretty printing is enabled.
|
||||
fn write_indent(w: &mut Writer) {
|
||||
if w.pretty {
|
||||
w.buf.push('\n');
|
||||
for _ in 0..w.level {
|
||||
w.buf.push_str(" ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode an HTML node into the writer.
|
||||
@ -67,9 +83,30 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for node in &element.children {
|
||||
write_node(w, node)?;
|
||||
let pretty = w.pretty;
|
||||
if !element.children.is_empty() {
|
||||
w.pretty &= is_pretty(element);
|
||||
let mut indent = w.pretty;
|
||||
|
||||
w.level += 1;
|
||||
for c in &element.children {
|
||||
let pretty_child = match c {
|
||||
HtmlNode::Tag(_) => continue,
|
||||
HtmlNode::Element(element) => is_pretty(element),
|
||||
HtmlNode::Text(..) | HtmlNode::Frame(_) => false,
|
||||
};
|
||||
|
||||
if core::mem::take(&mut indent) || pretty_child {
|
||||
write_indent(w);
|
||||
}
|
||||
write_node(w, c)?;
|
||||
indent = pretty_child;
|
||||
}
|
||||
w.level -= 1;
|
||||
|
||||
write_indent(w)
|
||||
}
|
||||
w.pretty = pretty;
|
||||
|
||||
w.buf.push_str("</");
|
||||
w.buf.push_str(&element.tag.resolve());
|
||||
@ -78,6 +115,11 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether the element should be pretty-printed.
|
||||
fn is_pretty(element: &HtmlElement) -> bool {
|
||||
tag::is_block_by_default(element.tag) || matches!(element.tag, tag::meta)
|
||||
}
|
||||
|
||||
/// Escape a character.
|
||||
fn write_escape(w: &mut Writer, c: char) -> StrResult<()> {
|
||||
// See <https://html.spec.whatwg.org/multipage/syntax.html#syntax-charref>
|
||||
|
@ -123,6 +123,36 @@ pub fn named_items<T>(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(v) = parent.cast::<ast::Closure>().filter(|v| {
|
||||
// Check if the node is in the body of the closure.
|
||||
let body = parent.find(v.body().span());
|
||||
body.is_some_and(|n| n.find(node.span()).is_some())
|
||||
}) {
|
||||
for param in v.params().children() {
|
||||
match param {
|
||||
ast::Param::Pos(pattern) => {
|
||||
for ident in pattern.bindings() {
|
||||
if let Some(t) = recv(NamedItem::Var(ident)) {
|
||||
return Some(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
ast::Param::Named(n) => {
|
||||
if let Some(t) = recv(NamedItem::Var(n.name())) {
|
||||
return Some(t);
|
||||
}
|
||||
}
|
||||
ast::Param::Spread(s) => {
|
||||
if let Some(sink_ident) = s.sink_ident() {
|
||||
if let Some(t) = recv(NamedItem::Var(sink_ident)) {
|
||||
return Some(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ancestor = Some(parent.clone());
|
||||
continue;
|
||||
}
|
||||
@ -269,6 +299,17 @@ mod tests {
|
||||
assert!(!has_named_items(r#"#let a = 1;#let b = 2;"#, 8, "b"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_param_named_items() {
|
||||
// Has named items
|
||||
assert!(has_named_items(r#"#let f(a) = 1;#let b = 2;"#, 12, "a"));
|
||||
assert!(has_named_items(r#"#let f(a: b) = 1;#let b = 2;"#, 15, "a"));
|
||||
|
||||
// Doesn't have named items
|
||||
assert!(!has_named_items(r#"#let f(a) = 1;#let b = 2;"#, 19, "a"));
|
||||
assert!(!has_named_items(r#"#let f(a: b) = 1;#let b = 2;"#, 15, "b"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_import_named_items() {
|
||||
// Cannot test much.
|
||||
|
@ -337,6 +337,21 @@ mod tests {
|
||||
fn test_tooltip_closure() {
|
||||
test("#let f(x) = x + y", 11, Side::Before)
|
||||
.must_be_text("This closure captures `y`");
|
||||
// Same tooltip if `y` is defined first.
|
||||
test("#let y = 10; #let f(x) = x + y", 24, Side::Before)
|
||||
.must_be_text("This closure captures `y`");
|
||||
// Names are sorted.
|
||||
test("#let f(x) = x + y + z + a", 11, Side::Before)
|
||||
.must_be_text("This closure captures `a`, `y`, and `z`");
|
||||
// Names are de-duplicated.
|
||||
test("#let f(x) = x + y + z + y", 11, Side::Before)
|
||||
.must_be_text("This closure captures `y` and `z`");
|
||||
// With arrow syntax.
|
||||
test("#let f = (x) => x + y", 15, Side::Before)
|
||||
.must_be_text("This closure captures `y`");
|
||||
// No recursion with arrow syntax.
|
||||
test("#let f = (x) => x + y + f", 13, Side::After)
|
||||
.must_be_text("This closure captures `f` and `y`");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -214,6 +214,13 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
|
||||
}
|
||||
|
||||
/// Lay out the inner contents of a column.
|
||||
///
|
||||
/// Pending floats and footnotes are also laid out at this step. For those,
|
||||
/// however, we forbid footnote migration (moving the frame containing the
|
||||
/// footnote reference if the corresponding entry doesn't fit), allowing
|
||||
/// the footnote invariant to be broken, as it would require handling a
|
||||
/// [`Stop::Finish`] at this point, but that is exclusively handled by the
|
||||
/// distributor.
|
||||
fn column_contents(&mut self, regions: Regions) -> FlowResult<Frame> {
|
||||
// Process pending footnotes.
|
||||
for note in std::mem::take(&mut self.work.footnotes) {
|
||||
@ -222,7 +229,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
|
||||
|
||||
// Process pending floats.
|
||||
for placed in std::mem::take(&mut self.work.floats) {
|
||||
self.float(placed, ®ions, false)?;
|
||||
self.float(placed, ®ions, false, false)?;
|
||||
}
|
||||
|
||||
distribute(self, regions)
|
||||
@ -236,13 +243,21 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
|
||||
/// (depending on `placed.scope`).
|
||||
///
|
||||
/// When the float does not fit, it is queued into `work.floats`. The
|
||||
/// value of `clearance` that between the float and flow content is needed
|
||||
/// --- it is set if there are already distributed items.
|
||||
/// value of `clearance` indicates that between the float and flow content
|
||||
/// is needed --- it is set if there are already distributed items.
|
||||
///
|
||||
/// The value of `migratable` determines whether footnotes within the float
|
||||
/// should be allowed to prompt its migration if they don't fit in order to
|
||||
/// respect the footnote invariant (entries in the same page as the
|
||||
/// references), triggering [`Stop::Finish`]. This is usually `true` within
|
||||
/// the distributor, as it can handle that particular flow event, and
|
||||
/// `false` elsewhere.
|
||||
pub fn float(
|
||||
&mut self,
|
||||
placed: &'b PlacedChild<'a>,
|
||||
regions: &Regions,
|
||||
clearance: bool,
|
||||
migratable: bool,
|
||||
) -> FlowResult<()> {
|
||||
// If the float is already processed, skip it.
|
||||
let loc = placed.location();
|
||||
@ -291,7 +306,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
|
||||
}
|
||||
|
||||
// Handle footnotes in the float.
|
||||
self.footnotes(regions, &frame, need, false)?;
|
||||
self.footnotes(regions, &frame, need, false, migratable)?;
|
||||
|
||||
// Determine the float's vertical alignment. We can unwrap the inner
|
||||
// `Option` because `Custom(None)` is checked for during collection.
|
||||
@ -326,12 +341,19 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
|
||||
/// Lays out footnotes in the `frame` if this is the root flow and there are
|
||||
/// any. The value of `breakable` indicates whether the element that
|
||||
/// produced the frame is breakable. If not, the frame is treated as atomic.
|
||||
///
|
||||
/// The value of `migratable` indicates whether footnote migration should be
|
||||
/// possible (at least for the first footnote found in the frame, as it is
|
||||
/// forbidden for the second footnote onwards). It is usually `true` within
|
||||
/// the distributor and `false` elsewhere, as the distributor can handle
|
||||
/// [`Stop::Finish`] which is returned when migration is requested.
|
||||
pub fn footnotes(
|
||||
&mut self,
|
||||
regions: &Regions,
|
||||
frame: &Frame,
|
||||
flow_need: Abs,
|
||||
breakable: bool,
|
||||
migratable: bool,
|
||||
) -> FlowResult<()> {
|
||||
// Footnotes are only supported at the root level.
|
||||
if !self.config.root {
|
||||
@ -352,7 +374,7 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
|
||||
|
||||
let mut relayout = false;
|
||||
let mut regions = *regions;
|
||||
let mut migratable = !breakable && regions.may_progress();
|
||||
let mut migratable = migratable && !breakable && regions.may_progress();
|
||||
|
||||
for (y, elem) in notes {
|
||||
// The amount of space used by the in-flow content that contains the
|
||||
@ -470,7 +492,20 @@ impl<'a, 'b> Composer<'a, 'b, '_, '_> {
|
||||
|
||||
// Lay out nested footnotes.
|
||||
for (_, note) in nested {
|
||||
self.footnote(note, regions, flow_need, migratable)?;
|
||||
match self.footnote(note, regions, flow_need, migratable) {
|
||||
// This footnote was already processed or queued.
|
||||
Ok(_) => {}
|
||||
// Footnotes always request a relayout when processed for the
|
||||
// first time, so we ignore a relayout request since we're
|
||||
// about to do so afterwards. Without this check, the first
|
||||
// inner footnote interrupts processing of the following ones.
|
||||
Err(Stop::Relayout(_)) => {}
|
||||
// Either of
|
||||
// - A `Stop::Finish` indicating that the frame's origin element
|
||||
// should migrate to uphold the footnote invariant.
|
||||
// - A fatal error.
|
||||
err => return err,
|
||||
}
|
||||
}
|
||||
|
||||
// Since we laid out a footnote, we need a relayout.
|
||||
|
@ -17,7 +17,7 @@ pub fn distribute(composer: &mut Composer, regions: Regions) -> FlowResult<Frame
|
||||
regions,
|
||||
items: vec![],
|
||||
sticky: None,
|
||||
stickable: false,
|
||||
stickable: None,
|
||||
};
|
||||
let init = distributor.snapshot();
|
||||
let forced = match distributor.run() {
|
||||
@ -42,9 +42,26 @@ struct Distributor<'a, 'b, 'x, 'y, 'z> {
|
||||
/// A snapshot which can be restored to migrate a suffix of sticky blocks to
|
||||
/// the next region.
|
||||
sticky: Option<DistributionSnapshot<'a, 'b>>,
|
||||
/// Whether there was at least one proper block. Otherwise, sticky blocks
|
||||
/// are disabled (or else they'd keep being migrated).
|
||||
stickable: bool,
|
||||
/// Whether the current group of consecutive sticky blocks are still sticky
|
||||
/// and may migrate with the attached frame. This is `None` while we aren't
|
||||
/// processing sticky blocks. On the first sticky block, this will become
|
||||
/// `Some(true)` if migrating sticky blocks as usual would make a
|
||||
/// difference - this is given by `regions.may_progress()`. Otherwise, it
|
||||
/// is set to `Some(false)`, which is usually the case when the first
|
||||
/// sticky block in the group is at the very top of the page (then,
|
||||
/// migrating it would just lead us back to the top of the page, leading
|
||||
/// to an infinite loop). In that case, all sticky blocks of the group are
|
||||
/// also disabled, until this is reset to `None` on the first non-sticky
|
||||
/// frame we find.
|
||||
///
|
||||
/// While this behavior of disabling stickiness of sticky blocks at the
|
||||
/// very top of the page may seem non-ideal, it is only problematic (that
|
||||
/// is, may lead to orphaned sticky blocks / headings) if the combination
|
||||
/// of 'sticky blocks + attached frame' doesn't fit in one page, in which
|
||||
/// case there is nothing Typst can do to improve the situation, as sticky
|
||||
/// blocks are supposed to always be in the same page as the subsequent
|
||||
/// frame, but that is impossible in that case, which is thus pathological.
|
||||
stickable: Option<bool>,
|
||||
}
|
||||
|
||||
/// A snapshot of the distribution state.
|
||||
@ -240,7 +257,8 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> {
|
||||
|
||||
// Handle fractionally sized blocks.
|
||||
if let Some(fr) = single.fr {
|
||||
self.composer.footnotes(&self.regions, &frame, Abs::zero(), false)?;
|
||||
self.composer
|
||||
.footnotes(&self.regions, &frame, Abs::zero(), false, true)?;
|
||||
self.flush_tags();
|
||||
self.items.push(Item::Fr(fr, Some(single)));
|
||||
return Ok(());
|
||||
@ -313,18 +331,41 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> {
|
||||
// If the frame is sticky and we haven't remembered a preceding
|
||||
// sticky element, make a checkpoint which we can restore should we
|
||||
// end on this sticky element.
|
||||
if self.stickable && self.sticky.is_none() {
|
||||
//
|
||||
// The first sticky block within consecutive sticky blocks
|
||||
// determines whether this group of sticky blocks has stickiness
|
||||
// disabled or not.
|
||||
//
|
||||
// The criteria used here is: if migrating this group of sticky
|
||||
// blocks together with the "attached" block can't improve the lack
|
||||
// of space, since we're at the start of the region, then we don't
|
||||
// do so, and stickiness is disabled (at least, for this region).
|
||||
// Otherwise, migration is allowed.
|
||||
//
|
||||
// Note that, since the whole region is checked, this ensures sticky
|
||||
// blocks at the top of a block - but not necessarily of the page -
|
||||
// can still be migrated.
|
||||
if self.sticky.is_none()
|
||||
&& *self.stickable.get_or_insert_with(|| self.regions.may_progress())
|
||||
{
|
||||
self.sticky = Some(self.snapshot());
|
||||
}
|
||||
} else if !frame.is_empty() {
|
||||
// If the frame isn't sticky, we can forget a previous snapshot.
|
||||
self.stickable = true;
|
||||
// If the frame isn't sticky, we can forget a previous snapshot. We
|
||||
// interrupt a group of sticky blocks, if there was one, so we reset
|
||||
// the saved stickable check for the next group of sticky blocks.
|
||||
self.sticky = None;
|
||||
self.stickable = None;
|
||||
}
|
||||
|
||||
// Handle footnotes.
|
||||
self.composer
|
||||
.footnotes(&self.regions, &frame, frame.height(), breakable)?;
|
||||
self.composer.footnotes(
|
||||
&self.regions,
|
||||
&frame,
|
||||
frame.height(),
|
||||
breakable,
|
||||
true,
|
||||
)?;
|
||||
|
||||
// Push an item for the frame.
|
||||
self.regions.size.y -= frame.height();
|
||||
@ -347,11 +388,13 @@ impl<'a, 'b> Distributor<'a, 'b, '_, '_, '_> {
|
||||
placed,
|
||||
&self.regions,
|
||||
self.items.iter().any(|item| matches!(item, Item::Frame(..))),
|
||||
true,
|
||||
)?;
|
||||
self.regions.size.y -= weak_spacing;
|
||||
} else {
|
||||
let frame = placed.layout(self.composer.engine, self.regions.base())?;
|
||||
self.composer.footnotes(&self.regions, &frame, Abs::zero(), true)?;
|
||||
self.composer
|
||||
.footnotes(&self.regions, &frame, Abs::zero(), true, true)?;
|
||||
self.flush_tags();
|
||||
self.items.push(Item::Placed(frame, placed));
|
||||
}
|
||||
|
@ -54,7 +54,8 @@ pub fn layout_image(
|
||||
format,
|
||||
elem.alt(styles),
|
||||
engine.world,
|
||||
&families(styles).collect::<Vec<_>>(),
|
||||
&families(styles).map(|f| f.as_str()).collect::<Vec<_>>(),
|
||||
elem.flatten_text(styles),
|
||||
)
|
||||
.at(span)?;
|
||||
|
||||
|
@ -256,8 +256,7 @@ impl<'a> Collector<'a> {
|
||||
}
|
||||
|
||||
fn push_text(&mut self, text: &str, styles: StyleChain<'a>) {
|
||||
self.full.push_str(text);
|
||||
self.push_segment(Segment::Text(text.len(), styles));
|
||||
self.build_text(styles, |full| full.push_str(text));
|
||||
}
|
||||
|
||||
fn build_text<F>(&mut self, styles: StyleChain<'a>, f: F)
|
||||
@ -266,33 +265,33 @@ impl<'a> Collector<'a> {
|
||||
{
|
||||
let prev = self.full.len();
|
||||
f(&mut self.full);
|
||||
let len = self.full.len() - prev;
|
||||
self.push_segment(Segment::Text(len, styles));
|
||||
let segment_len = self.full.len() - prev;
|
||||
|
||||
// Merge adjacent text segments with the same styles.
|
||||
if let Some(Segment::Text(last_len, last_styles)) = self.segments.last_mut() {
|
||||
if *last_styles == styles {
|
||||
*last_len += segment_len;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.segments.push(Segment::Text(segment_len, styles));
|
||||
}
|
||||
|
||||
fn push_item(&mut self, item: Item<'a>) {
|
||||
self.full.push_str(item.textual());
|
||||
self.push_segment(Segment::Item(item));
|
||||
}
|
||||
|
||||
fn push_segment(&mut self, segment: Segment<'a>) {
|
||||
match (self.segments.last_mut(), &segment) {
|
||||
// Merge adjacent text segments with the same styles.
|
||||
(Some(Segment::Text(last_len, last_styles)), Segment::Text(len, styles))
|
||||
if *last_styles == *styles =>
|
||||
{
|
||||
*last_len += *len;
|
||||
}
|
||||
|
||||
match (self.segments.last_mut(), &item) {
|
||||
// Merge adjacent weak spacing by taking the maximum.
|
||||
(
|
||||
Some(Segment::Item(Item::Absolute(prev_amount, true))),
|
||||
Segment::Item(Item::Absolute(amount, true)),
|
||||
Item::Absolute(amount, true),
|
||||
) => {
|
||||
*prev_amount = (*prev_amount).max(*amount);
|
||||
}
|
||||
|
||||
_ => self.segments.push(segment),
|
||||
_ => {
|
||||
self.full.push_str(item.textual());
|
||||
self.segments.push(Segment::Item(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -971,11 +971,13 @@ where
|
||||
}
|
||||
|
||||
/// Estimates the metrics for the line spanned by the range.
|
||||
#[track_caller]
|
||||
fn estimate(&self, range: Range) -> T {
|
||||
self.get(range.end) - self.get(range.start)
|
||||
}
|
||||
|
||||
/// Get the metric at the given byte position.
|
||||
#[track_caller]
|
||||
fn get(&self, index: usize) -> T {
|
||||
match index.checked_sub(1) {
|
||||
None => T::default(),
|
||||
|
@ -11,8 +11,8 @@ use typst_library::engine::Engine;
|
||||
use typst_library::foundations::{Smart, StyleChain};
|
||||
use typst_library::layout::{Abs, Dir, Em, Frame, FrameItem, Point, Size};
|
||||
use typst_library::text::{
|
||||
families, features, is_default_ignorable, variant, Font, FontVariant, Glyph, Lang,
|
||||
Region, TextEdgeBounds, TextElem, TextItem,
|
||||
families, features, is_default_ignorable, variant, Font, FontFamily, FontVariant,
|
||||
Glyph, Lang, Region, TextEdgeBounds, TextElem, TextItem,
|
||||
};
|
||||
use typst_library::World;
|
||||
use typst_utils::SliceExt;
|
||||
@ -351,7 +351,7 @@ impl<'a> ShapedText<'a> {
|
||||
for family in families(self.styles) {
|
||||
if let Some(font) = world
|
||||
.book()
|
||||
.select(family, self.variant)
|
||||
.select(family.as_str(), self.variant)
|
||||
.and_then(|id| world.font(id))
|
||||
{
|
||||
expand(&font, TextEdgeBounds::Zero);
|
||||
@ -463,7 +463,8 @@ impl<'a> ShapedText<'a> {
|
||||
None
|
||||
};
|
||||
let mut chain = families(self.styles)
|
||||
.map(|family| book.select(family, self.variant))
|
||||
.filter(|family| family.covers().map_or(true, |c| c.is_match("-")))
|
||||
.map(|family| book.select(family.as_str(), self.variant))
|
||||
.chain(fallback_func.iter().map(|f| f()))
|
||||
.flatten();
|
||||
|
||||
@ -719,7 +720,7 @@ fn shape_segment<'a>(
|
||||
ctx: &mut ShapingContext,
|
||||
base: usize,
|
||||
text: &str,
|
||||
mut families: impl Iterator<Item = &'a str> + Clone,
|
||||
mut families: impl Iterator<Item = &'a FontFamily> + Clone,
|
||||
) {
|
||||
// Don't try shaping newlines, tabs, or default ignorables.
|
||||
if text
|
||||
@ -732,11 +733,18 @@ fn shape_segment<'a>(
|
||||
// Find the next available family.
|
||||
let world = ctx.engine.world;
|
||||
let book = world.book();
|
||||
let mut selection = families.find_map(|family| {
|
||||
book.select(family, ctx.variant)
|
||||
let mut selection = None;
|
||||
let mut covers = None;
|
||||
for family in families.by_ref() {
|
||||
selection = book
|
||||
.select(family.as_str(), ctx.variant)
|
||||
.and_then(|id| world.font(id))
|
||||
.filter(|font| !ctx.used.contains(font))
|
||||
});
|
||||
.filter(|font| !ctx.used.contains(font));
|
||||
if selection.is_some() {
|
||||
covers = family.covers();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Do font fallback if the families are exhausted and fallback is enabled.
|
||||
if selection.is_none() && ctx.fallback {
|
||||
@ -795,6 +803,16 @@ fn shape_segment<'a>(
|
||||
let pos = buffer.glyph_positions();
|
||||
let ltr = ctx.dir.is_positive();
|
||||
|
||||
// Whether the character at the given offset is covered by the coverage.
|
||||
let is_covered = |offset| {
|
||||
let end = text[offset..]
|
||||
.char_indices()
|
||||
.nth(1)
|
||||
.map(|(i, _)| offset + i)
|
||||
.unwrap_or(text.len());
|
||||
covers.map_or(true, |cov| cov.is_match(&text[offset..end]))
|
||||
};
|
||||
|
||||
// Collect the shaped glyphs, doing fallback and shaping parts again with
|
||||
// the next font if necessary.
|
||||
let mut i = 0;
|
||||
@ -803,7 +821,7 @@ fn shape_segment<'a>(
|
||||
let cluster = info.cluster as usize;
|
||||
|
||||
// Add the glyph to the shaped output.
|
||||
if info.glyph_id != 0 {
|
||||
if info.glyph_id != 0 && is_covered(cluster) {
|
||||
// Determine the text range of the glyph.
|
||||
let start = base + cluster;
|
||||
let end = base
|
||||
@ -836,7 +854,9 @@ fn shape_segment<'a>(
|
||||
} else {
|
||||
// First, search for the end of the tofu sequence.
|
||||
let k = i;
|
||||
while infos.get(i + 1).is_some_and(|info| info.glyph_id == 0) {
|
||||
while infos.get(i + 1).is_some_and(|info| {
|
||||
info.glyph_id == 0 || !is_covered(info.cluster as usize)
|
||||
}) {
|
||||
i += 1;
|
||||
}
|
||||
|
||||
|
@ -74,6 +74,7 @@ pub fn layout_enum(
|
||||
regions: Regions,
|
||||
) -> SourceResult<Fragment> {
|
||||
let numbering = elem.numbering(styles);
|
||||
let reversed = elem.reversed(styles);
|
||||
let indent = elem.indent(styles);
|
||||
let body_indent = elem.body_indent(styles);
|
||||
let gutter = elem.spacing(styles).unwrap_or_else(|| {
|
||||
@ -86,7 +87,9 @@ pub fn layout_enum(
|
||||
|
||||
let mut cells = vec![];
|
||||
let mut locator = locator.split();
|
||||
let mut number = elem.start(styles);
|
||||
let mut number =
|
||||
elem.start(styles)
|
||||
.unwrap_or_else(|| if reversed { elem.children.len() } else { 1 });
|
||||
let mut parents = EnumElem::parents_in(styles);
|
||||
|
||||
let full = elem.full(styles);
|
||||
@ -127,7 +130,8 @@ pub fn layout_enum(
|
||||
item.body.clone().styled(EnumElem::set_parents(smallvec![number])),
|
||||
locator.next(&item.body.span()),
|
||||
));
|
||||
number = number.saturating_add(1);
|
||||
number =
|
||||
if reversed { number.saturating_sub(1) } else { number.saturating_add(1) };
|
||||
}
|
||||
|
||||
let grid = CellGrid::new(
|
||||
|
@ -127,7 +127,9 @@ fn layout_vec_body(
|
||||
let denom_style = style_for_denominator(styles);
|
||||
let mut flat = vec![];
|
||||
for child in column {
|
||||
flat.push(ctx.layout_into_run(child, styles.chain(&denom_style))?);
|
||||
// We allow linebreaks in cases and vectors, which are functionally
|
||||
// identical to commas.
|
||||
flat.extend(ctx.layout_into_run(child, styles.chain(&denom_style))?.rows());
|
||||
}
|
||||
// We pad ascent and descent with the ascent and descent of the paren
|
||||
// to ensure that normal vectors are aligned with others unless they are
|
||||
|
@ -237,7 +237,7 @@ fn find_math_font(
|
||||
let variant = variant(styles);
|
||||
let world = engine.world;
|
||||
let Some(font) = families(styles).find_map(|family| {
|
||||
let id = world.book().select(family, variant)?;
|
||||
let id = world.book().select(family.as_str(), variant)?;
|
||||
let font = world.font(id)?;
|
||||
let _ = font.ttf().tables().math?.constants?;
|
||||
Some(font)
|
||||
|
@ -121,7 +121,6 @@ pub fn stack(
|
||||
alternator: LeftRightAlternator,
|
||||
minimum_ascent_descent: Option<(Abs, Abs)>,
|
||||
) -> Frame {
|
||||
let rows: Vec<_> = rows.into_iter().flat_map(|r| r.rows()).collect();
|
||||
let AlignmentResult { points, width } = alignments(&rows);
|
||||
let rows: Vec<_> = rows
|
||||
.into_iter()
|
||||
|
@ -297,7 +297,7 @@ fn layout_underoverspreader(
|
||||
if let Some(annotation) = annotation {
|
||||
let under_style = style_for_subscript(styles);
|
||||
let annotation_styles = styles.chain(&under_style);
|
||||
rows.push(ctx.layout_into_run(annotation, annotation_styles)?);
|
||||
rows.extend(ctx.layout_into_run(annotation, annotation_styles)?.rows());
|
||||
}
|
||||
0
|
||||
}
|
||||
@ -305,7 +305,7 @@ fn layout_underoverspreader(
|
||||
if let Some(annotation) = annotation {
|
||||
let over_style = style_for_superscript(styles);
|
||||
let annotation_styles = styles.chain(&over_style);
|
||||
rows.push(ctx.layout_into_run(annotation, annotation_styles)?);
|
||||
rows.extend(ctx.layout_into_run(annotation, annotation_styles)?.rows());
|
||||
}
|
||||
rows.push(stretched.into());
|
||||
rows.push(MathRun::new(vec![body]));
|
||||
|
@ -53,7 +53,7 @@ pub fn collect<'a>(
|
||||
|
||||
// The initial styles for the next page are ours unless this is a
|
||||
// "boundary" pagebreak. Such a pagebreak is generated at the end of
|
||||
// the scope of a page set rule to ensure a page boundary. It's
|
||||
// the scope of a page set rule to ensure a page boundary. Its
|
||||
// styles correspond to the styles _before_ the page set rule, so we
|
||||
// don't want to apply it to a potential empty page.
|
||||
if !pagebreak.boundary(styles) {
|
||||
|
@ -33,8 +33,17 @@ pub fn layout_repeat(
|
||||
let fill = region.size.x;
|
||||
let width = piece.width();
|
||||
|
||||
// count * width + (count - 1) * gap = fill, but count is an integer so
|
||||
// we need to round down and get the remainder.
|
||||
// We need to fit the body N times, but the number of gaps is (N - 1):
|
||||
// N * w + (N - 1) * g ≤ F
|
||||
// where N - body count (count)
|
||||
// w - body width (width)
|
||||
// g - gap width (gap)
|
||||
// F - available space to fill (fill)
|
||||
//
|
||||
// N * w + N * g - g ≤ F
|
||||
// N * (w + g) ≤ F + g
|
||||
// N ≤ (F + g) / (w + g)
|
||||
// N = ⌊(F + g) / (w + g)⌋
|
||||
let count = ((fill + gap) / (width + gap)).floor();
|
||||
let remaining = (fill + gap) % (width + gap);
|
||||
|
||||
@ -52,7 +61,7 @@ pub fn layout_repeat(
|
||||
if width > Abs::zero() {
|
||||
for _ in 0..(count as usize).min(1000) {
|
||||
frame.push_frame(Point::with_x(offset), piece.clone());
|
||||
offset += piece.width() + gap;
|
||||
offset += width + gap;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -348,29 +348,48 @@ fn layout_shape(
|
||||
pod.size = crate::pad::shrink(region.size, &inset);
|
||||
}
|
||||
|
||||
// Layout the child.
|
||||
frame = crate::layout_frame(engine, child, locator.relayout(), styles, pod)?;
|
||||
|
||||
// If the child is a square or circle, relayout with full expansion into
|
||||
// square region to make sure the result is really quadratic.
|
||||
// If the shape is quadratic, we first measure it to determine its size
|
||||
// and then layout with full expansion to force the aspect ratio and
|
||||
// make sure it's really quadratic.
|
||||
if kind.is_quadratic() {
|
||||
let length = frame.size().max_by_side().min(pod.size.min_by_side());
|
||||
let quad_pod = Region::new(Size::splat(length), Axes::splat(true));
|
||||
frame = crate::layout_frame(engine, child, locator, styles, quad_pod)?;
|
||||
let length = match quadratic_size(pod) {
|
||||
Some(length) => length,
|
||||
None => {
|
||||
// Take as much as the child wants, but without overflowing.
|
||||
crate::layout_frame(engine, child, locator.relayout(), styles, pod)?
|
||||
.size()
|
||||
.max_by_side()
|
||||
.min(pod.size.min_by_side())
|
||||
}
|
||||
};
|
||||
|
||||
pod = Region::new(Size::splat(length), Axes::splat(true));
|
||||
}
|
||||
|
||||
// Layout the child.
|
||||
frame = crate::layout_frame(engine, child, locator, styles, pod)?;
|
||||
|
||||
// Apply the inset.
|
||||
if has_inset {
|
||||
crate::pad::grow(&mut frame, &inset);
|
||||
}
|
||||
} else {
|
||||
// The default size that a shape takes on if it has no child and
|
||||
// enough space.
|
||||
let default = Size::new(Abs::pt(45.0), Abs::pt(30.0));
|
||||
let mut size = region.expand.select(region.size, default.min(region.size));
|
||||
if kind.is_quadratic() {
|
||||
size = Size::splat(size.min_by_side());
|
||||
}
|
||||
// The default size that a shape takes on if it has no child and no
|
||||
// forced sizes.
|
||||
let default = Size::new(Abs::pt(45.0), Abs::pt(30.0)).min(region.size);
|
||||
|
||||
let size = if kind.is_quadratic() {
|
||||
Size::splat(match quadratic_size(region) {
|
||||
Some(length) => length,
|
||||
None => default.min_by_side(),
|
||||
})
|
||||
} else {
|
||||
// For each dimension, pick the region size if forced, otherwise
|
||||
// use the default size (or the region size if the default
|
||||
// is too large for the region).
|
||||
region.expand.select(region.size, default)
|
||||
};
|
||||
|
||||
frame = Frame::soft(size);
|
||||
}
|
||||
|
||||
@ -411,6 +430,24 @@ fn layout_shape(
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
/// Determines the forced size of a quadratic shape based on the region, if any.
|
||||
///
|
||||
/// The size is forced if at least one axis is expanded because `expand` is
|
||||
/// `true` for axes whose size was manually specified by the user.
|
||||
fn quadratic_size(region: Region) -> Option<Abs> {
|
||||
if region.expand.x && region.expand.y {
|
||||
// If both `width` and `height` are specified, we choose the
|
||||
// smaller one.
|
||||
Some(region.size.x.min(region.size.y))
|
||||
} else if region.expand.x {
|
||||
Some(region.size.x)
|
||||
} else if region.expand.y {
|
||||
Some(region.size.y)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new rectangle as a path.
|
||||
pub fn clip_rect(
|
||||
size: Size,
|
||||
@ -682,11 +719,7 @@ fn segment(
|
||||
false
|
||||
}
|
||||
|
||||
let solid = stroke
|
||||
.dash
|
||||
.as_ref()
|
||||
.map(|pattern| pattern.array.is_empty())
|
||||
.unwrap_or(true);
|
||||
let solid = stroke.dash.as_ref().map(|dash| dash.array.is_empty()).unwrap_or(true);
|
||||
|
||||
let use_fill = solid && fill_corners(start, end, corners);
|
||||
let shape = if use_fill {
|
||||
|
@ -44,6 +44,7 @@ png = { workspace = true }
|
||||
qcms = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
regex-syntax = { workspace = true }
|
||||
roxmltree = { workspace = true }
|
||||
rust_decimal = { workspace = true }
|
||||
rustybuzz = { workspace = true }
|
||||
|
@ -11,7 +11,12 @@ use crate::foundations::{
|
||||
///
|
||||
/// The number can be negative, zero, or positive. As Typst uses 64 bits to
|
||||
/// store integers, integers cannot be smaller than `{-9223372036854775808}` or
|
||||
/// larger than `{9223372036854775807}`.
|
||||
/// larger than `{9223372036854775807}`. Integer literals are always positive,
|
||||
/// so a negative integer such as `{-1}` is semantically the negation `-` of the
|
||||
/// positive literal `1`. A positive integer greater than the maximum value and
|
||||
/// a negative integer less than or equal to the minimum value cannot be
|
||||
/// represented as an integer literal, and are instead parsed as a `{float}`.
|
||||
/// The minimum integer value can still be obtained through integer arithmetic.
|
||||
///
|
||||
/// The number can also be specified as hexadecimal, octal, or binary by
|
||||
/// starting it with a zero followed by either `x`, `o`, or `b`.
|
||||
|
@ -119,7 +119,6 @@ pub(super) fn define(global: &mut Scope, inputs: Dict, features: &Features) {
|
||||
global.define_func::<panic>();
|
||||
global.define_func::<assert>();
|
||||
global.define_func::<eval>();
|
||||
global.define_func::<style>();
|
||||
if features.is_enabled(Feature::Html) {
|
||||
global.define_func::<target>();
|
||||
}
|
||||
|
@ -36,11 +36,6 @@ pub fn join(lhs: Value, rhs: Value) -> StrResult<Value> {
|
||||
(Symbol(a), Content(b)) => Content(TextElem::packed(a.get()) + b),
|
||||
(Array(a), Array(b)) => Array(a + b),
|
||||
(Dict(a), Dict(b)) => Dict(a + b),
|
||||
|
||||
// Type compatibility.
|
||||
(Type(a), Str(b)) => Str(format_str!("{a}{b}")),
|
||||
(Str(a), Type(b)) => Str(format_str!("{a}{b}")),
|
||||
|
||||
(a, b) => mismatch!("cannot join {} with {}", a, b),
|
||||
})
|
||||
}
|
||||
@ -149,18 +144,14 @@ pub fn add(lhs: Value, rhs: Value) -> HintedStrResult<Value> {
|
||||
| (Length(thickness), Gradient(gradient)) => {
|
||||
Stroke::from_pair(gradient, thickness).into_value()
|
||||
}
|
||||
(Pattern(pattern), Length(thickness)) | (Length(thickness), Pattern(pattern)) => {
|
||||
Stroke::from_pair(pattern, thickness).into_value()
|
||||
(Tiling(tiling), Length(thickness)) | (Length(thickness), Tiling(tiling)) => {
|
||||
Stroke::from_pair(tiling, thickness).into_value()
|
||||
}
|
||||
|
||||
(Duration(a), Duration(b)) => Duration(a + b),
|
||||
(Datetime(a), Duration(b)) => Datetime(a + b),
|
||||
(Duration(a), Datetime(b)) => Datetime(b + a),
|
||||
|
||||
// Type compatibility.
|
||||
(Type(a), Str(b)) => Str(format_str!("{a}{b}")),
|
||||
(Str(a), Type(b)) => Str(format_str!("{a}{b}")),
|
||||
|
||||
(Dyn(a), Dyn(b)) => {
|
||||
// Alignments can be summed.
|
||||
if let (Some(&a), Some(&b)) =
|
||||
@ -469,9 +460,6 @@ pub fn equal(lhs: &Value, rhs: &Value) -> bool {
|
||||
rat == rel.rel && rel.abs.is_zero()
|
||||
}
|
||||
|
||||
// Type compatibility.
|
||||
(Type(ty), Str(str)) | (Str(str), Type(ty)) => ty.compat_name() == str.as_str(),
|
||||
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@ -569,10 +557,6 @@ pub fn contains(lhs: &Value, rhs: &Value) -> Option<bool> {
|
||||
(Str(a), Dict(b)) => Some(b.contains(a)),
|
||||
(a, Array(b)) => Some(b.contains(a.clone())),
|
||||
|
||||
// Type compatibility.
|
||||
(Type(a), Str(b)) => Some(b.as_str().contains(a.compat_name())),
|
||||
(Type(a), Dict(b)) => Some(b.contains(a.compat_name())),
|
||||
|
||||
_ => Option::None,
|
||||
}
|
||||
}
|
||||
|
@ -3,75 +3,19 @@ use std::fmt::{self, Debug, Formatter};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::{mem, ptr};
|
||||
|
||||
use comemo::{Track, Tracked};
|
||||
use comemo::Tracked;
|
||||
use ecow::{eco_vec, EcoString, EcoVec};
|
||||
use smallvec::SmallVec;
|
||||
use typst_syntax::Span;
|
||||
use typst_utils::LazyHash;
|
||||
|
||||
use crate::diag::{warning, SourceResult, Trace, Tracepoint};
|
||||
use crate::diag::{SourceResult, Trace, Tracepoint};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
cast, elem, func, ty, Content, Context, Element, Func, NativeElement, Packed, Repr,
|
||||
Selector, Show,
|
||||
cast, ty, Content, Context, Element, Func, NativeElement, Repr, Selector,
|
||||
};
|
||||
use crate::introspection::Locatable;
|
||||
use crate::text::{FontFamily, FontList, TextElem};
|
||||
|
||||
/// Provides access to active styles.
|
||||
///
|
||||
/// **Deprecation planned.** Use [context] instead.
|
||||
///
|
||||
/// ```example
|
||||
/// #let thing(body) = style(styles => {
|
||||
/// let size = measure(body, styles)
|
||||
/// [Width of "#body" is #size.width]
|
||||
/// })
|
||||
///
|
||||
/// #thing[Hey] \
|
||||
/// #thing[Welcome]
|
||||
/// ```
|
||||
#[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
|
||||
/// in the document.
|
||||
///
|
||||
/// This function is called once for each time the content returned by
|
||||
/// `style` appears in the document. That makes it possible to generate
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// Executes a style access.
|
||||
#[elem(Locatable, Show)]
|
||||
struct StyleElem {
|
||||
/// The function to call with the styles.
|
||||
#[required]
|
||||
func: Func,
|
||||
}
|
||||
|
||||
impl Show for Packed<StyleElem> {
|
||||
#[typst_macros::time(name = "style", span = self.span())]
|
||||
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||
let context = Context::new(self.location(), Some(styles));
|
||||
Ok(self
|
||||
.func()
|
||||
.call(engine, context.track(), [styles.to_map()])?
|
||||
.display())
|
||||
}
|
||||
}
|
||||
|
||||
/// A list of style properties.
|
||||
#[ty(cast)]
|
||||
#[derive(Default, PartialEq, Clone, Hash)]
|
||||
|
@ -246,10 +246,50 @@ impl Debug for List {
|
||||
|
||||
impl crate::foundations::Repr for Symbol {
|
||||
fn repr(&self) -> EcoString {
|
||||
eco_format!("\"{}\"", self.get())
|
||||
match &self.0 {
|
||||
Repr::Single(c) => eco_format!("symbol(\"{}\")", *c),
|
||||
Repr::Complex(variants) => {
|
||||
eco_format!("symbol{}", repr_variants(variants.iter().copied(), ""))
|
||||
}
|
||||
Repr::Modified(arc) => {
|
||||
let (list, modifiers) = arc.as_ref();
|
||||
if modifiers.is_empty() {
|
||||
eco_format!("symbol{}", repr_variants(list.variants(), ""))
|
||||
} else {
|
||||
eco_format!("symbol{}", repr_variants(list.variants(), modifiers))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn repr_variants<'a>(
|
||||
variants: impl Iterator<Item = (&'a str, char)>,
|
||||
applied_modifiers: &str,
|
||||
) -> String {
|
||||
crate::foundations::repr::pretty_array_like(
|
||||
&variants
|
||||
.filter(|(variant, _)| {
|
||||
// Only keep variants that can still be accessed, i.e., variants
|
||||
// that contain all applied modifiers.
|
||||
parts(applied_modifiers).all(|am| variant.split('.').any(|m| m == am))
|
||||
})
|
||||
.map(|(variant, c)| {
|
||||
let trimmed_variant = variant
|
||||
.split('.')
|
||||
.filter(|&m| parts(applied_modifiers).all(|am| m != am));
|
||||
if trimmed_variant.clone().all(|m| m.is_empty()) {
|
||||
eco_format!("\"{c}\"")
|
||||
} else {
|
||||
let trimmed_modifiers = trimmed_variant.collect::<Vec<_>>().join(".");
|
||||
eco_format!("(\"{}\", \"{}\")", trimmed_modifiers, c)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
impl Serialize for Symbol {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
|
@ -44,16 +44,6 @@ use crate::foundations::{
|
||||
/// #type(int) \
|
||||
/// #type(type)
|
||||
/// ```
|
||||
///
|
||||
/// # Compatibility
|
||||
/// In Typst 0.7 and lower, the `type` function returned a string instead of a
|
||||
/// type. Compatibility with the old way will remain for a while to give package
|
||||
/// authors time to upgrade, but it will be removed at some point.
|
||||
///
|
||||
/// - Checks like `{int == "integer"}` evaluate to `{true}`
|
||||
/// - Adding/joining a type and string will yield a string
|
||||
/// - The `{in}` operator on a type and a dictionary will evaluate to `{true}`
|
||||
/// if the dictionary has a string key matching the type's name
|
||||
#[ty(scope, cast)]
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Type(Static<NativeTypeData>);
|
||||
@ -111,14 +101,6 @@ impl Type {
|
||||
}
|
||||
}
|
||||
|
||||
// Type compatibility.
|
||||
impl Type {
|
||||
/// The type's backward-compatible name.
|
||||
pub fn compat_name(&self) -> &str {
|
||||
self.long_name()
|
||||
}
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl Type {
|
||||
/// Determines a value's type.
|
||||
|
@ -20,7 +20,7 @@ use crate::foundations::{
|
||||
};
|
||||
use crate::layout::{Abs, Angle, Em, Fr, Length, Ratio, Rel};
|
||||
use crate::text::{RawContent, RawElem, TextElem};
|
||||
use crate::visualize::{Color, Gradient, Pattern};
|
||||
use crate::visualize::{Color, Gradient, Tiling};
|
||||
|
||||
/// A computational value.
|
||||
#[derive(Default, Clone)]
|
||||
@ -50,8 +50,8 @@ pub enum Value {
|
||||
Color(Color),
|
||||
/// A gradient value: `gradient.linear(...)`.
|
||||
Gradient(Gradient),
|
||||
/// A pattern fill: `pattern(...)`.
|
||||
Pattern(Pattern),
|
||||
/// A tiling fill: `tiling(...)`.
|
||||
Tiling(Tiling),
|
||||
/// A symbol: `arrow.l`.
|
||||
Symbol(Symbol),
|
||||
/// A version.
|
||||
@ -130,7 +130,7 @@ impl Value {
|
||||
Self::Fraction(_) => Type::of::<Fr>(),
|
||||
Self::Color(_) => Type::of::<Color>(),
|
||||
Self::Gradient(_) => Type::of::<Gradient>(),
|
||||
Self::Pattern(_) => Type::of::<Pattern>(),
|
||||
Self::Tiling(_) => Type::of::<Tiling>(),
|
||||
Self::Symbol(_) => Type::of::<Symbol>(),
|
||||
Self::Version(_) => Type::of::<Version>(),
|
||||
Self::Str(_) => Type::of::<Str>(),
|
||||
@ -244,7 +244,7 @@ impl Debug for Value {
|
||||
Self::Fraction(v) => Debug::fmt(v, f),
|
||||
Self::Color(v) => Debug::fmt(v, f),
|
||||
Self::Gradient(v) => Debug::fmt(v, f),
|
||||
Self::Pattern(v) => Debug::fmt(v, f),
|
||||
Self::Tiling(v) => Debug::fmt(v, f),
|
||||
Self::Symbol(v) => Debug::fmt(v, f),
|
||||
Self::Version(v) => Debug::fmt(v, f),
|
||||
Self::Str(v) => Debug::fmt(v, f),
|
||||
@ -282,7 +282,7 @@ impl Repr for Value {
|
||||
Self::Fraction(v) => v.repr(),
|
||||
Self::Color(v) => v.repr(),
|
||||
Self::Gradient(v) => v.repr(),
|
||||
Self::Pattern(v) => v.repr(),
|
||||
Self::Tiling(v) => v.repr(),
|
||||
Self::Symbol(v) => v.repr(),
|
||||
Self::Version(v) => v.repr(),
|
||||
Self::Str(v) => v.repr(),
|
||||
@ -333,7 +333,7 @@ impl Hash for Value {
|
||||
Self::Fraction(v) => v.hash(state),
|
||||
Self::Color(v) => v.hash(state),
|
||||
Self::Gradient(v) => v.hash(state),
|
||||
Self::Pattern(v) => v.hash(state),
|
||||
Self::Tiling(v) => v.hash(state),
|
||||
Self::Symbol(v) => v.hash(state),
|
||||
Self::Version(v) => v.hash(state),
|
||||
Self::Str(v) => v.hash(state),
|
||||
@ -640,7 +640,7 @@ primitive! { Rel<Length>: "relative length",
|
||||
primitive! { Fr: "fraction", Fraction }
|
||||
primitive! { Color: "color", Color }
|
||||
primitive! { Gradient: "gradient", Gradient }
|
||||
primitive! { Pattern: "pattern", Pattern }
|
||||
primitive! { Tiling: "tiling", Tiling }
|
||||
primitive! { Symbol: "symbol", Symbol }
|
||||
primitive! { Version: "version", Version }
|
||||
primitive! {
|
||||
|
@ -122,8 +122,8 @@ impl HtmlTag {
|
||||
let bytes = string.as_bytes();
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
if !bytes[i].is_ascii_alphanumeric() {
|
||||
panic!("constant tag name must be ASCII alphanumeric");
|
||||
if !bytes[i].is_ascii() || !charsets::is_valid_in_tag_name(bytes[i] as char) {
|
||||
panic!("not all characters are valid in a tag name");
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
@ -220,8 +220,10 @@ impl HtmlAttr {
|
||||
let bytes = string.as_bytes();
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
if !bytes[i].is_ascii_alphanumeric() {
|
||||
panic!("constant attribute name must be ASCII alphanumeric");
|
||||
if !bytes[i].is_ascii()
|
||||
|| !charsets::is_valid_in_attribute_name(bytes[i] as char)
|
||||
{
|
||||
panic!("not all characters are valid in an attribute name");
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
@ -470,6 +472,59 @@ pub mod tag {
|
||||
wbr
|
||||
}
|
||||
|
||||
/// Whether nodes with the tag have the CSS property `display: block` by
|
||||
/// default.
|
||||
///
|
||||
/// If this is true, then pretty-printing can insert spaces around such
|
||||
/// nodes and around the contents of such nodes.
|
||||
///
|
||||
/// However, when users change the properties of such tags via CSS, the
|
||||
/// insertion of whitespace may actually impact the visual output; for
|
||||
/// example, <https://www.w3.org/TR/css-text-3/#example-af2745cd> shows how
|
||||
/// adding CSS rules to `<p>` can make it sensitive to whitespace. In such
|
||||
/// cases, users should disable pretty-printing.
|
||||
pub fn is_block_by_default(tag: HtmlTag) -> bool {
|
||||
matches!(
|
||||
tag,
|
||||
self::html
|
||||
| self::head
|
||||
| self::body
|
||||
| self::article
|
||||
| self::aside
|
||||
| self::h1
|
||||
| self::h2
|
||||
| self::h3
|
||||
| self::h4
|
||||
| self::h5
|
||||
| self::h6
|
||||
| self::hgroup
|
||||
| self::nav
|
||||
| self::section
|
||||
| self::dd
|
||||
| self::dl
|
||||
| self::dt
|
||||
| self::menu
|
||||
| self::ol
|
||||
| self::ul
|
||||
| self::address
|
||||
| self::blockquote
|
||||
| self::dialog
|
||||
| self::div
|
||||
| self::fieldset
|
||||
| self::figure
|
||||
| self::figcaption
|
||||
| self::footer
|
||||
| self::form
|
||||
| self::header
|
||||
| self::hr
|
||||
| self::legend
|
||||
| self::main
|
||||
| self::p
|
||||
| self::pre
|
||||
| self::search
|
||||
)
|
||||
}
|
||||
|
||||
/// Whether the element is inline-level as opposed to being block-level.
|
||||
///
|
||||
/// Not sure whether this distinction really makes sense. But we somehow
|
||||
@ -480,7 +535,7 @@ pub mod tag {
|
||||
/// <https://www.w3.org/TR/html401/struct/global.html#block-inline>
|
||||
/// <https://developer.mozilla.org/en-US/docs/Glossary/Inline-level_content>
|
||||
/// <https://github.com/orgs/mdn/discussions/353>
|
||||
pub fn is_inline(tag: HtmlTag) -> bool {
|
||||
pub fn is_inline_by_default(tag: HtmlTag) -> bool {
|
||||
matches!(
|
||||
tag,
|
||||
self::abbr
|
||||
@ -568,5 +623,9 @@ pub mod attr {
|
||||
href
|
||||
name
|
||||
value
|
||||
role
|
||||
}
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub const aria_level: HtmlAttr = HtmlAttr::constant("aria-level");
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ use smallvec::{smallvec, SmallVec};
|
||||
use typst_syntax::Span;
|
||||
use typst_utils::NonZeroExt;
|
||||
|
||||
use crate::diag::{bail, warning, At, HintedStrResult, SourceResult};
|
||||
use crate::diag::{bail, 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,
|
||||
@ -353,7 +353,7 @@ impl Counter {
|
||||
}
|
||||
|
||||
/// Shared implementation of displaying between `counter.display` and
|
||||
/// `DisplayElem`, which will be deprecated.
|
||||
/// `CounterDisplayElem`.
|
||||
fn display_impl(
|
||||
&self,
|
||||
engine: &mut Engine,
|
||||
@ -366,20 +366,22 @@ impl Counter {
|
||||
.custom()
|
||||
.or_else(|| {
|
||||
let styles = styles?;
|
||||
let CounterKey::Selector(Selector::Elem(func, _)) = self.0 else {
|
||||
return None;
|
||||
};
|
||||
|
||||
if func == HeadingElem::elem() {
|
||||
HeadingElem::numbering_in(styles).clone()
|
||||
} else if func == FigureElem::elem() {
|
||||
FigureElem::numbering_in(styles).clone()
|
||||
} else if func == EquationElem::elem() {
|
||||
EquationElem::numbering_in(styles).clone()
|
||||
} else if func == FootnoteElem::elem() {
|
||||
Some(FootnoteElem::numbering_in(styles).clone())
|
||||
} else {
|
||||
None
|
||||
match self.0 {
|
||||
CounterKey::Page => PageElem::numbering_in(styles).clone(),
|
||||
CounterKey::Selector(Selector::Elem(func, _)) => {
|
||||
if func == HeadingElem::elem() {
|
||||
HeadingElem::numbering_in(styles).clone()
|
||||
} else if func == FigureElem::elem() {
|
||||
FigureElem::numbering_in(styles).clone()
|
||||
} else if func == EquationElem::elem() {
|
||||
EquationElem::numbering_in(styles).clone()
|
||||
} else if func == FootnoteElem::elem() {
|
||||
Some(FootnoteElem::numbering_in(styles).clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| NumberingPattern::from_str("1.1").unwrap().into());
|
||||
@ -439,11 +441,6 @@ impl Counter {
|
||||
|
||||
/// Displays the current value of the counter with a numbering and returns
|
||||
/// the formatted output.
|
||||
///
|
||||
/// _Compatibility:_ For compatibility with Typst 0.10 and lower, this
|
||||
/// function also works without an established context. Then, it will create
|
||||
/// opaque contextual content rather than directly returning the output of
|
||||
/// the numbering. This behaviour will be removed in a future release.
|
||||
#[func(contextual)]
|
||||
pub fn display(
|
||||
self,
|
||||
@ -472,19 +469,8 @@ impl Counter {
|
||||
#[default(false)]
|
||||
both: bool,
|
||||
) -> SourceResult<Value> {
|
||||
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)
|
||||
.into_value())
|
||||
}
|
||||
let loc = context.location().at(span)?;
|
||||
self.display_impl(engine, loc, numbering, both, context.styles().ok())
|
||||
}
|
||||
|
||||
/// Retrieves the value of the counter at the given location. Always returns
|
||||
@ -493,10 +479,6 @@ impl Counter {
|
||||
/// The `selector` must match exactly one element in the document. The most
|
||||
/// useful kinds of selectors for this are [labels]($label) and
|
||||
/// [locations]($location).
|
||||
///
|
||||
/// _Compatibility:_ For compatibility with Typst 0.10 and lower, this
|
||||
/// function also works without a known context if the `selector` is a
|
||||
/// location. This behaviour will be removed in a future release.
|
||||
#[func(contextual)]
|
||||
pub fn at(
|
||||
&self,
|
||||
@ -524,21 +506,8 @@ impl Counter {
|
||||
context: Tracked<Context>,
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// _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"
|
||||
));
|
||||
}
|
||||
|
||||
context.introspect().at(span)?;
|
||||
let sequence = self.sequence(engine)?;
|
||||
let (mut state, page) = sequence.last().unwrap().clone();
|
||||
if self.is_page() {
|
||||
@ -759,8 +728,6 @@ impl Count for Packed<CounterUpdateElem> {
|
||||
}
|
||||
|
||||
/// Executes a display of a counter.
|
||||
///
|
||||
/// **Deprecation planned.**
|
||||
#[elem(Construct, Locatable, Show)]
|
||||
pub struct CounterDisplayElem {
|
||||
/// The counter.
|
||||
@ -786,7 +753,6 @@ impl Construct for CounterDisplayElem {
|
||||
}
|
||||
|
||||
impl Show for Packed<CounterDisplayElem> {
|
||||
#[typst_macros::time(name = "counter.display", span = self.span())]
|
||||
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||
Ok(self
|
||||
.counter
|
||||
|
@ -1,13 +1,9 @@
|
||||
use comemo::{Track, Tracked};
|
||||
use typst_syntax::Span;
|
||||
use comemo::Tracked;
|
||||
|
||||
use crate::diag::{warning, HintedStrResult, SourceResult};
|
||||
use crate::diag::HintedStrResult;
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
cast, elem, func, Content, Context, Func, LocatableSelector, NativeElement, Packed,
|
||||
Show, StyleChain, Value,
|
||||
};
|
||||
use crate::introspection::{Locatable, Location};
|
||||
use crate::foundations::{func, Context, LocatableSelector};
|
||||
use crate::introspection::Location;
|
||||
|
||||
/// Determines the location of an element in the document.
|
||||
///
|
||||
@ -26,23 +22,12 @@ use crate::introspection::{Locatable, Location};
|
||||
///
|
||||
/// = Introduction <intro>
|
||||
/// ```
|
||||
///
|
||||
/// # 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). 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.
|
||||
engine: &mut Engine,
|
||||
/// The callsite context.
|
||||
context: Tracked<Context>,
|
||||
/// The span of the `locate` call.
|
||||
span: Span,
|
||||
/// A selector that should match exactly one element. This element will be
|
||||
/// located.
|
||||
///
|
||||
@ -50,70 +35,7 @@ pub fn locate(
|
||||
/// - [`here`] to locate the current context,
|
||||
/// - a [`location`] retrieved from some queried element via the
|
||||
/// [`location()`]($content.location) method on content.
|
||||
selector: LocateInput,
|
||||
) -> HintedStrResult<LocateOutput> {
|
||||
Ok(match selector {
|
||||
LocateInput::Selector(selector) => {
|
||||
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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Compatible input type.
|
||||
pub enum LocateInput {
|
||||
Selector(LocatableSelector),
|
||||
Func(Func),
|
||||
}
|
||||
|
||||
cast! {
|
||||
LocateInput,
|
||||
v: Func => {
|
||||
if v.element().is_some() {
|
||||
Self::Selector(Value::Func(v).cast()?)
|
||||
} else {
|
||||
Self::Func(v)
|
||||
}
|
||||
},
|
||||
v: LocatableSelector => Self::Selector(v),
|
||||
}
|
||||
|
||||
/// Compatible output type.
|
||||
pub enum LocateOutput {
|
||||
Location(Location),
|
||||
Content(Content),
|
||||
}
|
||||
|
||||
cast! {
|
||||
LocateOutput,
|
||||
self => match self {
|
||||
Self::Location(v) => v.into_value(),
|
||||
Self::Content(v) => v.into_value(),
|
||||
},
|
||||
v: Location => Self::Location(v),
|
||||
v: Content => Self::Content(v),
|
||||
}
|
||||
|
||||
/// Executes a `locate` call.
|
||||
#[elem(Locatable, Show)]
|
||||
struct LocateElem {
|
||||
/// The function to call with the location.
|
||||
#[required]
|
||||
func: Func,
|
||||
}
|
||||
|
||||
impl Show for Packed<LocateElem> {
|
||||
#[typst_macros::time(name = "locate", span = self.span())]
|
||||
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||
let location = self.location().unwrap();
|
||||
let context = Context::new(Some(location), Some(styles));
|
||||
Ok(self.func().call(engine, context.track(), [location])?.display())
|
||||
}
|
||||
selector: LocatableSelector,
|
||||
) -> HintedStrResult<Location> {
|
||||
selector.resolve_unique(engine.introspector, context)
|
||||
}
|
||||
|
@ -161,7 +161,7 @@ impl<'a> Locator<'a> {
|
||||
///
|
||||
/// Should typically only be created at the document level, though there
|
||||
/// are a few places where we use it as well that just don't support
|
||||
/// introspection (e.g. drawable patterns).
|
||||
/// introspection (e.g. tilings).
|
||||
pub fn root() -> Self {
|
||||
Self { local: 0, outer: None }
|
||||
}
|
||||
|
@ -1,10 +1,8 @@
|
||||
use comemo::Tracked;
|
||||
use typst_syntax::Span;
|
||||
|
||||
use crate::diag::{warning, HintedStrResult};
|
||||
use crate::diag::HintedStrResult;
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{func, Array, Context, LocatableSelector, Value};
|
||||
use crate::introspection::Location;
|
||||
|
||||
/// Finds elements in the document.
|
||||
///
|
||||
@ -142,8 +140,6 @@ 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>}`,
|
||||
@ -152,20 +148,8 @@ pub fn query(
|
||||
///
|
||||
/// Only [locatable]($location/#locatable) element functions are supported.
|
||||
target: LocatableSelector,
|
||||
/// _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"
|
||||
));
|
||||
}
|
||||
|
||||
context.introspect()?;
|
||||
let vec = engine.introspector.query(&target.0);
|
||||
Ok(vec.into_iter().map(Value::Content).collect())
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ use comemo::{Track, Tracked, TrackedMut};
|
||||
use ecow::{eco_format, eco_vec, EcoString, EcoVec};
|
||||
use typst_syntax::Span;
|
||||
|
||||
use crate::diag::{bail, warning, At, SourceResult};
|
||||
use crate::diag::{bail, At, SourceResult};
|
||||
use crate::engine::{Engine, Route, Sink, Traced};
|
||||
use crate::foundations::{
|
||||
cast, elem, func, scope, select_where, ty, Args, Construct, Content, Context, Func,
|
||||
@ -305,10 +305,6 @@ impl State {
|
||||
/// The `selector` must match exactly one element in the document. The most
|
||||
/// useful kinds of selectors for this are [labels]($label) and
|
||||
/// [locations]($location).
|
||||
///
|
||||
/// _Compatibility:_ For compatibility with Typst 0.10 and lower, this
|
||||
/// function also works without a known context if the `selector` is a
|
||||
/// location. This behaviour will be removed in a future release.
|
||||
#[typst_macros::time(name = "state.at", span = span)]
|
||||
#[func(contextual)]
|
||||
pub fn at(
|
||||
@ -336,21 +332,8 @@ impl State {
|
||||
context: Tracked<Context>,
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// _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"
|
||||
));
|
||||
}
|
||||
|
||||
context.introspect().at(span)?;
|
||||
let sequence = self.sequence(engine)?;
|
||||
Ok(sequence.last().unwrap().clone())
|
||||
}
|
||||
@ -375,30 +358,6 @@ impl State {
|
||||
) -> Content {
|
||||
StateUpdateElem::new(self.key, update).pack().spanned(span)
|
||||
}
|
||||
|
||||
/// Displays the current value of the state.
|
||||
///
|
||||
/// **Deprecation planned:** Use [`get`]($state.get) instead.
|
||||
#[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
|
||||
/// arbitrary content which is then displayed. If this is omitted, the
|
||||
/// value is directly displayed.
|
||||
#[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)
|
||||
}
|
||||
}
|
||||
|
||||
impl Repr for State {
|
||||
@ -446,38 +405,3 @@ impl Show for Packed<StateUpdateElem> {
|
||||
Ok(Content::empty())
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes a display of a state.
|
||||
///
|
||||
/// **Deprecation planned.**
|
||||
#[elem(Construct, Locatable, Show)]
|
||||
struct StateDisplayElem {
|
||||
/// The state.
|
||||
#[required]
|
||||
#[internal]
|
||||
state: State,
|
||||
|
||||
/// The function to display the state with.
|
||||
#[required]
|
||||
#[internal]
|
||||
func: Option<Func>,
|
||||
}
|
||||
|
||||
impl Show for Packed<StateDisplayElem> {
|
||||
#[typst_macros::time(name = "state.display", span = self.span())]
|
||||
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||
let location = self.location().unwrap();
|
||||
let context = Context::new(Some(location), Some(styles));
|
||||
let value = self.state().at_loc(engine, location)?;
|
||||
Ok(match self.func() {
|
||||
Some(func) => func.call(engine, context.track(), [value])?.display(),
|
||||
None => value.display(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Construct for StateDisplayElem {
|
||||
fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
|
||||
bail!(args.span, "cannot be constructed manually");
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,9 @@
|
||||
use comemo::Tracked;
|
||||
use typst_syntax::Span;
|
||||
|
||||
use crate::diag::{warning, At, SourceResult};
|
||||
use crate::diag::{At, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
dict, func, Content, Context, Dict, Resolve, Smart, StyleChain, Styles,
|
||||
};
|
||||
use crate::foundations::{dict, func, Content, Context, Dict, Resolve, Smart};
|
||||
use crate::introspection::{Locator, LocatorLink};
|
||||
use crate::layout::{Abs, Axes, Length, Region, Size};
|
||||
|
||||
@ -76,23 +74,9 @@ pub fn measure(
|
||||
height: Smart<Length>,
|
||||
/// The content whose size to measure.
|
||||
content: Content,
|
||||
/// _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) => {
|
||||
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)?,
|
||||
};
|
||||
|
||||
// Create a pod region with the available space.
|
||||
let styles = context.styles().at(span)?;
|
||||
let pod = Region::new(
|
||||
Axes::new(
|
||||
width.resolve(styles).unwrap_or(Abs::inf()),
|
||||
|
@ -9,7 +9,7 @@ use crate::foundations::{
|
||||
cast, elem, scope, Array, Content, NativeElement, Packed, Show, Smart, StyleChain,
|
||||
Styles, TargetElem,
|
||||
};
|
||||
use crate::html::{attr, tag, HtmlElem};
|
||||
use crate::html::{attr, tag, HtmlAttr, HtmlElem};
|
||||
use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem};
|
||||
use crate::model::{ListItemLike, ListLike, Numbering, NumberingPattern, ParElem};
|
||||
|
||||
@ -127,8 +127,7 @@ pub struct EnumElem {
|
||||
/// [Ahead],
|
||||
/// )
|
||||
/// ```
|
||||
#[default(1)]
|
||||
pub start: usize,
|
||||
pub start: Smart<usize>,
|
||||
|
||||
/// Whether to display the full numbering, including the numbers of
|
||||
/// all parent enumerations.
|
||||
@ -144,6 +143,17 @@ pub struct EnumElem {
|
||||
#[default(false)]
|
||||
pub full: bool,
|
||||
|
||||
/// Whether to reverse the numbering for this enumeration.
|
||||
///
|
||||
/// ```example
|
||||
/// #set enum(reversed: true)
|
||||
/// + Coffee
|
||||
/// + Tea
|
||||
/// + Milk
|
||||
/// ```
|
||||
#[default(false)]
|
||||
pub reversed: bool,
|
||||
|
||||
/// The indentation of each item.
|
||||
#[resolve]
|
||||
pub indent: Length,
|
||||
@ -217,7 +227,12 @@ impl EnumElem {
|
||||
impl Show for Packed<EnumElem> {
|
||||
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||
if TargetElem::target_in(styles).is_html() {
|
||||
return Ok(HtmlElem::new(tag::ol)
|
||||
let mut elem = HtmlElem::new(tag::ol);
|
||||
if self.reversed(styles) {
|
||||
elem =
|
||||
elem.with_attr(const { HtmlAttr::constant("reversed") }, "reversed");
|
||||
}
|
||||
return Ok(elem
|
||||
.with_body(Some(Content::sequence(self.children.iter().map(|item| {
|
||||
let mut li = HtmlElem::new(tag::li);
|
||||
if let Some(nr) = item.number(styles) {
|
||||
|
@ -308,6 +308,7 @@ impl Synthesize for Packed<FigureElem> {
|
||||
// Fill the figure's caption.
|
||||
let mut caption = elem.caption(styles);
|
||||
if let Some(caption) = &mut caption {
|
||||
caption.synthesize(engine, styles)?;
|
||||
caption.push_kind(kind.clone());
|
||||
caption.push_supplement(supplement.clone());
|
||||
caption.push_numbering(numbering.clone());
|
||||
|
@ -194,7 +194,7 @@ cast! {
|
||||
/// before any page content, typically at the very start of the document.
|
||||
#[elem(name = "entry", title = "Footnote Entry", Show, ShowSet)]
|
||||
pub struct FootnoteEntry {
|
||||
/// The footnote for this entry. It's location can be used to determine
|
||||
/// The footnote for this entry. Its location can be used to determine
|
||||
/// the footnote counter state.
|
||||
///
|
||||
/// ```example
|
||||
|
@ -1,14 +1,15 @@
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use ecow::eco_format;
|
||||
use typst_utils::NonZeroExt;
|
||||
|
||||
use crate::diag::SourceResult;
|
||||
use crate::diag::{warning, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
elem, Content, NativeElement, Packed, Resolve, Show, ShowSet, Smart, StyleChain,
|
||||
Styles, Synthesize, TargetElem,
|
||||
};
|
||||
use crate::html::{tag, HtmlElem};
|
||||
use crate::html::{attr, tag, HtmlElem};
|
||||
use crate::introspection::{
|
||||
Count, Counter, CounterUpdate, Locatable, Locator, LocatorLink,
|
||||
};
|
||||
@ -272,9 +273,26 @@ impl Show for Packed<HeadingElem> {
|
||||
// Meanwhile, a level 1 Typst heading is a section heading. For this
|
||||
// reason, levels are offset by one: A Typst level 1 heading becomes
|
||||
// a `<h2>`.
|
||||
let level = self.resolve_level(styles);
|
||||
let t = [tag::h2, tag::h3, tag::h4, tag::h5, tag::h6][level.get().min(5) - 1];
|
||||
HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span)
|
||||
let level = self.resolve_level(styles).get();
|
||||
if level >= 6 {
|
||||
engine.sink.warn(warning!(span,
|
||||
"heading of level {} was transformed to \
|
||||
<div role=\"heading\" aria-level=\"{}\">, which is not \
|
||||
supported by all assistive technology",
|
||||
level, level + 1;
|
||||
hint: "HTML only supports <h1> to <h6>, not <h{}>", level + 1;
|
||||
hint: "you may want to restructure your document so that \
|
||||
it doesn't contain deep headings"));
|
||||
HtmlElem::new(tag::div)
|
||||
.with_body(Some(realized))
|
||||
.with_attr(attr::role, "heading")
|
||||
.with_attr(attr::aria_level, eco_format!("{}", level + 1))
|
||||
.pack()
|
||||
.spanned(span)
|
||||
} else {
|
||||
let t = [tag::h2, tag::h3, tag::h4, tag::h5, tag::h6][level - 1];
|
||||
HtmlElem::new(t).with_body(Some(realized)).pack().spanned(span)
|
||||
}
|
||||
} else {
|
||||
let realized = BlockBody::Content(realized);
|
||||
BlockElem::new().with_body(Some(realized)).pack().spanned(span)
|
||||
|
@ -60,8 +60,9 @@ pub fn numbering(
|
||||
/// Defines how the numbering works.
|
||||
///
|
||||
/// **Counting symbols** are `1`, `a`, `A`, `i`, `I`, `α`, `Α`, `一`, `壹`,
|
||||
/// `あ`, `い`, `ア`, `イ`, `א`, `가`, `ㄱ`, `*`, `①`, and `⓵`. They are
|
||||
/// replaced by the number in the sequence, preserving the original case.
|
||||
/// `あ`, `い`, `ア`, `イ`, `א`, `가`, `ㄱ`, `*`, `١`, `۱`, `१`, `১`, `ক`,
|
||||
/// `①`, and `⓵`. They are replaced by the number in the sequence,
|
||||
/// preserving the original case.
|
||||
///
|
||||
/// The `*` character means that symbols should be used to count, in the
|
||||
/// order of `*`, `†`, `‡`, `§`, `¶`, `‖`. If there are more than six
|
||||
|
@ -137,10 +137,6 @@ pub struct OutlineElem {
|
||||
/// `{n => n * 2em}` would be equivalent to just specifying `{2em}`, while
|
||||
/// `{n => [→ ] * n}` would indent with one arrow per nesting level.
|
||||
///
|
||||
/// *Migration hints:* Specifying `{true}` (equivalent to `{auto}`) or
|
||||
/// `{false}` (equivalent to `{none}`) for this option is deprecated and
|
||||
/// will be removed in a future release.
|
||||
///
|
||||
/// ```example
|
||||
/// #set heading(numbering: "1.a.")
|
||||
///
|
||||
@ -294,7 +290,6 @@ pub trait Outlinable: Refable {
|
||||
/// Defines how an outline is indented.
|
||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||
pub enum OutlineIndent {
|
||||
Bool(bool),
|
||||
Rel(Rel<Length>),
|
||||
Func(Func),
|
||||
}
|
||||
@ -310,10 +305,10 @@ impl OutlineIndent {
|
||||
) -> SourceResult<()> {
|
||||
match indent {
|
||||
// 'none' | 'false' => no indenting
|
||||
None | Some(Smart::Custom(OutlineIndent::Bool(false))) => {}
|
||||
None => {}
|
||||
|
||||
// 'auto' | 'true' => use numbering alignment for indenting
|
||||
Some(Smart::Auto | Smart::Custom(OutlineIndent::Bool(true))) => {
|
||||
Some(Smart::Auto) => {
|
||||
// Add hidden ancestors numberings to realize the indent.
|
||||
let mut hidden = Content::empty();
|
||||
for ancestor in ancestors {
|
||||
@ -368,11 +363,9 @@ impl OutlineIndent {
|
||||
cast! {
|
||||
OutlineIndent,
|
||||
self => match self {
|
||||
Self::Bool(v) => v.into_value(),
|
||||
Self::Rel(v) => v.into_value(),
|
||||
Self::Func(v) => v.into_value()
|
||||
},
|
||||
v: bool => OutlineIndent::Bool(v),
|
||||
v: Rel<Length> => OutlineIndent::Rel(v),
|
||||
v: Func => OutlineIndent::Func(v),
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ pub use self::smartquote::*;
|
||||
pub use self::space::*;
|
||||
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
use std::hash::Hash;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use ecow::{eco_format, EcoString};
|
||||
@ -39,13 +40,14 @@ use rustybuzz::Feature;
|
||||
use smallvec::SmallVec;
|
||||
use ttf_parser::Tag;
|
||||
use typst_syntax::Spanned;
|
||||
use typst_utils::singleton;
|
||||
|
||||
use crate::diag::{bail, warning, HintedStrResult, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
cast, category, dict, elem, Args, Array, Cast, Category, Construct, Content, Dict,
|
||||
Fold, IntoValue, NativeElement, Never, NoneValue, Packed, PlainText, Repr, Resolve,
|
||||
Scope, Set, Smart, StyleChain,
|
||||
Fold, IntoValue, NativeElement, Never, NoneValue, Packed, PlainText, Regex, Repr,
|
||||
Resolve, Scope, Set, Smart, StyleChain,
|
||||
};
|
||||
use crate::layout::{Abs, Axis, Dir, Em, Length, Ratio, Rel};
|
||||
use crate::model::ParElem;
|
||||
@ -94,7 +96,21 @@ pub(super) fn define(global: &mut Scope) {
|
||||
/// ```
|
||||
#[elem(Debug, Construct, PlainText, Repr)]
|
||||
pub struct TextElem {
|
||||
/// A font family name or priority list of font family names.
|
||||
/// A font family descriptor or priority list of font family descriptor.
|
||||
///
|
||||
/// A font family descriptor can be a plain string representing the family
|
||||
/// name or a dictionary with the following keys:
|
||||
///
|
||||
/// - `name` (required): The font family name.
|
||||
/// - `covers` (optional): Defines the Unicode codepoints for which the
|
||||
/// family shall be used. This can be:
|
||||
/// - A predefined coverage set:
|
||||
/// - `{"latin-in-cjk"}` covers all codepoints except for those which
|
||||
/// exist in Latin fonts, but should preferrably be taken from CJK
|
||||
/// fonts.
|
||||
/// - A [regular expression]($regex) that defines exactly which codepoints
|
||||
/// shall be covered. Accepts only the subset of regular expressions
|
||||
/// which consist of exactly one dot, letter, or character class.
|
||||
///
|
||||
/// When processing text, Typst tries all specified font families in order
|
||||
/// until it finds a font that has the necessary glyphs. In the example
|
||||
@ -129,6 +145,21 @@ pub struct TextElem {
|
||||
///
|
||||
/// This is Latin. \
|
||||
/// هذا عربي.
|
||||
///
|
||||
/// // Change font only for numbers.
|
||||
/// #set text(font: (
|
||||
/// (name: "PT Sans", covers: regex("[0-9]")),
|
||||
/// "Libertinus Serif"
|
||||
/// ))
|
||||
///
|
||||
/// The number 123.
|
||||
///
|
||||
/// // Mix Latin and CJK fonts.
|
||||
/// #set text(font: (
|
||||
/// (name: "Inria Serif", covers: "latin-in-cjk"),
|
||||
/// "Noto Serif CJK SC"
|
||||
/// ))
|
||||
/// 分别设置“中文”和English字体
|
||||
/// ```
|
||||
#[parse({
|
||||
let font_list: Option<Spanned<FontList>> = args.named("font")?;
|
||||
@ -249,7 +280,7 @@ pub struct TextElem {
|
||||
if paint.v.relative() == Smart::Custom(RelativeTo::Self_) {
|
||||
bail!(
|
||||
paint.span,
|
||||
"gradients and patterns on text must be relative to the parent";
|
||||
"gradients and tilings on text must be relative to the parent";
|
||||
hint: "make sure to set `relative: auto` on your text fill"
|
||||
);
|
||||
}
|
||||
@ -766,35 +797,107 @@ impl PlainText for Packed<TextElem> {
|
||||
}
|
||||
|
||||
/// A lowercased font family like "arial".
|
||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||
pub struct FontFamily(EcoString);
|
||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||
pub struct FontFamily {
|
||||
// The name of the font family
|
||||
name: EcoString,
|
||||
// A regex that defines the Unicode codepoints supported by the font.
|
||||
covers: Option<Covers>,
|
||||
}
|
||||
|
||||
impl FontFamily {
|
||||
/// Create a named font family variant.
|
||||
pub fn new(string: &str) -> Self {
|
||||
Self(string.to_lowercase().into())
|
||||
Self::with_coverage(string, None)
|
||||
}
|
||||
|
||||
/// Create a font family by name and optional Unicode coverage.
|
||||
pub fn with_coverage(string: &str, covers: Option<Covers>) -> Self {
|
||||
Self { name: string.to_lowercase().into(), covers }
|
||||
}
|
||||
|
||||
/// The lowercased family name.
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for FontFamily {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
/// The user-set coverage of the font family.
|
||||
pub fn covers(&self) -> Option<&Regex> {
|
||||
self.covers.as_ref().map(|covers| covers.as_regex())
|
||||
}
|
||||
}
|
||||
|
||||
cast! {
|
||||
FontFamily,
|
||||
self => self.0.into_value(),
|
||||
self => self.name.into_value(),
|
||||
string: EcoString => Self::new(&string),
|
||||
mut v: Dict => {
|
||||
let ret = Self::with_coverage(
|
||||
&v.take("name")?.cast::<EcoString>()?,
|
||||
v.take("covers").ok().map(|v| v.cast()).transpose()?
|
||||
);
|
||||
v.finish(&["name", "covers"])?;
|
||||
ret
|
||||
},
|
||||
}
|
||||
|
||||
/// Defines which codepoints a font family will be used for.
|
||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||
pub enum Covers {
|
||||
/// Covers all codepoints except those used both in Latin and CJK fonts.
|
||||
LatinInCjk,
|
||||
/// Covers the set of codepoints for which the regex matches.
|
||||
Regex(Regex),
|
||||
}
|
||||
|
||||
impl Covers {
|
||||
/// Retrieve the regex for the coverage.
|
||||
pub fn as_regex(&self) -> &Regex {
|
||||
match self {
|
||||
Self::LatinInCjk => singleton!(
|
||||
Regex,
|
||||
Regex::new(
|
||||
"[^\u{00B7}\u{2013}\u{2014}\u{2018}\u{2019}\
|
||||
\u{201C}\u{201D}\u{2025}-\u{2027}\u{2E3A}]"
|
||||
)
|
||||
.unwrap()
|
||||
),
|
||||
Self::Regex(regex) => regex,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cast! {
|
||||
Covers,
|
||||
self => match self {
|
||||
Self::LatinInCjk => "latin-in-cjk".into_value(),
|
||||
Self::Regex(regex) => regex.into_value(),
|
||||
},
|
||||
|
||||
/// Covers all codepoints except those used both in Latin and CJK fonts.
|
||||
"latin-in-cjk" => Covers::LatinInCjk,
|
||||
|
||||
regex: Regex => {
|
||||
let ast = regex_syntax::ast::parse::Parser::new().parse(regex.as_str());
|
||||
match ast {
|
||||
Ok(
|
||||
regex_syntax::ast::Ast::ClassBracketed(..)
|
||||
| regex_syntax::ast::Ast::ClassUnicode(..)
|
||||
| regex_syntax::ast::Ast::ClassPerl(..)
|
||||
| regex_syntax::ast::Ast::Dot(..)
|
||||
| regex_syntax::ast::Ast::Literal(..),
|
||||
) => {}
|
||||
_ => bail!(
|
||||
"coverage regex may only use dot, letters, and character classes";
|
||||
hint: "the regex is applied to each letter individually"
|
||||
),
|
||||
}
|
||||
Covers::Regex(regex)
|
||||
},
|
||||
}
|
||||
|
||||
/// Font family fallback list.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
#[derive(Debug, Default, Clone, PartialEq, Hash)]
|
||||
pub struct FontList(pub Vec<FontFamily>);
|
||||
|
||||
impl<'a> IntoIterator for &'a FontList {
|
||||
@ -809,7 +912,7 @@ impl<'a> IntoIterator for &'a FontList {
|
||||
cast! {
|
||||
FontList,
|
||||
self => if self.0.len() == 1 {
|
||||
self.0.into_iter().next().unwrap().0.into_value()
|
||||
self.0.into_iter().next().unwrap().name.into_value()
|
||||
} else {
|
||||
self.0.into_value()
|
||||
},
|
||||
@ -818,20 +921,22 @@ cast! {
|
||||
}
|
||||
|
||||
/// Resolve a prioritized iterator over the font families.
|
||||
pub fn families(styles: StyleChain) -> impl Iterator<Item = &str> + Clone {
|
||||
const FALLBACKS: &[&str] = &[
|
||||
"libertinus serif",
|
||||
"twitter color emoji",
|
||||
"noto color emoji",
|
||||
"apple color emoji",
|
||||
"segoe ui emoji",
|
||||
];
|
||||
|
||||
let tail = if TextElem::fallback_in(styles) { FALLBACKS } else { &[] };
|
||||
TextElem::font_in(styles)
|
||||
pub fn families(styles: StyleChain) -> impl Iterator<Item = &FontFamily> + Clone {
|
||||
let fallbacks = singleton!(Vec<FontFamily>, {
|
||||
[
|
||||
"libertinus serif",
|
||||
"twitter color emoji",
|
||||
"noto color emoji",
|
||||
"apple color emoji",
|
||||
"segoe ui emoji",
|
||||
]
|
||||
.into_iter()
|
||||
.map(|family| family.as_str())
|
||||
.chain(tail.iter().copied())
|
||||
.map(FontFamily::new)
|
||||
.collect()
|
||||
});
|
||||
|
||||
let tail = if TextElem::fallback_in(styles) { fallbacks.as_slice() } else { &[] };
|
||||
TextElem::font_in(styles).into_iter().chain(tail.iter())
|
||||
}
|
||||
|
||||
/// Resolve the font variant.
|
||||
|
@ -157,7 +157,11 @@ fn is_shapable(engine: &Engine, text: &str, styles: StyleChain) -> bool {
|
||||
.select(family.as_str(), variant(styles))
|
||||
.and_then(|id| world.font(id))
|
||||
{
|
||||
return text.chars().all(|c| font.ttf().glyph_index(c).is_some());
|
||||
let covers = family.covers();
|
||||
return text.chars().all(|c| {
|
||||
covers.map_or(true, |cov| cov.is_match(c.encode_utf8(&mut [0; 4])))
|
||||
&& font.ttf().glyph_index(c).is_some()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -211,7 +211,7 @@ impl<'s> SmartQuotes<'s> {
|
||||
/// Swiss / Liechtensteinian German, Estonian, Icelandic, Italian, Latin,
|
||||
/// Lithuanian, Latvian, Slovak, Slovenian, Spanish, Bosnian, Finnish,
|
||||
/// Swedish, French, Swiss French, Hungarian, Polish, Romanian, Japanese,
|
||||
/// Traditional Chinese, Russian, Norwegian, and Hebrew.
|
||||
/// Traditional Chinese, Russian, Norwegian, Hebrew and Croatian.
|
||||
///
|
||||
/// For unknown languages, the English quotes are used as fallback.
|
||||
pub fn get(
|
||||
@ -250,6 +250,7 @@ impl<'s> SmartQuotes<'s> {
|
||||
"ru" | "no" | "nb" | "nn" | "uk" => ("’", "’", "«", "»"),
|
||||
"el" => ("‘", "’", "«", "»"),
|
||||
"he" => ("’", "’", "”", "”"),
|
||||
"hr" => ("‘", "’", "„", "”"),
|
||||
_ if lang.dir() == Dir::RTL => ("’", "‘", "”", "“"),
|
||||
_ => default,
|
||||
};
|
||||
|
@ -697,6 +697,8 @@ impl Gradient {
|
||||
}
|
||||
|
||||
/// Returns the angle of this gradient.
|
||||
///
|
||||
/// Returns `{none}` if the gradient is neither linear nor conic.
|
||||
#[func]
|
||||
pub fn angle(&self) -> Option<Angle> {
|
||||
match self {
|
||||
@ -706,6 +708,54 @@ impl Gradient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the center of this gradient.
|
||||
///
|
||||
/// Returns `{none}` if the gradient is neither radial nor conic.
|
||||
#[func]
|
||||
pub fn center(&self) -> Option<Axes<Ratio>> {
|
||||
match self {
|
||||
Self::Linear(_) => None,
|
||||
Self::Radial(radial) => Some(radial.center),
|
||||
Self::Conic(conic) => Some(conic.center),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the radius of this gradient.
|
||||
///
|
||||
/// Returns `{none}` if the gradient is not radial.
|
||||
#[func]
|
||||
pub fn radius(&self) -> Option<Ratio> {
|
||||
match self {
|
||||
Self::Linear(_) => None,
|
||||
Self::Radial(radial) => Some(radial.radius),
|
||||
Self::Conic(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the focal-center of this gradient.
|
||||
///
|
||||
/// Returns `{none}` if the gradient is not radial.
|
||||
#[func]
|
||||
pub fn focal_center(&self) -> Option<Axes<Ratio>> {
|
||||
match self {
|
||||
Self::Linear(_) => None,
|
||||
Self::Radial(radial) => Some(radial.focal_center),
|
||||
Self::Conic(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the focal-radius of this gradient.
|
||||
///
|
||||
/// Returns `{none}` if the gradient is not radial.
|
||||
#[func]
|
||||
pub fn focal_radius(&self) -> Option<Ratio> {
|
||||
match self {
|
||||
Self::Linear(_) => None,
|
||||
Self::Radial(radial) => Some(radial.focal_radius),
|
||||
Self::Conic(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sample the gradient at a given position.
|
||||
///
|
||||
/// The position is either a position along the gradient (a [ratio] between
|
||||
|
@ -94,6 +94,12 @@ pub struct ImageElem {
|
||||
/// ```
|
||||
#[default(ImageFit::Cover)]
|
||||
pub fit: ImageFit,
|
||||
|
||||
/// Whether text in SVG images should be converted into paths before
|
||||
/// embedding. This will result in the text becoming unselectable in
|
||||
/// the output.
|
||||
#[default(false)]
|
||||
pub flatten_text: bool,
|
||||
}
|
||||
|
||||
#[scope]
|
||||
@ -246,13 +252,14 @@ impl Image {
|
||||
alt: Option<EcoString>,
|
||||
world: Tracked<dyn World + '_>,
|
||||
families: &[&str],
|
||||
flatten_text: bool,
|
||||
) -> StrResult<Image> {
|
||||
let kind = match format {
|
||||
ImageFormat::Raster(format) => {
|
||||
ImageKind::Raster(RasterImage::new(data, format)?)
|
||||
}
|
||||
ImageFormat::Vector(VectorFormat::Svg) => {
|
||||
ImageKind::Svg(SvgImage::with_fonts(data, world, families)?)
|
||||
ImageKind::Svg(SvgImage::with_fonts(data, world, flatten_text, families)?)
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -22,6 +22,7 @@ pub struct SvgImage(Arc<Repr>);
|
||||
struct Repr {
|
||||
data: Bytes,
|
||||
size: Axes<f64>,
|
||||
flatten_text: bool,
|
||||
font_hash: u128,
|
||||
tree: usvg::Tree,
|
||||
}
|
||||
@ -32,7 +33,13 @@ impl SvgImage {
|
||||
pub fn new(data: Bytes) -> StrResult<SvgImage> {
|
||||
let tree =
|
||||
usvg::Tree::from_data(&data, &base_options()).map_err(format_usvg_error)?;
|
||||
Ok(Self(Arc::new(Repr { data, size: tree_size(&tree), font_hash: 0, tree })))
|
||||
Ok(Self(Arc::new(Repr {
|
||||
data,
|
||||
size: tree_size(&tree),
|
||||
font_hash: 0,
|
||||
flatten_text: false,
|
||||
tree,
|
||||
})))
|
||||
}
|
||||
|
||||
/// Decode an SVG image with access to fonts.
|
||||
@ -40,6 +47,7 @@ impl SvgImage {
|
||||
pub fn with_fonts(
|
||||
data: Bytes,
|
||||
world: Tracked<dyn World + '_>,
|
||||
flatten_text: bool,
|
||||
families: &[&str],
|
||||
) -> StrResult<SvgImage> {
|
||||
let book = world.book();
|
||||
@ -60,7 +68,13 @@ impl SvgImage {
|
||||
)
|
||||
.map_err(format_usvg_error)?;
|
||||
let font_hash = resolver.into_inner().unwrap().finish();
|
||||
Ok(Self(Arc::new(Repr { data, size: tree_size(&tree), font_hash, tree })))
|
||||
Ok(Self(Arc::new(Repr {
|
||||
data,
|
||||
size: tree_size(&tree),
|
||||
font_hash,
|
||||
flatten_text,
|
||||
tree,
|
||||
})))
|
||||
}
|
||||
|
||||
/// The raw image data.
|
||||
@ -73,6 +87,11 @@ impl SvgImage {
|
||||
self.0.size.x
|
||||
}
|
||||
|
||||
/// Whether the SVG's text should be flattened.
|
||||
pub fn flatten_text(&self) -> bool {
|
||||
self.0.flatten_text
|
||||
}
|
||||
|
||||
/// The SVG's height in pixels.
|
||||
pub fn height(&self) -> f64 {
|
||||
self.0.size.y
|
||||
|
@ -6,10 +6,10 @@ mod image;
|
||||
mod line;
|
||||
mod paint;
|
||||
mod path;
|
||||
mod pattern;
|
||||
mod polygon;
|
||||
mod shape;
|
||||
mod stroke;
|
||||
mod tiling;
|
||||
|
||||
pub use self::color::*;
|
||||
pub use self::gradient::*;
|
||||
@ -17,12 +17,12 @@ pub use self::image::*;
|
||||
pub use self::line::*;
|
||||
pub use self::paint::*;
|
||||
pub use self::path::*;
|
||||
pub use self::pattern::*;
|
||||
pub use self::polygon::*;
|
||||
pub use self::shape::*;
|
||||
pub use self::stroke::*;
|
||||
pub use self::tiling::*;
|
||||
|
||||
use crate::foundations::{category, Category, Scope};
|
||||
use crate::foundations::{category, Category, Scope, Type};
|
||||
|
||||
/// Drawing and data visualization.
|
||||
///
|
||||
@ -37,7 +37,7 @@ pub(super) fn define(global: &mut Scope) {
|
||||
global.category(VISUALIZE);
|
||||
global.define_type::<Color>();
|
||||
global.define_type::<Gradient>();
|
||||
global.define_type::<Pattern>();
|
||||
global.define_type::<Tiling>();
|
||||
global.define_type::<Stroke>();
|
||||
global.define_elem::<ImageElem>();
|
||||
global.define_elem::<LineElem>();
|
||||
@ -47,4 +47,7 @@ pub(super) fn define(global: &mut Scope) {
|
||||
global.define_elem::<CircleElem>();
|
||||
global.define_elem::<PolygonElem>();
|
||||
global.define_elem::<PathElem>();
|
||||
|
||||
// Compatibility.
|
||||
global.define("pattern", Type::of::<Tiling>());
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ use std::fmt::{self, Debug, Formatter};
|
||||
use ecow::EcoString;
|
||||
|
||||
use crate::foundations::{cast, Repr, Smart};
|
||||
use crate::visualize::{Color, Gradient, Pattern, RelativeTo};
|
||||
use crate::visualize::{Color, Gradient, RelativeTo, Tiling};
|
||||
|
||||
/// How a fill or stroke should be painted.
|
||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||
@ -12,8 +12,8 @@ pub enum Paint {
|
||||
Solid(Color),
|
||||
/// A gradient.
|
||||
Gradient(Gradient),
|
||||
/// A pattern.
|
||||
Pattern(Pattern),
|
||||
/// A tiling.
|
||||
Tiling(Tiling),
|
||||
}
|
||||
|
||||
impl Paint {
|
||||
@ -21,7 +21,7 @@ impl Paint {
|
||||
pub fn unwrap_solid(&self) -> Color {
|
||||
match self {
|
||||
Self::Solid(color) => *color,
|
||||
Self::Gradient(_) | Self::Pattern(_) => panic!("expected solid color"),
|
||||
Self::Gradient(_) | Self::Tiling(_) => panic!("expected solid color"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,7 +30,7 @@ impl Paint {
|
||||
match self {
|
||||
Self::Solid(_) => Smart::Auto,
|
||||
Self::Gradient(gradient) => gradient.relative(),
|
||||
Self::Pattern(pattern) => pattern.relative(),
|
||||
Self::Tiling(tiling) => tiling.relative(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,8 +44,8 @@ impl Paint {
|
||||
Self::Gradient(gradient) => {
|
||||
Self::Gradient(gradient.clone().with_relative(RelativeTo::Parent))
|
||||
}
|
||||
Self::Pattern(pattern) => {
|
||||
Self::Pattern(pattern.clone().with_relative(RelativeTo::Parent))
|
||||
Self::Tiling(tiling) => {
|
||||
Self::Tiling(tiling.clone().with_relative(RelativeTo::Parent))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -56,14 +56,14 @@ impl Debug for Paint {
|
||||
match self {
|
||||
Self::Solid(v) => v.fmt(f),
|
||||
Self::Gradient(v) => v.fmt(f),
|
||||
Self::Pattern(v) => v.fmt(f),
|
||||
Self::Tiling(v) => v.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Pattern> for Paint {
|
||||
fn from(pattern: Pattern) -> Self {
|
||||
Self::Pattern(pattern)
|
||||
impl From<Tiling> for Paint {
|
||||
fn from(tiling: Tiling) -> Self {
|
||||
Self::Tiling(tiling)
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,7 +72,7 @@ impl Repr for Paint {
|
||||
match self {
|
||||
Self::Solid(color) => color.repr(),
|
||||
Self::Gradient(gradient) => gradient.repr(),
|
||||
Self::Pattern(pattern) => pattern.repr(),
|
||||
Self::Tiling(tiling) => tiling.repr(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -94,9 +94,9 @@ cast! {
|
||||
self => match self {
|
||||
Self::Solid(color) => color.into_value(),
|
||||
Self::Gradient(gradient) => gradient.into_value(),
|
||||
Self::Pattern(pattern) => pattern.into_value(),
|
||||
Self::Tiling(tiling) => tiling.into_value(),
|
||||
},
|
||||
color: Color => Self::Solid(color),
|
||||
gradient: Gradient => Self::Gradient(gradient),
|
||||
pattern: Pattern => Self::Pattern(pattern),
|
||||
tiling: Tiling => Self::Tiling(tiling),
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ use crate::foundations::{
|
||||
Resolve, Smart, StyleChain, Value,
|
||||
};
|
||||
use crate::layout::{Abs, Length};
|
||||
use crate::visualize::{Color, Gradient, Paint, Pattern};
|
||||
use crate::visualize::{Color, Gradient, Paint, Tiling};
|
||||
|
||||
/// Defines how to draw a line.
|
||||
///
|
||||
@ -213,9 +213,9 @@ impl<T: Numeric> Stroke<T> {
|
||||
thickness: self.thickness.map(&f),
|
||||
cap: self.cap,
|
||||
join: self.join,
|
||||
dash: self.dash.map(|pattern| {
|
||||
pattern.map(|pattern| DashPattern {
|
||||
array: pattern
|
||||
dash: self.dash.map(|dash| {
|
||||
dash.map(|dash| DashPattern {
|
||||
array: dash
|
||||
.array
|
||||
.into_iter()
|
||||
.map(|l| match l {
|
||||
@ -223,7 +223,7 @@ impl<T: Numeric> Stroke<T> {
|
||||
DashLength::LineWidth => DashLength::LineWidth,
|
||||
})
|
||||
.collect(),
|
||||
phase: f(pattern.phase),
|
||||
phase: f(dash.phase),
|
||||
})
|
||||
}),
|
||||
miter_limit: self.miter_limit,
|
||||
@ -237,14 +237,10 @@ impl Stroke<Abs> {
|
||||
let thickness = self.thickness.unwrap_or(default.thickness);
|
||||
let dash = self
|
||||
.dash
|
||||
.map(|pattern| {
|
||||
pattern.map(|pattern| DashPattern {
|
||||
array: pattern
|
||||
.array
|
||||
.into_iter()
|
||||
.map(|l| l.finish(thickness))
|
||||
.collect(),
|
||||
phase: pattern.phase,
|
||||
.map(|dash| {
|
||||
dash.map(|dash| DashPattern {
|
||||
array: dash.array.into_iter().map(|l| l.finish(thickness)).collect(),
|
||||
phase: dash.phase,
|
||||
})
|
||||
})
|
||||
.unwrap_or(default.dash);
|
||||
@ -372,8 +368,8 @@ cast! {
|
||||
paint: Smart::Custom(gradient.into()),
|
||||
..Default::default()
|
||||
},
|
||||
pattern: Pattern => Self {
|
||||
paint: Smart::Custom(pattern.into()),
|
||||
tiling: Tiling => Self {
|
||||
paint: Smart::Custom(tiling.into()),
|
||||
..Default::default()
|
||||
},
|
||||
mut dict: Dict => {
|
||||
|
@ -13,18 +13,18 @@ use crate::layout::{Abs, Axes, Frame, Length, Region, Size};
|
||||
use crate::visualize::RelativeTo;
|
||||
use crate::World;
|
||||
|
||||
/// A repeating pattern fill.
|
||||
/// A repeating tiling fill.
|
||||
///
|
||||
/// Typst supports the most common pattern type of tiled patterns, where a
|
||||
/// pattern is repeated in a grid-like fashion, covering the entire area of an
|
||||
/// element that is filled or stroked. The pattern is defined by a tile size and
|
||||
/// a body defining the content of each cell. You can also add horizontal or
|
||||
/// vertical spacing between the cells of the pattern.
|
||||
/// Typst supports the most common type of tilings, where a pattern is repeated
|
||||
/// in a grid-like fashion, covering the entire area of an element that is
|
||||
/// filled or stroked. The pattern is defined by a tile size and a body defining
|
||||
/// the content of each cell. You can also add horizontal or vertical spacing
|
||||
/// between the cells of the tiling.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```example
|
||||
/// #let pat = pattern(size: (30pt, 30pt))[
|
||||
/// #let pat = tiling(size: (30pt, 30pt))[
|
||||
/// #place(line(start: (0%, 0%), end: (100%, 100%)))
|
||||
/// #place(line(start: (0%, 100%), end: (100%, 0%)))
|
||||
/// ]
|
||||
@ -32,14 +32,14 @@ use crate::World;
|
||||
/// #rect(fill: pat, width: 100%, height: 60pt, stroke: 1pt)
|
||||
/// ```
|
||||
///
|
||||
/// Patterns are also supported on text, but only when setting the
|
||||
/// [relativeness]($pattern.relative) to either `{auto}` (the default value) or
|
||||
/// `{"parent"}`. To create word-by-word or glyph-by-glyph patterns, you can
|
||||
/// Tilings are also supported on text, but only when setting the
|
||||
/// [relativeness]($tiling.relative) to either `{auto}` (the default value) or
|
||||
/// `{"parent"}`. To create word-by-word or glyph-by-glyph tilings, you can
|
||||
/// wrap the words or characters of your text in [boxes]($box) manually or
|
||||
/// through a [show rule]($styling/#show-rules).
|
||||
///
|
||||
/// ```example
|
||||
/// #let pat = pattern(
|
||||
/// #let pat = tiling(
|
||||
/// size: (30pt, 30pt),
|
||||
/// relative: "parent",
|
||||
/// square(
|
||||
@ -54,13 +54,13 @@ use crate::World;
|
||||
/// ```
|
||||
///
|
||||
/// You can also space the elements further or closer apart using the
|
||||
/// [`spacing`]($pattern.spacing) feature of the pattern. If the spacing
|
||||
/// is lower than the size of the pattern, the pattern will overlap.
|
||||
/// If it is higher, the pattern will have gaps of the same color as the
|
||||
/// background of the pattern.
|
||||
/// [`spacing`]($tiling.spacing) feature of the tiling. If the spacing
|
||||
/// is lower than the size of the tiling, the tiling will overlap.
|
||||
/// If it is higher, the tiling will have gaps of the same color as the
|
||||
/// background of the tiling.
|
||||
///
|
||||
/// ```example
|
||||
/// #let pat = pattern(
|
||||
/// #let pat = tiling(
|
||||
/// size: (30pt, 30pt),
|
||||
/// spacing: (10pt, 10pt),
|
||||
/// relative: "parent",
|
||||
@ -79,11 +79,11 @@ use crate::World;
|
||||
/// ```
|
||||
///
|
||||
/// # Relativeness
|
||||
/// The location of the starting point of the pattern is dependent on the
|
||||
/// The location of the starting point of the tiling is dependent on the
|
||||
/// dimensions of a container. This container can either be the shape that it is
|
||||
/// being painted on, or the closest surrounding container. This is controlled
|
||||
/// by the `relative` argument of a pattern constructor. By default, patterns
|
||||
/// are relative to the shape they are being painted on, unless the pattern is
|
||||
/// by the `relative` argument of a tiling constructor. By default, tilings
|
||||
/// are relative to the shape they are being painted on, unless the tiling is
|
||||
/// applied on text, in which case they are relative to the closest ancestor
|
||||
/// container.
|
||||
///
|
||||
@ -94,29 +94,33 @@ use crate::World;
|
||||
/// contains the shape. This includes the boxes and blocks that are implicitly
|
||||
/// created by show rules and elements. For example, a [`rotate`] will not
|
||||
/// affect the parent of a gradient, but a [`grid`] will.
|
||||
#[ty(scope, cast)]
|
||||
///
|
||||
/// # Compatibility
|
||||
/// This type used to be called `pattern`. The name remains as an alias, but is
|
||||
/// deprecated since Typst 0.13.
|
||||
#[ty(scope, cast, keywords = ["pattern"])]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Pattern(Arc<Repr>);
|
||||
pub struct Tiling(Arc<Repr>);
|
||||
|
||||
/// Internal representation of [`Pattern`].
|
||||
/// Internal representation of [`Tiling`].
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
struct Repr {
|
||||
/// The pattern's rendered content.
|
||||
/// The tiling's rendered content.
|
||||
frame: LazyHash<Frame>,
|
||||
/// The pattern's tile size.
|
||||
/// The tiling's tile size.
|
||||
size: Size,
|
||||
/// The pattern's tile spacing.
|
||||
/// The tiling's tile spacing.
|
||||
spacing: Size,
|
||||
/// The pattern's relative transform.
|
||||
/// The tiling's relative transform.
|
||||
relative: Smart<RelativeTo>,
|
||||
}
|
||||
|
||||
#[scope]
|
||||
impl Pattern {
|
||||
/// Construct a new pattern.
|
||||
impl Tiling {
|
||||
/// Construct a new tiling.
|
||||
///
|
||||
/// ```example
|
||||
/// #let pat = pattern(
|
||||
/// #let pat = tiling(
|
||||
/// size: (20pt, 20pt),
|
||||
/// relative: "parent",
|
||||
/// place(
|
||||
@ -136,15 +140,15 @@ impl Pattern {
|
||||
engine: &mut Engine,
|
||||
/// The callsite span.
|
||||
span: Span,
|
||||
/// The bounding box of each cell of the pattern.
|
||||
/// The bounding box of each cell of the tiling.
|
||||
#[named]
|
||||
#[default(Spanned::new(Smart::Auto, Span::detached()))]
|
||||
size: Spanned<Smart<Axes<Length>>>,
|
||||
/// The spacing between cells of the pattern.
|
||||
/// The spacing between cells of the tiling.
|
||||
#[named]
|
||||
#[default(Spanned::new(Axes::splat(Length::zero()), Span::detached()))]
|
||||
spacing: Spanned<Axes<Length>>,
|
||||
/// The [relative placement](#relativeness) of the pattern.
|
||||
/// The [relative placement](#relativeness) of the tiling.
|
||||
///
|
||||
/// For an element placed at the root/top level of the document, the
|
||||
/// parent is the page itself. For other elements, the parent is the
|
||||
@ -153,14 +157,14 @@ impl Pattern {
|
||||
#[named]
|
||||
#[default(Smart::Auto)]
|
||||
relative: Smart<RelativeTo>,
|
||||
/// The content of each cell of the pattern.
|
||||
/// The content of each cell of the tiling.
|
||||
body: Content,
|
||||
) -> SourceResult<Pattern> {
|
||||
) -> SourceResult<Tiling> {
|
||||
let size_span = size.span;
|
||||
if let Smart::Custom(size) = size.v {
|
||||
// Ensure that sizes are absolute.
|
||||
if !size.x.em.is_zero() || !size.y.em.is_zero() {
|
||||
bail!(size_span, "pattern tile size must be absolute");
|
||||
bail!(size_span, "tile size must be absolute");
|
||||
}
|
||||
|
||||
// Ensure that sizes are non-zero and finite.
|
||||
@ -169,25 +173,25 @@ impl Pattern {
|
||||
|| !size.x.is_finite()
|
||||
|| !size.y.is_finite()
|
||||
{
|
||||
bail!(size_span, "pattern tile size must be non-zero and non-infinite");
|
||||
bail!(size_span, "tile size must be non-zero and non-infinite");
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that spacing is absolute.
|
||||
if !spacing.v.x.em.is_zero() || !spacing.v.y.em.is_zero() {
|
||||
bail!(spacing.span, "pattern tile spacing must be absolute");
|
||||
bail!(spacing.span, "tile spacing must be absolute");
|
||||
}
|
||||
|
||||
// Ensure that spacing is finite.
|
||||
if !spacing.v.x.is_finite() || !spacing.v.y.is_finite() {
|
||||
bail!(spacing.span, "pattern tile spacing must be finite");
|
||||
bail!(spacing.span, "tile spacing must be finite");
|
||||
}
|
||||
|
||||
// The size of the frame
|
||||
let size = size.v.map(|l| l.map(|a| a.abs));
|
||||
let region = size.unwrap_or_else(|| Axes::splat(Abs::inf()));
|
||||
|
||||
// Layout the pattern.
|
||||
// Layout the tiling.
|
||||
let world = engine.world;
|
||||
let library = world.library();
|
||||
let locator = Locator::root();
|
||||
@ -204,7 +208,7 @@ impl Pattern {
|
||||
// Check that the frame is non-zero.
|
||||
if frame.width().is_zero() || frame.height().is_zero() {
|
||||
bail!(
|
||||
span, "pattern tile size must be non-zero";
|
||||
span, "tile size must be non-zero";
|
||||
hint: "try setting the size manually"
|
||||
);
|
||||
}
|
||||
@ -218,8 +222,8 @@ impl Pattern {
|
||||
}
|
||||
}
|
||||
|
||||
impl Pattern {
|
||||
/// Set the relative placement of the pattern.
|
||||
impl Tiling {
|
||||
/// Set the relative placement of the tiling.
|
||||
pub fn with_relative(mut self, relative: RelativeTo) -> Self {
|
||||
if let Some(this) = Arc::get_mut(&mut self.0) {
|
||||
this.relative = Smart::Custom(relative);
|
||||
@ -233,27 +237,27 @@ impl Pattern {
|
||||
self
|
||||
}
|
||||
|
||||
/// Return the frame of the pattern.
|
||||
/// Return the frame of the tiling.
|
||||
pub fn frame(&self) -> &Frame {
|
||||
&self.0.frame
|
||||
}
|
||||
|
||||
/// Return the size of the pattern in absolute units.
|
||||
/// Return the size of the tiling in absolute units.
|
||||
pub fn size(&self) -> Size {
|
||||
self.0.size
|
||||
}
|
||||
|
||||
/// Return the spacing of the pattern in absolute units.
|
||||
/// Return the spacing of the tiling in absolute units.
|
||||
pub fn spacing(&self) -> Size {
|
||||
self.0.spacing
|
||||
}
|
||||
|
||||
/// Returns the relative placement of the pattern.
|
||||
/// Returns the relative placement of the tiling.
|
||||
pub fn relative(&self) -> Smart<RelativeTo> {
|
||||
self.0.relative
|
||||
}
|
||||
|
||||
/// Returns the relative placement of the pattern.
|
||||
/// Returns the relative placement of the tiling.
|
||||
pub fn unwrap_relative(&self, on_text: bool) -> RelativeTo {
|
||||
self.0.relative.unwrap_or_else(|| {
|
||||
if on_text {
|
||||
@ -265,10 +269,10 @@ impl Pattern {
|
||||
}
|
||||
}
|
||||
|
||||
impl repr::Repr for Pattern {
|
||||
impl repr::Repr for Tiling {
|
||||
fn repr(&self) -> EcoString {
|
||||
let mut out =
|
||||
eco_format!("pattern(({}, {})", self.0.size.x.repr(), self.0.size.y.repr());
|
||||
eco_format!("tiling(({}, {})", self.0.size.x.repr(), self.0.size.y.repr());
|
||||
|
||||
if self.0.spacing.is_zero() {
|
||||
out.push_str(", spacing: (");
|
@ -280,7 +280,7 @@ pub fn category(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream
|
||||
/// - `Reflect` makes Typst's runtime aware of the type's characteristics.
|
||||
/// It's important for autocompletion, error messages, etc.
|
||||
/// - `FromValue` defines how to cast from a value into this type.
|
||||
/// - `IntoValue` defines how to cast fromthis type into a value.
|
||||
/// - `IntoValue` defines how to cast from this type into a value.
|
||||
///
|
||||
/// ```ignore
|
||||
/// /// An integer between 0 and 13.
|
||||
|
@ -9,10 +9,10 @@ use typst_library::foundations::{Datetime, Smart};
|
||||
use typst_library::layout::Dir;
|
||||
use typst_library::text::Lang;
|
||||
use typst_syntax::Span;
|
||||
use xmp_writer::{DateTime, LangId, RenditionClass, Timezone, XmpWriter};
|
||||
use xmp_writer::{DateTime, LangId, RenditionClass, XmpWriter};
|
||||
|
||||
use crate::page::PdfPageLabel;
|
||||
use crate::{hash_base64, outline, TextStrExt, WithEverything};
|
||||
use crate::{hash_base64, outline, TextStrExt, Timezone, WithEverything};
|
||||
|
||||
/// Write the document catalog.
|
||||
pub fn write_catalog(
|
||||
@ -87,8 +87,17 @@ pub fn write_catalog(
|
||||
xmp.pdf_keywords(&joined);
|
||||
}
|
||||
|
||||
let date = ctx.document.info.date.unwrap_or(ctx.options.timestamp);
|
||||
let tz = ctx.document.info.date.is_auto();
|
||||
// (1) If the `document.date` is set to specific `datetime` or `none`, use it.
|
||||
// (2) If the `document.date` is set to `auto` or not set, try to use the
|
||||
// date from the options.
|
||||
// (3) Otherwise, we don't write date metadata.
|
||||
let (date, tz) = match (ctx.document.info.date, ctx.options.timestamp) {
|
||||
(Smart::Custom(date), _) => (date, None),
|
||||
(Smart::Auto, Some(timestamp)) => {
|
||||
(Some(timestamp.datetime), Some(timestamp.timezone))
|
||||
}
|
||||
_ => (None, None),
|
||||
};
|
||||
if let Some(date) = date {
|
||||
if let Some(pdf_date) = pdf_date(date, tz) {
|
||||
info.creation_date(pdf_date);
|
||||
@ -281,7 +290,7 @@ pub(crate) fn write_page_labels(
|
||||
}
|
||||
|
||||
/// Converts a datetime to a pdf-writer date.
|
||||
fn pdf_date(datetime: Datetime, tz: bool) -> Option<pdf_writer::Date> {
|
||||
fn pdf_date(datetime: Datetime, tz: Option<Timezone>) -> Option<pdf_writer::Date> {
|
||||
let year = datetime.year().filter(|&y| y >= 0)? as u16;
|
||||
|
||||
let mut pdf_date = pdf_writer::Date::new(year);
|
||||
@ -306,16 +315,36 @@ fn pdf_date(datetime: Datetime, tz: bool) -> Option<pdf_writer::Date> {
|
||||
pdf_date = pdf_date.second(s);
|
||||
}
|
||||
|
||||
if tz {
|
||||
pdf_date = pdf_date.utc_offset_hour(0).utc_offset_minute(0);
|
||||
match tz {
|
||||
Some(Timezone::UTC) => {
|
||||
pdf_date = pdf_date.utc_offset_hour(0).utc_offset_minute(0)
|
||||
}
|
||||
Some(Timezone::Local { hour_offset, minute_offset }) => {
|
||||
pdf_date =
|
||||
pdf_date.utc_offset_hour(hour_offset).utc_offset_minute(minute_offset)
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
Some(pdf_date)
|
||||
}
|
||||
|
||||
/// Converts a datetime to an xmp-writer datetime.
|
||||
fn xmp_date(datetime: Datetime, tz: bool) -> Option<xmp_writer::DateTime> {
|
||||
fn xmp_date(
|
||||
datetime: Datetime,
|
||||
timezone: Option<Timezone>,
|
||||
) -> Option<xmp_writer::DateTime> {
|
||||
let year = datetime.year().filter(|&y| y >= 0)? as u16;
|
||||
let timezone = timezone.map(|tz| match tz {
|
||||
Timezone::UTC => xmp_writer::Timezone::Utc,
|
||||
Timezone::Local { hour_offset, minute_offset } => {
|
||||
// The xmp-writer use signed integers for the minute offset, which
|
||||
// can be buggy if the minute offset is negative. And because our
|
||||
// minute_offset is ensured to be `0 <= minute_offset < 60`, we can
|
||||
// safely cast it to a signed integer.
|
||||
xmp_writer::Timezone::Local { hour: hour_offset, minute: minute_offset as i8 }
|
||||
}
|
||||
});
|
||||
Some(DateTime {
|
||||
year,
|
||||
month: datetime.month(),
|
||||
@ -323,6 +352,6 @@ fn xmp_date(datetime: Datetime, tz: bool) -> Option<xmp_writer::DateTime> {
|
||||
hour: datetime.hour(),
|
||||
minute: datetime.minute(),
|
||||
second: datetime.second(),
|
||||
timezone: if tz { Some(Timezone::Utc) } else { None },
|
||||
timezone,
|
||||
})
|
||||
}
|
||||
|
@ -222,7 +222,7 @@ impl PaintEncode for Paint {
|
||||
match self {
|
||||
Self::Solid(c) => c.set_as_fill(ctx, on_text, transforms),
|
||||
Self::Gradient(gradient) => gradient.set_as_fill(ctx, on_text, transforms),
|
||||
Self::Pattern(pattern) => pattern.set_as_fill(ctx, on_text, transforms),
|
||||
Self::Tiling(tiling) => tiling.set_as_fill(ctx, on_text, transforms),
|
||||
}
|
||||
}
|
||||
|
||||
@ -235,7 +235,7 @@ impl PaintEncode for Paint {
|
||||
match self {
|
||||
Self::Solid(c) => c.set_as_stroke(ctx, on_text, transforms),
|
||||
Self::Gradient(gradient) => gradient.set_as_stroke(ctx, on_text, transforms),
|
||||
Self::Pattern(pattern) => pattern.set_as_stroke(ctx, on_text, transforms),
|
||||
Self::Tiling(tiling) => tiling.set_as_stroke(ctx, on_text, transforms),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
//! Generic writer for PDF content.
|
||||
//!
|
||||
//! It is used to write page contents, color glyph instructions, and patterns.
|
||||
//! It is used to write page contents, color glyph instructions, and tilings.
|
||||
//!
|
||||
//! See also [`pdf_writer::Content`].
|
||||
|
||||
@ -96,7 +96,7 @@ pub struct Encoded {
|
||||
/// objects only through resources.
|
||||
///
|
||||
/// Content streams can be used for page contents, but also to describe color
|
||||
/// glyphs and patterns.
|
||||
/// glyphs and tilings.
|
||||
pub struct Builder<'a, R = ()> {
|
||||
/// Settings for PDF export.
|
||||
pub(crate) options: &'a PdfOptions<'a>,
|
||||
@ -187,7 +187,7 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
/// Subset of the state used to calculate the transform of gradients and patterns.
|
||||
/// Subset of the state used to calculate the transform of gradients and tilings.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(super) struct Transforms {
|
||||
/// The transform of the current item.
|
||||
@ -229,7 +229,7 @@ impl Builder<'_, ()> {
|
||||
let get_opacity = |paint: &Paint| {
|
||||
let color = match paint {
|
||||
Paint::Solid(color) => *color,
|
||||
Paint::Gradient(_) | Paint::Pattern(_) => return 255,
|
||||
Paint::Gradient(_) | Paint::Tiling(_) => return 255,
|
||||
};
|
||||
|
||||
color.alpha().map_or(255, |v| (v * 255.0).round() as u8)
|
||||
@ -330,10 +330,10 @@ impl Builder<'_, ()> {
|
||||
self.content.set_line_join(to_pdf_line_join(*join));
|
||||
}
|
||||
if self.state.stroke.as_ref().map(|s| &s.dash) != Some(dash) {
|
||||
if let Some(pattern) = dash {
|
||||
if let Some(dash) = dash {
|
||||
self.content.set_dash_pattern(
|
||||
pattern.array.iter().map(|l| l.to_f32()),
|
||||
pattern.phase.to_f32(),
|
||||
dash.array.iter().map(|l| l.to_f32()),
|
||||
dash.phase.to_f32(),
|
||||
);
|
||||
} else {
|
||||
self.content.set_dash_pattern([], 0.0);
|
||||
|
@ -208,7 +208,11 @@ fn encode_svg(
|
||||
) -> Result<(Chunk, Ref), svg2pdf::ConversionError> {
|
||||
svg2pdf::to_chunk(
|
||||
svg.tree(),
|
||||
svg2pdf::ConversionOptions { pdfa, ..Default::default() },
|
||||
svg2pdf::ConversionOptions {
|
||||
pdfa,
|
||||
embed_text: !svg.flatten_text(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -11,8 +11,8 @@ mod image;
|
||||
mod named_destination;
|
||||
mod outline;
|
||||
mod page;
|
||||
mod pattern;
|
||||
mod resources;
|
||||
mod tiling;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
@ -39,10 +39,10 @@ use crate::gradient::{write_gradients, PdfGradient};
|
||||
use crate::image::write_images;
|
||||
use crate::named_destination::{write_named_destinations, NamedDestinations};
|
||||
use crate::page::{alloc_page_refs, traverse_pages, write_page_tree, EncodedPage};
|
||||
use crate::pattern::{write_patterns, PdfPattern};
|
||||
use crate::resources::{
|
||||
alloc_resources_refs, write_resource_dictionaries, Resources, ResourcesRefs,
|
||||
};
|
||||
use crate::tiling::{write_tilings, PdfTiling};
|
||||
|
||||
/// Export a document into a PDF file.
|
||||
///
|
||||
@ -65,7 +65,7 @@ pub fn pdf(document: &PagedDocument, options: &PdfOptions) -> SourceResult<Vec<u
|
||||
color_fonts: builder.run(write_color_fonts)?,
|
||||
images: builder.run(write_images)?,
|
||||
gradients: builder.run(write_gradients)?,
|
||||
patterns: builder.run(write_patterns)?,
|
||||
tilings: builder.run(write_tilings)?,
|
||||
ext_gs: builder.run(write_graphic_states)?,
|
||||
})
|
||||
})?
|
||||
@ -89,9 +89,9 @@ pub struct PdfOptions<'a> {
|
||||
/// `Auto`, a hash of the document's title and author is used instead (which
|
||||
/// is reasonably unique and stable).
|
||||
pub ident: Smart<&'a str>,
|
||||
/// If not `None`, shall be the creation date of the document as a UTC
|
||||
/// datetime. It will only be used if `set document(date: ..)` is `auto`.
|
||||
pub timestamp: Option<Datetime>,
|
||||
/// If not `None`, shall be the creation timestamp of the document. It will
|
||||
/// only be used if `set document(date: ..)` is `auto`.
|
||||
pub timestamp: Option<Timestamp>,
|
||||
/// Specifies which ranges of pages should be exported in the PDF. When
|
||||
/// `None`, all pages should be exported.
|
||||
pub page_ranges: Option<PageRanges>,
|
||||
@ -99,6 +99,51 @@ pub struct PdfOptions<'a> {
|
||||
pub standards: PdfStandards,
|
||||
}
|
||||
|
||||
/// A timestamp with timezone information.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Timestamp {
|
||||
/// The datetime of the timestamp.
|
||||
pub(crate) datetime: Datetime,
|
||||
/// The timezone of the timestamp.
|
||||
pub(crate) timezone: Timezone,
|
||||
}
|
||||
|
||||
impl Timestamp {
|
||||
/// Create a new timestamp with a given datetime and UTC suffix.
|
||||
pub fn new_utc(datetime: Datetime) -> Self {
|
||||
Self { datetime, timezone: Timezone::UTC }
|
||||
}
|
||||
|
||||
/// Create a new timestamp with a given datetime, and a local timezone offset.
|
||||
pub fn new_local(datetime: Datetime, whole_minute_offset: i32) -> Option<Self> {
|
||||
let hour_offset = (whole_minute_offset / 60).try_into().ok()?;
|
||||
// Note: the `%` operator in Rust is the remainder operator, not the
|
||||
// modulo operator. The remainder operator can return negative results.
|
||||
// We can simply apply `abs` here because we assume the `minute_offset`
|
||||
// will have the same sign as `hour_offset`.
|
||||
let minute_offset = (whole_minute_offset % 60).abs().try_into().ok()?;
|
||||
match (hour_offset, minute_offset) {
|
||||
// Only accept valid timezone offsets with `-23 <= hours <= 23`,
|
||||
// and `0 <= minutes <= 59`.
|
||||
(-23..=23, 0..=59) => Some(Self {
|
||||
datetime,
|
||||
timezone: Timezone::Local { hour_offset, minute_offset },
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A timezone.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Timezone {
|
||||
/// The UTC timezone.
|
||||
UTC,
|
||||
/// The local timezone offset from UTC. And the `minute_offset` will have
|
||||
/// same sign as `hour_offset`.
|
||||
Local { hour_offset: i8, minute_offset: u8 },
|
||||
}
|
||||
|
||||
/// Encapsulates a list of compatible PDF standards.
|
||||
#[derive(Clone)]
|
||||
pub struct PdfStandards {
|
||||
@ -267,8 +312,8 @@ struct References {
|
||||
images: HashMap<Image, Ref>,
|
||||
/// The IDs of written gradients.
|
||||
gradients: HashMap<PdfGradient, Ref>,
|
||||
/// The IDs of written patterns.
|
||||
patterns: HashMap<PdfPattern, Ref>,
|
||||
/// The IDs of written tilings.
|
||||
tilings: HashMap<PdfTiling, Ref>,
|
||||
/// The IDs of written external graphics states.
|
||||
ext_gs: HashMap<ExtGState, Ref>,
|
||||
}
|
||||
@ -612,3 +657,41 @@ fn transform_to_array(ts: Transform) -> [f32; 6] {
|
||||
ts.ty.to_f32(),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_timestamp_new_local() {
|
||||
let dummy_datetime = Datetime::from_ymd_hms(2024, 12, 17, 10, 10, 10).unwrap();
|
||||
let test = |whole_minute_offset, expect_timezone| {
|
||||
assert_eq!(
|
||||
Timestamp::new_local(dummy_datetime, whole_minute_offset)
|
||||
.unwrap()
|
||||
.timezone,
|
||||
expect_timezone
|
||||
);
|
||||
};
|
||||
|
||||
// Valid timezone offsets
|
||||
test(0, Timezone::Local { hour_offset: 0, minute_offset: 0 });
|
||||
test(480, Timezone::Local { hour_offset: 8, minute_offset: 0 });
|
||||
test(-480, Timezone::Local { hour_offset: -8, minute_offset: 0 });
|
||||
test(330, Timezone::Local { hour_offset: 5, minute_offset: 30 });
|
||||
test(-210, Timezone::Local { hour_offset: -3, minute_offset: 30 });
|
||||
test(-720, Timezone::Local { hour_offset: -12, minute_offset: 0 }); // AoE
|
||||
|
||||
// Corner cases
|
||||
test(315, Timezone::Local { hour_offset: 5, minute_offset: 15 });
|
||||
test(-225, Timezone::Local { hour_offset: -3, minute_offset: 45 });
|
||||
test(1439, Timezone::Local { hour_offset: 23, minute_offset: 59 });
|
||||
test(-1439, Timezone::Local { hour_offset: -23, minute_offset: 59 });
|
||||
|
||||
// Invalid timezone offsets
|
||||
assert!(Timestamp::new_local(dummy_datetime, 1440).is_none());
|
||||
assert!(Timestamp::new_local(dummy_datetime, -1440).is_none());
|
||||
assert!(Timestamp::new_local(dummy_datetime, i32::MAX).is_none());
|
||||
assert!(Timestamp::new_local(dummy_datetime, i32::MIN).is_none());
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ use crate::color_font::ColorFontMap;
|
||||
use crate::extg::ExtGState;
|
||||
use crate::gradient::PdfGradient;
|
||||
use crate::image::EncodedImage;
|
||||
use crate::pattern::PatternRemapper;
|
||||
use crate::tiling::TilingRemapper;
|
||||
use crate::{PdfChunk, Renumber, WithEverything, WithResources};
|
||||
|
||||
/// All the resources that have been collected when traversing the document.
|
||||
@ -31,16 +31,16 @@ use crate::{PdfChunk, Renumber, WithEverything, WithResources};
|
||||
/// This does not allocate references to resources, only track what was used
|
||||
/// and deduplicate what can be deduplicated.
|
||||
///
|
||||
/// You may notice that this structure is a tree: [`PatternRemapper`] and
|
||||
/// You may notice that this structure is a tree: [`TilingRemapper`] and
|
||||
/// [`ColorFontMap`] (that are present in the fields of [`Resources`]),
|
||||
/// themselves contain [`Resources`] (that will be called "sub-resources" from
|
||||
/// now on). Because color glyphs and patterns are defined using content
|
||||
/// now on). Because color glyphs and tilings are defined using content
|
||||
/// streams, just like pages, they can refer to resources too, which are tracked
|
||||
/// by the respective sub-resources.
|
||||
///
|
||||
/// Each instance of this structure will become a `/Resources` dictionary in
|
||||
/// the final PDF. It is not possible to use a single shared dictionary for all
|
||||
/// pages, patterns and color fonts, because if a resource is listed in its own
|
||||
/// pages, tilings and color fonts, because if a resource is listed in its own
|
||||
/// `/Resources` dictionary, some PDF readers will fail to open the document.
|
||||
///
|
||||
/// Because we need to lazily initialize sub-resources (we don't know how deep
|
||||
@ -66,8 +66,8 @@ pub struct Resources<R = Ref> {
|
||||
pub deferred_images: HashMap<usize, (Deferred<StrResult<EncodedImage>>, Span)>,
|
||||
/// Deduplicates gradients used across the document.
|
||||
pub gradients: Remapper<PdfGradient>,
|
||||
/// Deduplicates patterns used across the document.
|
||||
pub patterns: Option<Box<PatternRemapper<R>>>,
|
||||
/// Deduplicates tilings used across the document.
|
||||
pub tilings: Option<Box<TilingRemapper<R>>>,
|
||||
/// Deduplicates external graphics states used across the document.
|
||||
pub ext_gs: Remapper<ExtGState>,
|
||||
/// Deduplicates color glyphs.
|
||||
@ -107,8 +107,8 @@ impl<R: Renumber> Renumber for Resources<R> {
|
||||
color_fonts.resources.renumber(offset);
|
||||
}
|
||||
|
||||
if let Some(patterns) = &mut self.patterns {
|
||||
patterns.resources.renumber(offset);
|
||||
if let Some(tilings) = &mut self.tilings {
|
||||
tilings.resources.renumber(offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -122,7 +122,7 @@ impl Default for Resources<()> {
|
||||
images: Remapper::new("Im"),
|
||||
deferred_images: HashMap::new(),
|
||||
gradients: Remapper::new("Gr"),
|
||||
patterns: None,
|
||||
tilings: None,
|
||||
ext_gs: Remapper::new("Gs"),
|
||||
color_fonts: None,
|
||||
languages: BTreeMap::new(),
|
||||
@ -144,9 +144,9 @@ impl Resources<()> {
|
||||
images: self.images,
|
||||
deferred_images: self.deferred_images,
|
||||
gradients: self.gradients,
|
||||
patterns: self
|
||||
.patterns
|
||||
.zip(refs.patterns.as_ref())
|
||||
tilings: self
|
||||
.tilings
|
||||
.zip(refs.tilings.as_ref())
|
||||
.map(|(p, r)| Box::new(p.with_refs(r))),
|
||||
ext_gs: self.ext_gs,
|
||||
color_fonts: self
|
||||
@ -172,8 +172,8 @@ impl<R> Resources<R> {
|
||||
if let Some(color_fonts) = &self.color_fonts {
|
||||
color_fonts.resources.traverse(process)?;
|
||||
}
|
||||
if let Some(patterns) = &self.patterns {
|
||||
patterns.resources.traverse(process)?;
|
||||
if let Some(tilings) = &self.tilings {
|
||||
tilings.resources.traverse(process)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@ -186,7 +186,7 @@ impl<R> Resources<R> {
|
||||
pub struct ResourcesRefs {
|
||||
pub reference: Ref,
|
||||
pub color_fonts: Option<Box<ResourcesRefs>>,
|
||||
pub patterns: Option<Box<ResourcesRefs>>,
|
||||
pub tilings: Option<Box<ResourcesRefs>>,
|
||||
}
|
||||
|
||||
impl Renumber for ResourcesRefs {
|
||||
@ -195,8 +195,8 @@ impl Renumber for ResourcesRefs {
|
||||
if let Some(color_fonts) = &mut self.color_fonts {
|
||||
color_fonts.renumber(offset);
|
||||
}
|
||||
if let Some(patterns) = &mut self.patterns {
|
||||
patterns.renumber(offset);
|
||||
if let Some(tilings) = &mut self.tilings {
|
||||
tilings.renumber(offset);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -214,8 +214,8 @@ pub fn alloc_resources_refs(
|
||||
.color_fonts
|
||||
.as_ref()
|
||||
.map(|c| Box::new(refs_for(&c.resources, chunk))),
|
||||
patterns: resources
|
||||
.patterns
|
||||
tilings: resources
|
||||
.tilings
|
||||
.as_ref()
|
||||
.map(|p| Box::new(refs_for(&p.resources, chunk))),
|
||||
}
|
||||
@ -231,7 +231,7 @@ pub fn alloc_resources_refs(
|
||||
/// to the root node of the page tree because using the resource inheritance
|
||||
/// feature breaks PDF merging with Apple Preview.
|
||||
///
|
||||
/// Also write resource dictionaries for Type3 fonts and patterns.
|
||||
/// Also write resource dictionaries for Type3 fonts and PDF patterns.
|
||||
pub fn write_resource_dictionaries(ctx: &WithEverything) -> SourceResult<(PdfChunk, ())> {
|
||||
let mut chunk = PdfChunk::new();
|
||||
let mut used_color_spaces = ColorSpaces::default();
|
||||
@ -266,8 +266,8 @@ pub fn write_resource_dictionaries(ctx: &WithEverything) -> SourceResult<(PdfChu
|
||||
resources
|
||||
.gradients
|
||||
.write(&ctx.references.gradients, &mut patterns_dict);
|
||||
if let Some(p) = &resources.patterns {
|
||||
p.remapper.write(&ctx.references.patterns, &mut patterns_dict);
|
||||
if let Some(p) = &resources.tilings {
|
||||
p.remapper.write(&ctx.references.tilings, &mut patterns_dict);
|
||||
}
|
||||
patterns_dict.finish();
|
||||
|
||||
|
@ -5,7 +5,7 @@ use pdf_writer::types::{ColorSpaceOperand, PaintType, TilingType};
|
||||
use pdf_writer::{Filter, Name, Rect, Ref};
|
||||
use typst_library::diag::SourceResult;
|
||||
use typst_library::layout::{Abs, Ratio, Transform};
|
||||
use typst_library::visualize::{Pattern, RelativeTo};
|
||||
use typst_library::visualize::{RelativeTo, Tiling};
|
||||
use typst_utils::Numeric;
|
||||
|
||||
use crate::color::PaintEncode;
|
||||
@ -14,18 +14,18 @@ use crate::{content, transform_to_array, PdfChunk, Resources, WithGlobalRefs};
|
||||
|
||||
/// Writes the actual patterns (tiling patterns) to the PDF.
|
||||
/// This is performed once after writing all pages.
|
||||
pub fn write_patterns(
|
||||
pub fn write_tilings(
|
||||
context: &WithGlobalRefs,
|
||||
) -> SourceResult<(PdfChunk, HashMap<PdfPattern, Ref>)> {
|
||||
) -> SourceResult<(PdfChunk, HashMap<PdfTiling, Ref>)> {
|
||||
let mut chunk = PdfChunk::new();
|
||||
let mut out = HashMap::new();
|
||||
context.resources.traverse(&mut |resources| {
|
||||
let Some(patterns) = &resources.patterns else {
|
||||
let Some(patterns) = &resources.tilings else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
for pdf_pattern in patterns.remapper.items() {
|
||||
let PdfPattern { transform, pattern, content, .. } = pdf_pattern;
|
||||
let PdfTiling { transform, pattern, content, .. } = pdf_pattern;
|
||||
if out.contains_key(pdf_pattern) {
|
||||
continue;
|
||||
}
|
||||
@ -69,11 +69,11 @@ pub fn write_patterns(
|
||||
|
||||
/// A pattern and its transform.
|
||||
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
|
||||
pub struct PdfPattern {
|
||||
pub struct PdfTiling {
|
||||
/// The transform to apply to the pattern.
|
||||
pub transform: Transform,
|
||||
/// The pattern to paint.
|
||||
pub pattern: Pattern,
|
||||
pub pattern: Tiling,
|
||||
/// The rendered pattern.
|
||||
pub content: Vec<u8>,
|
||||
}
|
||||
@ -81,14 +81,14 @@ pub struct PdfPattern {
|
||||
/// Registers a pattern with the PDF.
|
||||
fn register_pattern(
|
||||
ctx: &mut content::Builder,
|
||||
pattern: &Pattern,
|
||||
pattern: &Tiling,
|
||||
on_text: bool,
|
||||
mut transforms: content::Transforms,
|
||||
) -> SourceResult<usize> {
|
||||
let patterns = ctx
|
||||
.resources
|
||||
.patterns
|
||||
.get_or_insert_with(|| Box::new(PatternRemapper::new()));
|
||||
.tilings
|
||||
.get_or_insert_with(|| Box::new(TilingRemapper::new()));
|
||||
|
||||
// Edge cases for strokes.
|
||||
if transforms.size.x.is_zero() {
|
||||
@ -113,7 +113,7 @@ fn register_pattern(
|
||||
None,
|
||||
)?;
|
||||
|
||||
let pdf_pattern = PdfPattern {
|
||||
let pdf_pattern = PdfTiling {
|
||||
transform,
|
||||
pattern: pattern.clone(),
|
||||
content: content.content.wait().clone(),
|
||||
@ -122,7 +122,7 @@ fn register_pattern(
|
||||
Ok(patterns.remapper.insert(pdf_pattern))
|
||||
}
|
||||
|
||||
impl PaintEncode for Pattern {
|
||||
impl PaintEncode for Tiling {
|
||||
fn set_as_fill(
|
||||
&self,
|
||||
ctx: &mut content::Builder,
|
||||
@ -159,14 +159,14 @@ impl PaintEncode for Pattern {
|
||||
}
|
||||
|
||||
/// De-duplicate patterns and the resources they require to be drawn.
|
||||
pub struct PatternRemapper<R> {
|
||||
pub struct TilingRemapper<R> {
|
||||
/// Pattern de-duplicator.
|
||||
pub remapper: Remapper<PdfPattern>,
|
||||
pub remapper: Remapper<PdfTiling>,
|
||||
/// PDF resources that are used by these patterns.
|
||||
pub resources: Resources<R>,
|
||||
}
|
||||
|
||||
impl PatternRemapper<()> {
|
||||
impl TilingRemapper<()> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
remapper: Remapper::new("P"),
|
||||
@ -175,8 +175,8 @@ impl PatternRemapper<()> {
|
||||
}
|
||||
|
||||
/// Allocate a reference to the resource dictionary of these patterns.
|
||||
pub fn with_refs(self, refs: &ResourcesRefs) -> PatternRemapper<Ref> {
|
||||
PatternRemapper {
|
||||
pub fn with_refs(self, refs: &ResourcesRefs) -> TilingRemapper<Ref> {
|
||||
TilingRemapper {
|
||||
remapper: self.remapper,
|
||||
resources: self.resources.with_refs(refs),
|
||||
}
|
@ -823,7 +823,7 @@ static PAR: GroupingRule = GroupingRule {
|
||||
RealizationKind::HtmlDocument(_) | RealizationKind::HtmlFragment
|
||||
) && content
|
||||
.to_packed::<HtmlElem>()
|
||||
.is_some_and(|elem| tag::is_inline(elem.tag)))
|
||||
.is_some_and(|elem| tag::is_inline_by_default(elem.tag)))
|
||||
},
|
||||
inner: |content| content.elem() == SpaceElem::elem(),
|
||||
interrupt: |elem| elem == ParElem::elem() || elem == AlignElem::elem(),
|
||||
@ -836,7 +836,9 @@ static CITES: GroupingRule = GroupingRule {
|
||||
tags: false,
|
||||
trigger: |content, _| content.elem() == CiteElem::elem(),
|
||||
inner: |content| content.elem() == SpaceElem::elem(),
|
||||
interrupt: |elem| elem == CiteGroup::elem(),
|
||||
interrupt: |elem| {
|
||||
elem == CiteGroup::elem() || elem == ParElem::elem() || elem == AlignElem::elem()
|
||||
},
|
||||
finish: finish_cites,
|
||||
};
|
||||
|
||||
@ -859,7 +861,7 @@ const fn list_like_grouping<T: ListLike>() -> GroupingRule {
|
||||
let elem = content.elem();
|
||||
elem == SpaceElem::elem() || elem == ParbreakElem::elem()
|
||||
},
|
||||
interrupt: |elem| elem == T::elem(),
|
||||
interrupt: |elem| elem == T::elem() || elem == AlignElem::elem(),
|
||||
finish: finish_list_like::<T>,
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ use std::sync::Arc;
|
||||
|
||||
use tiny_skia as sk;
|
||||
use typst_library::layout::{Axes, Point, Ratio, Size};
|
||||
use typst_library::visualize::{Color, Gradient, Paint, Pattern, RelativeTo};
|
||||
use typst_library::visualize::{Color, Gradient, Paint, RelativeTo, Tiling};
|
||||
|
||||
use crate::{AbsExt, State};
|
||||
|
||||
@ -72,26 +72,26 @@ impl PaintSampler for GradientSampler<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// State used when sampling patterns for text.
|
||||
/// State used when sampling tilings for text.
|
||||
///
|
||||
/// It caches the inverse transform to the parent, so that we can
|
||||
/// reuse it instead of recomputing it for each pixel.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct PatternSampler<'a> {
|
||||
pub struct TilingSampler<'a> {
|
||||
size: Size,
|
||||
transform_to_parent: sk::Transform,
|
||||
pixmap: &'a sk::Pixmap,
|
||||
pixel_per_pt: f32,
|
||||
}
|
||||
|
||||
impl<'a> PatternSampler<'a> {
|
||||
impl<'a> TilingSampler<'a> {
|
||||
pub fn new(
|
||||
pattern: &'a Pattern,
|
||||
tilings: &'a Tiling,
|
||||
pixmap: &'a sk::Pixmap,
|
||||
state: &State,
|
||||
on_text: bool,
|
||||
) -> Self {
|
||||
let relative = pattern.unwrap_relative(on_text);
|
||||
let relative = tilings.unwrap_relative(on_text);
|
||||
let fill_transform = match relative {
|
||||
RelativeTo::Self_ => sk::Transform::identity(),
|
||||
RelativeTo::Parent => state.container_transform.invert().unwrap(),
|
||||
@ -99,17 +99,17 @@ impl<'a> PatternSampler<'a> {
|
||||
|
||||
Self {
|
||||
pixmap,
|
||||
size: (pattern.size() + pattern.spacing()) * state.pixel_per_pt as f64,
|
||||
size: (tilings.size() + tilings.spacing()) * state.pixel_per_pt as f64,
|
||||
transform_to_parent: fill_transform,
|
||||
pixel_per_pt: state.pixel_per_pt,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PaintSampler for PatternSampler<'_> {
|
||||
impl PaintSampler for TilingSampler<'_> {
|
||||
/// Samples a single point in a glyph.
|
||||
fn sample(self, (x, y): (u32, u32)) -> sk::PremultipliedColorU8 {
|
||||
// Compute the point in the pattern's coordinate space.
|
||||
// Compute the point in the tilings's coordinate space.
|
||||
let mut point = sk::Point { x: x as f32, y: y as f32 };
|
||||
self.transform_to_parent.map_point(&mut point);
|
||||
|
||||
@ -118,7 +118,7 @@ impl PaintSampler for PatternSampler<'_> {
|
||||
let y =
|
||||
(point.y * self.pixel_per_pt).rem_euclid(self.size.y.to_f32()).floor() as u32;
|
||||
|
||||
// Sample the pattern
|
||||
// Sample the tilings
|
||||
self.pixmap.pixel(x, y).unwrap()
|
||||
}
|
||||
}
|
||||
@ -218,8 +218,8 @@ pub fn to_sk_paint<'a>(
|
||||
|
||||
sk_paint.anti_alias = gradient.anti_alias();
|
||||
}
|
||||
Paint::Pattern(pattern) => {
|
||||
let relative = pattern.unwrap_relative(on_text);
|
||||
Paint::Tiling(tilings) => {
|
||||
let relative = tilings.unwrap_relative(on_text);
|
||||
|
||||
let fill_transform = match relative {
|
||||
RelativeTo::Self_ => fill_transform.unwrap_or_default(),
|
||||
@ -228,7 +228,7 @@ pub fn to_sk_paint<'a>(
|
||||
.post_concat(state.transform.invert().unwrap()),
|
||||
};
|
||||
|
||||
let canvas = render_pattern_frame(&state, pattern);
|
||||
let canvas = render_tiling_frame(&state, tilings);
|
||||
*pixmap = Some(Arc::new(canvas));
|
||||
|
||||
let offset = match relative {
|
||||
@ -265,17 +265,17 @@ pub fn to_sk_color_u8(color: Color) -> sk::ColorU8 {
|
||||
sk::ColorU8::from_rgba(r, g, b, a)
|
||||
}
|
||||
|
||||
pub fn render_pattern_frame(state: &State, pattern: &Pattern) -> sk::Pixmap {
|
||||
let size = pattern.size() + pattern.spacing();
|
||||
pub fn render_tiling_frame(state: &State, tilings: &Tiling) -> sk::Pixmap {
|
||||
let size = tilings.size() + tilings.spacing();
|
||||
let mut canvas = sk::Pixmap::new(
|
||||
(size.x.to_f32() * state.pixel_per_pt).round() as u32,
|
||||
(size.y.to_f32() * state.pixel_per_pt).round() as u32,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Render the pattern into a new canvas.
|
||||
// Render the tilings into a new canvas.
|
||||
let ts = sk::Transform::from_scale(state.pixel_per_pt, state.pixel_per_pt);
|
||||
let temp_state = State::new(pattern.size(), ts, state.pixel_per_pt);
|
||||
crate::render_frame(&mut canvas, temp_state, pattern.frame());
|
||||
let temp_state = State::new(tilings.size(), ts, state.pixel_per_pt);
|
||||
crate::render_frame(&mut canvas, temp_state, tilings.frame());
|
||||
canvas
|
||||
}
|
||||
|
@ -168,11 +168,11 @@ pub fn to_sk_line_join(join: LineJoin) -> sk::LineJoin {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_sk_dash_pattern(pattern: &DashPattern<Abs, Abs>) -> Option<sk::StrokeDash> {
|
||||
pub fn to_sk_dash_pattern(dash: &DashPattern<Abs, Abs>) -> Option<sk::StrokeDash> {
|
||||
// tiny-skia only allows dash patterns with an even number of elements,
|
||||
// while pdf allows any number.
|
||||
let pattern_len = pattern.array.len();
|
||||
let pattern_len = dash.array.len();
|
||||
let len = if pattern_len % 2 == 1 { 2 * pattern_len } else { pattern_len };
|
||||
let dash_array = pattern.array.iter().map(|l| l.to_f32()).cycle().take(len).collect();
|
||||
sk::StrokeDash::new(dash_array, pattern.phase.to_f32())
|
||||
let dash_array = dash.array.iter().map(|l| l.to_f32()).cycle().take(len).collect();
|
||||
sk::StrokeDash::new(dash_array, dash.phase.to_f32())
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ use typst_library::text::color::{glyph_frame, should_outline};
|
||||
use typst_library::text::{Font, TextItem};
|
||||
use typst_library::visualize::{FixedStroke, Paint};
|
||||
|
||||
use crate::paint::{self, GradientSampler, PaintSampler, PatternSampler};
|
||||
use crate::paint::{self, GradientSampler, PaintSampler, TilingSampler};
|
||||
use crate::{shape, AbsExt, State};
|
||||
|
||||
/// Render a text run into the canvas.
|
||||
@ -145,9 +145,9 @@ fn render_outline_glyph(
|
||||
paint::to_sk_color_u8(*color).premultiply(),
|
||||
)?;
|
||||
}
|
||||
Paint::Pattern(pattern) => {
|
||||
let pixmap = paint::render_pattern_frame(&state, pattern);
|
||||
let sampler = PatternSampler::new(pattern, &pixmap, &state, true);
|
||||
Paint::Tiling(tiling) => {
|
||||
let pixmap = paint::render_tiling_frame(&state, tiling);
|
||||
let sampler = TilingSampler::new(tiling, &pixmap, &state, true);
|
||||
write_bitmap(canvas, &bitmap, &state, sampler)?;
|
||||
}
|
||||
}
|
||||
|
@ -14,11 +14,11 @@ use typst_library::layout::{
|
||||
Abs, Frame, FrameItem, FrameKind, GroupItem, Page, PagedDocument, Point, Ratio, Size,
|
||||
Transform,
|
||||
};
|
||||
use typst_library::visualize::{Geometry, Gradient, Pattern};
|
||||
use typst_library::visualize::{Geometry, Gradient, Tiling};
|
||||
use typst_utils::hash128;
|
||||
use xmlwriter::XmlWriter;
|
||||
|
||||
use crate::paint::{GradientRef, PatternRef, SVGSubGradient};
|
||||
use crate::paint::{GradientRef, SVGSubGradient, TilingRef};
|
||||
use crate::text::RenderedGlyph;
|
||||
|
||||
/// Export a frame into a SVG file.
|
||||
@ -92,12 +92,12 @@ struct SVGRenderer {
|
||||
/// different transforms. Therefore this allows us to reuse the same gradient
|
||||
/// multiple times.
|
||||
gradient_refs: Deduplicator<GradientRef>,
|
||||
/// Deduplicated patterns with transform matrices. They use a reference
|
||||
/// (`href`) to a "source" pattern instead of being defined inline.
|
||||
/// This saves a lot of space since patterns are often reused but with
|
||||
/// Deduplicated tilings with transform matrices. They use a reference
|
||||
/// (`href`) to a "source" tiling instead of being defined inline.
|
||||
/// This saves a lot of space since tilings are often reused but with
|
||||
/// different transforms. Therefore this allows us to reuse the same gradient
|
||||
/// multiple times.
|
||||
pattern_refs: Deduplicator<PatternRef>,
|
||||
tiling_refs: Deduplicator<TilingRef>,
|
||||
/// These are the actual gradients being written in the SVG file.
|
||||
/// These gradients are deduplicated because they do not contain the transform
|
||||
/// matrix, allowing them to be reused across multiple invocations.
|
||||
@ -105,12 +105,12 @@ struct SVGRenderer {
|
||||
/// The `Ratio` is the aspect ratio of the gradient, this is used to correct
|
||||
/// the angle of the gradient.
|
||||
gradients: Deduplicator<(Gradient, Ratio)>,
|
||||
/// These are the actual patterns being written in the SVG file.
|
||||
/// These patterns are deduplicated because they do not contain the transform
|
||||
/// These are the actual tilings being written in the SVG file.
|
||||
/// These tilings are deduplicated because they do not contain the transform
|
||||
/// matrix, allowing them to be reused across multiple invocations.
|
||||
///
|
||||
/// The `String` is the rendered pattern frame.
|
||||
patterns: Deduplicator<Pattern>,
|
||||
/// The `String` is the rendered tiling frame.
|
||||
tilings: Deduplicator<Tiling>,
|
||||
/// These are the gradients that compose a conic gradient.
|
||||
conic_subgradients: Deduplicator<SVGSubGradient>,
|
||||
}
|
||||
@ -163,8 +163,8 @@ impl SVGRenderer {
|
||||
gradient_refs: Deduplicator::new('g'),
|
||||
gradients: Deduplicator::new('f'),
|
||||
conic_subgradients: Deduplicator::new('s'),
|
||||
pattern_refs: Deduplicator::new('p'),
|
||||
patterns: Deduplicator::new('t'),
|
||||
tiling_refs: Deduplicator::new('p'),
|
||||
tilings: Deduplicator::new('t'),
|
||||
}
|
||||
}
|
||||
|
||||
@ -272,8 +272,8 @@ impl SVGRenderer {
|
||||
self.write_gradients();
|
||||
self.write_gradient_refs();
|
||||
self.write_subgradients();
|
||||
self.write_patterns();
|
||||
self.write_pattern_refs();
|
||||
self.write_tilings();
|
||||
self.write_tiling_refs();
|
||||
self.xml.end_document()
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@ use ecow::{eco_format, EcoString};
|
||||
use ttf_parser::OutlineBuilder;
|
||||
use typst_library::foundations::Repr;
|
||||
use typst_library::layout::{Angle, Axes, Frame, Quadrant, Ratio, Size, Transform};
|
||||
use typst_library::visualize::{Color, FillRule, Gradient, Paint, Pattern, RatioOrAngle};
|
||||
use typst_library::visualize::{Color, FillRule, Gradient, Paint, RatioOrAngle, Tiling};
|
||||
use typst_utils::hash128;
|
||||
use xmlwriter::XmlWriter;
|
||||
|
||||
@ -17,7 +17,7 @@ const CONIC_SEGMENT: usize = 360;
|
||||
|
||||
impl SVGRenderer {
|
||||
/// Render a frame to a string.
|
||||
pub(super) fn render_pattern_frame(
|
||||
pub(super) fn render_tiling_frame(
|
||||
&mut self,
|
||||
state: State,
|
||||
ts: Transform,
|
||||
@ -44,8 +44,8 @@ impl SVGRenderer {
|
||||
let id = self.push_gradient(gradient, size, ts);
|
||||
self.xml.write_attribute_fmt("fill", format_args!("url(#{id})"));
|
||||
}
|
||||
Paint::Pattern(pattern) => {
|
||||
let id = self.push_pattern(pattern, size, ts);
|
||||
Paint::Tiling(tiling) => {
|
||||
let id = self.push_tiling(tiling, size, ts);
|
||||
self.xml.write_attribute_fmt("fill", format_args!("url(#{id})"));
|
||||
}
|
||||
}
|
||||
@ -86,32 +86,31 @@ impl SVGRenderer {
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn push_pattern(
|
||||
pub(super) fn push_tiling(
|
||||
&mut self,
|
||||
pattern: &Pattern,
|
||||
tiling: &Tiling,
|
||||
size: Size,
|
||||
ts: Transform,
|
||||
) -> Id {
|
||||
let pattern_size = pattern.size() + pattern.spacing();
|
||||
let tiling_size = tiling.size() + tiling.spacing();
|
||||
// Unfortunately due to a limitation of `xmlwriter`, we need to
|
||||
// render the frame twice: once to allocate all of the resources
|
||||
// that it needs and once to actually render it.
|
||||
self.render_pattern_frame(
|
||||
State::new(pattern_size, Transform::identity()),
|
||||
self.render_tiling_frame(
|
||||
State::new(tiling_size, Transform::identity()),
|
||||
Transform::identity(),
|
||||
pattern.frame(),
|
||||
tiling.frame(),
|
||||
);
|
||||
|
||||
let pattern_id = self.patterns.insert_with(hash128(pattern), || pattern.clone());
|
||||
self.pattern_refs
|
||||
.insert_with(hash128(&(pattern_id, ts)), || PatternRef {
|
||||
id: pattern_id,
|
||||
transform: ts,
|
||||
ratio: Axes::new(
|
||||
Ratio::new(pattern_size.x.to_pt() / size.x.to_pt()),
|
||||
Ratio::new(pattern_size.y.to_pt() / size.y.to_pt()),
|
||||
),
|
||||
})
|
||||
let tiling_id = self.tilings.insert_with(hash128(tiling), || tiling.clone());
|
||||
self.tiling_refs.insert_with(hash128(&(tiling_id, ts)), || TilingRef {
|
||||
id: tiling_id,
|
||||
transform: ts,
|
||||
ratio: Axes::new(
|
||||
Ratio::new(tiling_size.x.to_pt() / size.x.to_pt()),
|
||||
Ratio::new(tiling_size.y.to_pt() / size.y.to_pt()),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
/// Write the raw gradients (without transform) to the SVG file.
|
||||
@ -188,12 +187,12 @@ impl SVGRenderer {
|
||||
// Create the path for the segment.
|
||||
let mut builder = SvgPathBuilder::default();
|
||||
builder.move_to(
|
||||
correct_pattern_pos(center.0),
|
||||
correct_pattern_pos(center.1),
|
||||
correct_tiling_pos(center.0),
|
||||
correct_tiling_pos(center.1),
|
||||
);
|
||||
builder.line_to(
|
||||
correct_pattern_pos(-2.0 * (theta1 + angle).cos() + center.0),
|
||||
correct_pattern_pos(2.0 * (theta1 + angle).sin() + center.1),
|
||||
correct_tiling_pos(-2.0 * (theta1 + angle).cos() + center.0),
|
||||
correct_tiling_pos(2.0 * (theta1 + angle).sin() + center.1),
|
||||
);
|
||||
builder.arc(
|
||||
(2.0, 2.0),
|
||||
@ -201,10 +200,10 @@ impl SVGRenderer {
|
||||
0,
|
||||
1,
|
||||
(
|
||||
correct_pattern_pos(
|
||||
correct_tiling_pos(
|
||||
-2.0 * (theta2 + angle).cos() + center.0,
|
||||
),
|
||||
correct_pattern_pos(
|
||||
correct_tiling_pos(
|
||||
2.0 * (theta2 + angle).sin() + center.1,
|
||||
),
|
||||
),
|
||||
@ -370,19 +369,19 @@ impl SVGRenderer {
|
||||
self.xml.end_element();
|
||||
}
|
||||
|
||||
/// Write the raw gradients (without transform) to the SVG file.
|
||||
pub(super) fn write_patterns(&mut self) {
|
||||
if self.patterns.is_empty() {
|
||||
/// Write the raw tilings (without transform) to the SVG file.
|
||||
pub(super) fn write_tilings(&mut self) {
|
||||
if self.tilings.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.xml.start_element("defs");
|
||||
self.xml.write_attribute("id", "patterns");
|
||||
self.xml.write_attribute("id", "tilings");
|
||||
|
||||
for (id, pattern) in
|
||||
self.patterns.iter().map(|(i, p)| (i, p.clone())).collect::<Vec<_>>()
|
||||
for (id, tiling) in
|
||||
self.tilings.iter().map(|(i, p)| (i, p.clone())).collect::<Vec<_>>()
|
||||
{
|
||||
let size = pattern.size() + pattern.spacing();
|
||||
let size = tiling.size() + tiling.spacing();
|
||||
self.xml.start_element("pattern");
|
||||
self.xml.write_attribute("id", &id);
|
||||
self.xml.write_attribute("width", &size.x.to_pt());
|
||||
@ -396,7 +395,7 @@ impl SVGRenderer {
|
||||
// Render the frame.
|
||||
let state = State::new(size, Transform::identity());
|
||||
let ts = Transform::identity();
|
||||
self.render_frame(state, ts, pattern.frame());
|
||||
self.render_frame(state, ts, tiling.frame());
|
||||
|
||||
self.xml.end_element();
|
||||
}
|
||||
@ -404,28 +403,28 @@ impl SVGRenderer {
|
||||
self.xml.end_element()
|
||||
}
|
||||
|
||||
/// Writes the references to the deduplicated patterns for each usage site.
|
||||
pub(super) fn write_pattern_refs(&mut self) {
|
||||
if self.pattern_refs.is_empty() {
|
||||
/// Writes the references to the deduplicated tilings for each usage site.
|
||||
pub(super) fn write_tiling_refs(&mut self) {
|
||||
if self.tiling_refs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.xml.start_element("defs");
|
||||
self.xml.write_attribute("id", "pattern-refs");
|
||||
for (id, pattern_ref) in self.pattern_refs.iter() {
|
||||
self.xml.write_attribute("id", "tilings-refs");
|
||||
for (id, tiling_ref) in self.tiling_refs.iter() {
|
||||
self.xml.start_element("pattern");
|
||||
self.xml
|
||||
.write_attribute("patternTransform", &SvgMatrix(pattern_ref.transform));
|
||||
.write_attribute("patternTransform", &SvgMatrix(tiling_ref.transform));
|
||||
|
||||
self.xml.write_attribute("id", &id);
|
||||
|
||||
// Writing the href attribute to the "reference" pattern.
|
||||
self.xml
|
||||
.write_attribute_fmt("href", format_args!("#{}", pattern_ref.id));
|
||||
.write_attribute_fmt("href", format_args!("#{}", tiling_ref.id));
|
||||
|
||||
// Also writing the xlink:href attribute for compatibility.
|
||||
self.xml
|
||||
.write_attribute_fmt("xlink:href", format_args!("#{}", pattern_ref.id));
|
||||
.write_attribute_fmt("xlink:href", format_args!("#{}", tiling_ref.id));
|
||||
self.xml.end_element();
|
||||
}
|
||||
|
||||
@ -433,15 +432,15 @@ impl SVGRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
/// A reference to a deduplicated pattern, with a transform matrix.
|
||||
/// A reference to a deduplicated tiling, with a transform matrix.
|
||||
///
|
||||
/// Allows patterns to be reused across multiple invocations,
|
||||
/// simply by changing the transform matrix.
|
||||
/// Allows tilings to be reused across multiple invocations, simply by changing
|
||||
/// the transform matrix.
|
||||
#[derive(Hash)]
|
||||
pub struct PatternRef {
|
||||
pub struct TilingRef {
|
||||
/// The ID of the deduplicated gradient
|
||||
id: Id,
|
||||
/// The transform matrix to apply to the pattern.
|
||||
/// The transform matrix to apply to the tiling.
|
||||
transform: Transform,
|
||||
/// The ratio of the size of the cell to the size of the filled area.
|
||||
ratio: Axes<Ratio>,
|
||||
@ -587,7 +586,7 @@ impl ColorEncode for Color {
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps a coordinate in a unit size square to a coordinate in the pattern.
|
||||
pub fn correct_pattern_pos(x: f32) -> f32 {
|
||||
/// Maps a coordinate in a unit size square to a coordinate in the tiling.
|
||||
pub fn correct_tiling_pos(x: f32) -> f32 {
|
||||
(x + 0.5) / 2.0
|
||||
}
|
||||
|
@ -67,8 +67,8 @@ impl SVGRenderer {
|
||||
)
|
||||
.post_concat(state.transform.invert().unwrap()),
|
||||
}
|
||||
} else if let Paint::Pattern(pattern) = paint {
|
||||
match pattern.unwrap_relative(false) {
|
||||
} else if let Paint::Tiling(tiling) = paint {
|
||||
match tiling.unwrap_relative(false) {
|
||||
RelativeTo::Self_ => Transform::identity(),
|
||||
RelativeTo::Parent => state.transform.invert().unwrap(),
|
||||
}
|
||||
@ -112,8 +112,8 @@ impl SVGRenderer {
|
||||
let id = self.push_gradient(gradient, size, fill_transform);
|
||||
self.xml.write_attribute_fmt("stroke", format_args!("url(#{id})"));
|
||||
}
|
||||
Paint::Pattern(pattern) => {
|
||||
let id = self.push_pattern(pattern, size, fill_transform);
|
||||
Paint::Tiling(tiling) => {
|
||||
let id = self.push_tiling(tiling, size, fill_transform);
|
||||
self.xml.write_attribute_fmt("stroke", format_args!("url(#{id})"));
|
||||
}
|
||||
}
|
||||
@ -137,11 +137,11 @@ impl SVGRenderer {
|
||||
);
|
||||
self.xml
|
||||
.write_attribute("stroke-miterlimit", &stroke.miter_limit.get());
|
||||
if let Some(pattern) = &stroke.dash {
|
||||
self.xml.write_attribute("stroke-dashoffset", &pattern.phase.to_pt());
|
||||
if let Some(dash) = &stroke.dash {
|
||||
self.xml.write_attribute("stroke-dashoffset", &dash.phase.to_pt());
|
||||
self.xml.write_attribute(
|
||||
"stroke-dasharray",
|
||||
&pattern
|
||||
&dash
|
||||
.array
|
||||
.iter()
|
||||
.map(|dash| dash.to_pt().to_string())
|
||||
|
@ -165,7 +165,7 @@ impl SVGRenderer {
|
||||
)
|
||||
.post_concat(state.transform.invert().unwrap()),
|
||||
},
|
||||
Paint::Pattern(pattern) => match pattern.unwrap_relative(true) {
|
||||
Paint::Tiling(tiling) => match tiling.unwrap_relative(true) {
|
||||
RelativeTo::Self_ => Transform::identity(),
|
||||
RelativeTo::Parent => state.transform.invert().unwrap(),
|
||||
},
|
||||
|
@ -163,7 +163,7 @@ pub enum Expr<'a> {
|
||||
Parenthesized(Parenthesized<'a>),
|
||||
/// An array: `(1, "hi", 12cm)`.
|
||||
Array(Array<'a>),
|
||||
/// A dictionary: `(thickness: 3pt, pattern: dashed)`.
|
||||
/// A dictionary: `(thickness: 3pt, dash: "solid")`.
|
||||
Dict(Dict<'a>),
|
||||
/// A unary operation: `-x`.
|
||||
Unary(Unary<'a>),
|
||||
@ -1195,7 +1195,7 @@ impl<'a> AstNode<'a> for ArrayItem<'a> {
|
||||
}
|
||||
|
||||
node! {
|
||||
/// A dictionary: `(thickness: 3pt, pattern: dashed)`.
|
||||
/// A dictionary: `(thickness: 3pt, dash: "solid")`.
|
||||
Dict
|
||||
}
|
||||
|
||||
|
@ -224,7 +224,7 @@ pub enum SyntaxKind {
|
||||
Parenthesized,
|
||||
/// An array: `(1, "hi", 12cm)`.
|
||||
Array,
|
||||
/// A dictionary: `(thickness: 3pt, pattern: dashed)`.
|
||||
/// A dictionary: `(thickness: 3pt, dash: "solid")`.
|
||||
Dict,
|
||||
/// A named pair: `thickness: 3pt`.
|
||||
Named,
|
||||
|
@ -251,8 +251,9 @@ impl Lexer<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Lex an entire raw segment at once. This is a convenience to avoid going
|
||||
/// to and from the parser for each raw section.
|
||||
/// We parse entire raw segments in the lexer as a convenience to avoid
|
||||
/// going to and from the parser for each raw section. See comments in
|
||||
/// [`Self::blocky_raw`] and [`Self::inline_raw`] for specific details.
|
||||
fn raw(&mut self) -> (SyntaxKind, SyntaxNode) {
|
||||
let start = self.s.cursor() - 1;
|
||||
|
||||
@ -313,6 +314,35 @@ impl Lexer<'_> {
|
||||
(SyntaxKind::Raw, SyntaxNode::inner(SyntaxKind::Raw, nodes))
|
||||
}
|
||||
|
||||
/// Raw blocks parse a language tag, have smart behavior for trimming
|
||||
/// whitespace in the start/end lines, and trim common leading whitespace
|
||||
/// from all other lines as the "dedent". The exact behavior is described
|
||||
/// below.
|
||||
///
|
||||
/// ### The initial line:
|
||||
/// - A valid Typst identifier immediately following the opening delimiter
|
||||
/// is parsed as the language tag.
|
||||
/// - We check the rest of the line and if all characters are whitespace,
|
||||
/// trim it. Otherwise we trim a single leading space if present.
|
||||
/// - If more trimmed characters follow on future lines, they will be
|
||||
/// merged into the same trimmed element.
|
||||
/// - If we didn't trim the entire line, the rest is kept as text.
|
||||
///
|
||||
/// ### Inner lines:
|
||||
/// - We determine the "dedent" by iterating over the lines. The dedent is
|
||||
/// the minimum number of leading whitespace characters (not bytes) before
|
||||
/// each line that has any non-whitespace characters.
|
||||
/// - The opening delimiter's line does not contribute to the dedent, but
|
||||
/// the closing delimiter's line does (even if that line is entirely
|
||||
/// whitespace up to the delimiter).
|
||||
/// - We then trim the newline and dedent characters of each line, and add a
|
||||
/// (potentially empty) text element of all remaining characters.
|
||||
///
|
||||
/// ### The final line:
|
||||
/// - If the last line is entirely whitespace, it is trimmed.
|
||||
/// - Otherwise its text is kept like an inner line. However, if the last
|
||||
/// non-whitespace character of the final line is a backtick, then one
|
||||
/// ascii space (if present) is trimmed from the end.
|
||||
fn blocky_raw<F>(&mut self, inner_end: usize, mut push_raw: F)
|
||||
where
|
||||
F: FnMut(SyntaxKind, &Scanner),
|
||||
@ -323,12 +353,10 @@ impl Lexer<'_> {
|
||||
push_raw(SyntaxKind::RawLang, &self.s);
|
||||
}
|
||||
|
||||
// Determine inner content between backticks.
|
||||
self.s.eat_if(' ');
|
||||
let inner = self.s.to(inner_end);
|
||||
// The rest of the function operates on the lines between the backticks.
|
||||
let mut lines = split_newlines(self.s.to(inner_end));
|
||||
|
||||
// Determine dedent level.
|
||||
let mut lines = split_newlines(inner);
|
||||
let dedent = lines
|
||||
.iter()
|
||||
.skip(1)
|
||||
@ -339,35 +367,61 @@ impl Lexer<'_> {
|
||||
.min()
|
||||
.unwrap_or(0);
|
||||
|
||||
// Trim single space in last line if text ends with a backtick. The last
|
||||
// line is the one directly before the closing backticks and if it is
|
||||
// just whitespace, it will be completely trimmed below.
|
||||
if inner.trim_end().ends_with('`') {
|
||||
if let Some(last) = lines.last_mut() {
|
||||
// Trim whitespace from the last line. Will be added as a `RawTrimmed`
|
||||
// kind by the check for `self.s.cursor() != inner_end` below.
|
||||
if lines.last().is_some_and(|last| last.chars().all(char::is_whitespace)) {
|
||||
lines.pop();
|
||||
} else if let Some(last) = lines.last_mut() {
|
||||
// If last line ends in a backtick, try to trim a single space. This
|
||||
// check must happen before we add the first line since the last and
|
||||
// first lines might be the same.
|
||||
if last.trim_end().ends_with('`') {
|
||||
*last = last.strip_suffix(' ').unwrap_or(last);
|
||||
}
|
||||
}
|
||||
|
||||
let is_whitespace = |line: &&str| line.chars().all(char::is_whitespace);
|
||||
let starts_whitespace = lines.first().is_some_and(is_whitespace);
|
||||
let ends_whitespace = lines.last().is_some_and(is_whitespace);
|
||||
|
||||
let mut lines = lines.into_iter();
|
||||
let mut skipped = false;
|
||||
|
||||
// Trim whitespace + newline at start.
|
||||
if starts_whitespace {
|
||||
self.s.advance(lines.next().unwrap().len());
|
||||
skipped = true;
|
||||
}
|
||||
// Trim whitespace + newline at end.
|
||||
if ends_whitespace {
|
||||
lines.next_back();
|
||||
// Handle the first line: trim if all whitespace, or trim a single space
|
||||
// at the start. Note that the first line does not affect the dedent
|
||||
// value.
|
||||
if let Some(first_line) = lines.next() {
|
||||
if first_line.chars().all(char::is_whitespace) {
|
||||
self.s.advance(first_line.len());
|
||||
// This is the only spot we advance the scanner, but don't
|
||||
// immediately call `push_raw`. But the rest of the function
|
||||
// ensures we will always add this text to a `RawTrimmed` later.
|
||||
debug_assert!(self.s.cursor() != inner_end);
|
||||
// A proof by cases follows:
|
||||
// # First case: The loop runs
|
||||
// If the loop runs, there must be a newline following, so
|
||||
// `cursor != inner_end`. And if the loop runs, the first thing
|
||||
// it does is add a trimmed element.
|
||||
// # Second case: The final if-statement runs.
|
||||
// To _not_ reach the loop from here, we must have only one or
|
||||
// two lines:
|
||||
// 1. If one line, we cannot be here, because the first and last
|
||||
// lines are the same, so this line will have been removed by
|
||||
// the check for the last line being all whitespace.
|
||||
// 2. If two lines, the loop will run unless the last is fully
|
||||
// whitespace, but if it is, it will have been popped, then
|
||||
// the final if-statement will run because the text removed
|
||||
// by the last line must include at least a newline, so
|
||||
// `cursor != inner_end` here.
|
||||
} else {
|
||||
let line_end = self.s.cursor() + first_line.len();
|
||||
if self.s.eat_if(' ') {
|
||||
// Trim a single space after the lang tag on the first line.
|
||||
push_raw(SyntaxKind::RawTrimmed, &self.s);
|
||||
}
|
||||
// We know here that the rest of the line is non-empty.
|
||||
self.s.jump(line_end);
|
||||
push_raw(SyntaxKind::Text, &self.s);
|
||||
}
|
||||
}
|
||||
|
||||
// Add lines.
|
||||
for (i, line) in lines.enumerate() {
|
||||
let dedent = if i == 0 && !skipped { 0 } else { dedent };
|
||||
for line in lines {
|
||||
let offset: usize = line.chars().take(dedent).map(char::len_utf8).sum();
|
||||
self.s.eat_newline();
|
||||
self.s.advance(offset);
|
||||
@ -383,6 +437,9 @@ impl Lexer<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Inline raw text is split on lines with non-newlines as `Text` kinds and
|
||||
/// newlines as `RawTrimmed`. Inline raw text does not dedent the text, all
|
||||
/// non-newline whitespace is kept.
|
||||
fn inline_raw<F>(&mut self, inner_end: usize, mut push_raw: F)
|
||||
where
|
||||
F: FnMut(SyntaxKind, &Scanner),
|
||||
@ -766,6 +823,12 @@ impl Lexer<'_> {
|
||||
return self.error(eco_format!("invalid number suffix: {}", suffix));
|
||||
}
|
||||
|
||||
if base != 10 {
|
||||
let kind = self.error(eco_format!("invalid base-{base} prefix"));
|
||||
self.hint("numbers with a unit cannot have a base prefix");
|
||||
return kind;
|
||||
}
|
||||
|
||||
SyntaxKind::Numeric
|
||||
}
|
||||
|
||||
|
@ -47,14 +47,9 @@ fn markup_exprs(p: &mut Parser, mut at_start: bool, stop_set: SyntaxSet) {
|
||||
debug_assert!(stop_set.contains(SyntaxKind::End));
|
||||
at_start |= p.had_newline();
|
||||
let mut nesting: usize = 0;
|
||||
loop {
|
||||
match p.current() {
|
||||
SyntaxKind::LeftBracket => nesting += 1,
|
||||
SyntaxKind::RightBracket if nesting > 0 => nesting -= 1,
|
||||
_ if p.at_set(stop_set) => break,
|
||||
_ => {}
|
||||
}
|
||||
markup_expr(p, at_start);
|
||||
// Keep going if we're at a nested right-bracket regardless of the stop set.
|
||||
while !p.at_set(stop_set) || (nesting > 0 && p.at(SyntaxKind::RightBracket)) {
|
||||
markup_expr(p, at_start, &mut nesting);
|
||||
at_start = p.had_newline();
|
||||
}
|
||||
}
|
||||
@ -69,15 +64,12 @@ pub(super) fn reparse_markup(
|
||||
) -> Option<Vec<SyntaxNode>> {
|
||||
let mut p = Parser::new(text, range.start, LexMode::Markup);
|
||||
*at_start |= p.had_newline();
|
||||
while p.current_start() < range.end {
|
||||
match p.current() {
|
||||
SyntaxKind::LeftBracket => *nesting += 1,
|
||||
SyntaxKind::RightBracket if *nesting > 0 => *nesting -= 1,
|
||||
SyntaxKind::RightBracket if !top_level => break,
|
||||
SyntaxKind::End => break,
|
||||
_ => {}
|
||||
while !p.end() && p.current_start() < range.end {
|
||||
// If not top-level and at a new RightBracket, stop the reparse.
|
||||
if !top_level && *nesting == 0 && p.at(SyntaxKind::RightBracket) {
|
||||
break;
|
||||
}
|
||||
markup_expr(&mut p, *at_start);
|
||||
markup_expr(&mut p, *at_start, nesting);
|
||||
*at_start = p.had_newline();
|
||||
}
|
||||
(p.balanced && p.current_start() == range.end).then(|| p.finish())
|
||||
@ -86,8 +78,21 @@ pub(super) fn reparse_markup(
|
||||
/// Parses a single markup expression. This includes markup elements like text,
|
||||
/// headings, strong/emph, lists/enums, etc. This is also the entry point for
|
||||
/// parsing math equations and embedded code expressions.
|
||||
fn markup_expr(p: &mut Parser, at_start: bool) {
|
||||
fn markup_expr(p: &mut Parser, at_start: bool, nesting: &mut usize) {
|
||||
match p.current() {
|
||||
SyntaxKind::LeftBracket => {
|
||||
*nesting += 1;
|
||||
p.convert_and_eat(SyntaxKind::Text);
|
||||
}
|
||||
SyntaxKind::RightBracket if *nesting > 0 => {
|
||||
*nesting -= 1;
|
||||
p.convert_and_eat(SyntaxKind::Text);
|
||||
}
|
||||
SyntaxKind::RightBracket => {
|
||||
p.unexpected();
|
||||
p.hint("try using a backslash escape: \\]");
|
||||
}
|
||||
|
||||
SyntaxKind::Text
|
||||
| SyntaxKind::Linebreak
|
||||
| SyntaxKind::Escape
|
||||
@ -108,9 +113,7 @@ fn markup_expr(p: &mut Parser, at_start: bool) {
|
||||
SyntaxKind::RefMarker => reference(p),
|
||||
SyntaxKind::Dollar => equation(p),
|
||||
|
||||
SyntaxKind::LeftBracket
|
||||
| SyntaxKind::RightBracket
|
||||
| SyntaxKind::HeadingMarker
|
||||
SyntaxKind::HeadingMarker
|
||||
| SyntaxKind::ListMarker
|
||||
| SyntaxKind::EnumMarker
|
||||
| SyntaxKind::TermMarker
|
||||
@ -201,7 +204,7 @@ fn equation(p: &mut Parser) {
|
||||
let m = p.marker();
|
||||
p.enter_modes(LexMode::Math, AtNewline::Continue, |p| {
|
||||
p.assert(SyntaxKind::Dollar);
|
||||
math(p, syntax_set!(Dollar, RightBracket, End));
|
||||
math(p, syntax_set!(Dollar, End));
|
||||
p.expect_closing_delimiter(m, SyntaxKind::Dollar);
|
||||
});
|
||||
p.wrap(m, SyntaxKind::Equation);
|
||||
@ -1074,7 +1077,7 @@ fn expr_with_paren(p: &mut Parser, atomic: bool) {
|
||||
/// Parses either
|
||||
/// - a parenthesized expression: `(1 + 2)`, or
|
||||
/// - an array: `(1, "hi", 12cm)`, or
|
||||
/// - a dictionary: `(thickness: 3pt, pattern: dashed)`.
|
||||
/// - a dictionary: `(thickness: 3pt, dash: "solid")`.
|
||||
fn parenthesized_or_array_or_dict(p: &mut Parser) -> SyntaxKind {
|
||||
let mut state = GroupState {
|
||||
count: 0,
|
||||
|
@ -31,7 +31,7 @@ description: Changes in Typst 0.10.0
|
||||
- More LaTeX commands (e.g. for quotes) are now respected in `.bib` files
|
||||
|
||||
## Visualization
|
||||
- Added support for [patterns]($pattern) as fills and strokes
|
||||
- Added support for [patterns]($tiling) as fills and strokes
|
||||
- The `alpha` parameter of the [`components`]($color.components) function on
|
||||
colors is now a named parameter **(Breaking change)**
|
||||
- Added support for the [Oklch]($color.oklch) color space
|
||||
|
@ -59,13 +59,13 @@ _Thanks to [@PgBiel](https://github.com/PgBiel) for his work on tables!_
|
||||
- When context is available, [`counter.display`] now directly returns the result
|
||||
of applying the numbering instead of yielding opaque content. It should not be
|
||||
used anymore without context. (Deprecation planned)
|
||||
- The [`state.display`] function should not be used anymore, use [`state.get`]
|
||||
- The `state.display` function should not be used anymore, use [`state.get`]
|
||||
instead (Deprecation planned)
|
||||
- The `location` argument of [`query`], [`counter.final`], and [`state.final`]
|
||||
should not be used anymore (Deprecation planned)
|
||||
- The [`styles`]($measure.styles) argument of the `measure` function should not
|
||||
be used anymore (Deprecation planned)
|
||||
- The [`style`] function should not be used anymore, use context instead
|
||||
- The `styles` argument of the `measure` function should not be used anymore
|
||||
(Deprecation planned)
|
||||
- The `style` function should not be used anymore, use context instead
|
||||
(Deprecation planned)
|
||||
- The correct context is now also provided in various other places where it is
|
||||
available, e.g. in show rules, layout callbacks, and numbering functions in
|
||||
|
@ -37,7 +37,7 @@ description: Changes in Typst 0.11.1
|
||||
|
||||
## Export
|
||||
- Fixed [smart quotes]($smartquote) in PDF outline
|
||||
- Fixed [patterns]($pattern) with spacing in PDF
|
||||
- Fixed [patterns]($tiling) with spacing in PDF
|
||||
- Fixed wrong PDF page labels when [page numbering]($page.numbering) was
|
||||
disabled after being previously enabled
|
||||
|
||||
|
@ -380,11 +380,11 @@ description: Changes in Typst 0.12.0
|
||||
- [`counter.display`] without an established context
|
||||
- [`counter.final`] with a location
|
||||
- [`state.final`] with a location
|
||||
- [`state.display`]
|
||||
- `state.display`
|
||||
- [`query`] with a location as the second argument
|
||||
- [`locate`] with a callback function
|
||||
- [`measure`] with styles
|
||||
- [`style`]
|
||||
- `style`
|
||||
|
||||
## Development
|
||||
- Added `typst-kit` crate which provides useful APIs for `World` implementors
|
||||
|
@ -83,7 +83,7 @@ description: Changes in early, unversioned Typst
|
||||
|
||||
- New [`measure`] function
|
||||
- Measure the layouted size of elements
|
||||
- To be used in combination with the new [`style`] function that lets you
|
||||
- To be used in combination with the new `style` function that lets you
|
||||
generate different content based on the style context something is inserted
|
||||
into (because that affects the measured size of content)
|
||||
|
||||
|
@ -226,7 +226,7 @@ applications, while academic applications tend to use strokes instead.
|
||||
To add zebra stripes to a table, we use the `table` function's `fill` argument.
|
||||
It can take three kinds of arguments:
|
||||
|
||||
- A single color (this can also be a gradient or a pattern) to fill all cells
|
||||
- A single color (this can also be a gradient or a tiling) to fill all cells
|
||||
with. Because we want some cells to have another color, this is not useful if
|
||||
we want to build zebra tables.
|
||||
- An array with colors which Typst cycles through for each column. We can use an
|
||||
@ -828,7 +828,7 @@ line appears because there is no `top` line that could suppress it.
|
||||
|
||||
### How to achieve a double line? { #double-stroke }
|
||||
Typst does not yet have a native way to draw double strokes, but there are
|
||||
multiple ways to emulate them, for example with [patterns]($pattern). We will
|
||||
multiple ways to emulate them, for example with [tilings]($tiling). We will
|
||||
show a different workaround in this section: Table gutters.
|
||||
|
||||
Tables can space their cells apart using the `gutter` argument. When a gutter is
|
||||
|
BIN
tests/ref/circle-beyond-page-width-overflows.png
Normal file
After Width: | Height: | Size: 620 B |
BIN
tests/ref/circle-size-beyond-default.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 380 B |
BIN
tests/ref/counter-page-display.png
Normal file
After Width: | Height: | Size: 150 B |
BIN
tests/ref/double-percent.png
Normal file
After Width: | Height: | Size: 496 B |
BIN
tests/ref/enum-numbering-reversed-overriden.png
Normal file
After Width: | Height: | Size: 666 B |
BIN
tests/ref/enum-numbering-reversed.png
Normal file
After Width: | Height: | Size: 620 B |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
BIN
tests/ref/issue-5244-consecutive-weak-space-heading.png
Normal file
After Width: | Height: | Size: 346 B |
BIN
tests/ref/issue-5244-consecutive-weak-space.png
Normal file
After Width: | Height: | Size: 194 B |
BIN
tests/ref/issue-5253-consecutive-weak-space-math.png
Normal file
After Width: | Height: | Size: 138 B |
BIN
tests/ref/issue-5256-multiple-footnotes-in-footnote.png
Normal file
After Width: | Height: | Size: 796 B |
BIN
tests/ref/issue-5296-block-sticky-in-block-at-top.png
Normal file
After Width: | Height: | Size: 277 B |
BIN
tests/ref/issue-5296-block-sticky-spaced-from-top-of-page.png
Normal file
After Width: | Height: | Size: 277 B |
After Width: | Height: | Size: 236 B |
BIN
tests/ref/issue-5370-figure-caption-separator-outline.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
tests/ref/issue-5435-footnote-migration-in-floats.png
Normal file
After Width: | Height: | Size: 448 B |
BIN
tests/ref/issue-5503-cite-group-interrupted-by-par-align.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
tests/ref/issue-5503-cite-in-align.png
Normal file
After Width: | Height: | Size: 393 B |
BIN
tests/ref/issue-5503-enum-interrupted-by-par-align.png
Normal file
After Width: | Height: | Size: 1004 B |
BIN
tests/ref/issue-5503-list-interrupted-by-par-align.png
Normal file
After Width: | Height: | Size: 415 B |