Compare commits

...

72 Commits

Author SHA1 Message Date
PgBiel
d13617ed9b skip layout of redundant gutter at the top of footer 2025-06-28 22:39:35 -03:00
PgBiel
315612b1f7 detect short lived headers and footers at the table edges
even if headers and footers are interleaved
2025-06-28 22:39:35 -03:00
PgBiel
f3cc3bdae7 fix space calculation of new footers
however, there are widows...
2025-06-28 22:39:35 -03:00
PgBiel
a2f5593174 improve check to pull next repeating footer 2025-06-28 22:39:35 -03:00
PgBiel
c346fb8589 initial proper subfooter unit tests 2025-06-28 22:39:35 -03:00
PgBiel
8f434146d8 clippy lints 2025-06-28 22:39:35 -03:00
PgBiel
40ae2324d1 test subfooters demo 2025-06-28 22:39:35 -03:00
PgBiel
858e620ef7 fix footer layout order and consecutive footer pushing 2025-06-28 22:39:35 -03:00
PgBiel
8c416b88f2 add footer level fields 2025-06-28 22:39:35 -03:00
PgBiel
eae79440b0 update multiple footer tests 2025-06-28 22:39:35 -03:00
PgBiel
7ee5dfaa89 fix footer layout range 2025-06-28 22:39:35 -03:00
PgBiel
183f47ecc0 use footer.range like header.range 2025-06-28 22:39:35 -03:00
PgBiel
bd7e403a6d fix last repeating footers not being pushed 2025-06-28 22:39:35 -03:00
PgBiel
b3fd4676c4 not using repeatable 2025-06-28 22:39:35 -03:00
PgBiel
0951fe13fd resolve multiple footers 2025-06-28 22:39:35 -03:00
PgBiel
f9b1bfd1b0 fix tfoot in table html 2025-06-28 22:39:35 -03:00
PgBiel
b26e004be9 fix footer widow check and rowspans 2025-06-28 22:39:35 -03:00
PgBiel
9422ecc74a fix footer progression 2025-06-28 22:39:35 -03:00
PgBiel
58db042ff3 support repeated footers in rowspan simulation 2025-06-28 22:39:35 -03:00
PgBiel
e89e3066a4 repeated method fixes 2025-06-28 22:39:35 -03:00
PgBiel
3de1237f54 temporary workaround for footer lines 2025-06-28 22:39:35 -03:00
PgBiel
b63f6c99df initial footer simulation and placement 2025-06-28 22:39:35 -03:00
PgBiel
db2ac385a9 move height resets to finish region internal 2025-06-28 22:39:35 -03:00
PgBiel
5f663a8da4 initial footer properties and bumping 2025-06-28 22:39:35 -03:00
PgBiel
3bf0f2b48c clone footers after sorting
might want to rethink this
2025-06-28 22:39:35 -03:00
PgBiel
0a27b50551 footer pre sorting 2025-06-28 22:39:35 -03:00
PgBiel
5292c5b198 update html code for multiple footers
todo: test
2025-06-28 22:39:35 -03:00
PgBiel
cce5fe739a multiple footers 2025-06-28 22:39:35 -03:00
Max
74b1b10986
Bump typst-dev-assets (#6514) 2025-06-27 10:35:05 +00:00
+merlan #flirora
584dd5fec6
Fix panic when sampling across two coincident gradient stops (#6166) 2025-06-27 09:26:15 +00:00
+merlan #flirora
b9f3a95e03
Sort line items by logical order when constructing frame (#5887)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-27 08:36:46 +00:00
Florian Bohlken
e8ce894ee7
Improve sentence in guide for LaTeX users (#6511) 2025-06-26 15:24:55 +00:00
Laurenz
9311f6f08e
Basic support for text decoration functions in HTML (#6510) 2025-06-26 13:44:45 +00:00
Laurenz
7420ec972f
Fix nested HTML frames (#6509) 2025-06-26 13:20:22 +00:00
Said A.
5dd5771df0
Disallow empty labels and references (#5776) (#6332)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-26 09:18:51 +00:00
Malo
04fd0acaca
Allow deprecating symbol variants (#6441) 2025-06-26 08:24:21 +00:00
Laurenz
6a1d6c08e2
Consistent sizing for html.frame (#6505) 2025-06-26 08:07:41 +00:00
Laurenz
35809387f8
Support in operator on strings and modules (#6498) 2025-06-26 08:06:22 +00:00
Connor K
d3caedd813
Fix typos in page-setup.md (#6499) 2025-06-25 16:59:19 +00:00
+merlan #flirora
d54544297b
Minor fixes to doc comments (#6500) 2025-06-25 16:58:40 +00:00
Laurenz
f2f527c451
Also fix encoding of <textarea> (#6497) 2025-06-24 15:52:15 +00:00
Laurenz
9e3c1199ed
Check that git tree is clean after build (#6495) 2025-06-24 15:05:02 +00:00
Tobias Schmitz
70399a94fd
Bump krilla to current Git version (#6488)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-24 13:23:37 +00:00
Andrew Voynov
d4be7c4ca5
Add page reference customization example (#6480)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-24 13:00:51 +00:00
Andrew Voynov
f162c37101
Improve equation reference example (#6481) 2025-06-24 12:49:28 +00:00
Andrew Voynov
87c5686560
Add docs for std module (#6407)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-24 10:22:55 +00:00
Max
899de6d5d5
Use ICU data to check if accent is bottom (#6393)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-24 10:03:10 +00:00
Andrew Voynov
24293a6c12
Rewrite outline.indent example (#6383)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-24 09:56:58 +00:00
Ivica Nakić
87cb8f5094
Adding Croatian translations entries (#6413) 2025-06-23 15:09:03 +00:00
Wannes Malfait
38dd6da237
Fix stroke cap of shapes with partial stroke (#5688) 2025-06-23 14:58:04 +00:00
Laurenz
bf8ef2a4a5 Properly handle raw text elements 2025-06-23 15:59:22 +02:00
Laurenz
c2e2fd99f6 Extract write_children function 2025-06-23 15:56:01 +02:00
Laurenz
f8dc1ad3bd Handle pre elements that start with a newline 2025-06-23 15:56:01 +02:00
Laurenz
9050ee1639 Turn non-empty void element into export error 2025-06-23 14:22:09 +02:00
Laurenz
c1b2aee1a9 Test runner support for HTML export errors 2025-06-23 14:21:35 +02:00
Laurenz
fbb02f40d9 Consistent codepoint formatting in HTML and PDF error messages 2025-06-23 14:18:41 +02:00
Laurenz
e9dc4bb204
Typed HTML API (#6476) 2025-06-23 09:12:58 +00:00
Laurenz
3602d06a15 Support for generating native functions at runtime 2025-06-20 17:32:37 +02:00
Laurenz
15302dbe7a Add typst_utils::display 2025-06-20 17:32:37 +02:00
Laurenz
4580daf307 More type-safe color conversions 2025-06-20 17:32:37 +02:00
Laurenz
d821633f50 Generic casting for Axes<T> 2025-06-20 17:32:37 +02:00
Laurenz
3b35f0cecf Add Duration::decompose 2025-06-20 17:32:37 +02:00
Laurenz
fee6844045 Encode empty attributes with shorthand syntax 2025-06-20 17:32:37 +02:00
Laurenz
f364b3c323
Fix param autocompletion false positive (#6475) 2025-06-20 12:32:04 +00:00
Noam Zaks
f1c761e88b
Fix align link in layout documentation (#6451) 2025-06-19 21:24:02 +00:00
Andrew Voynov
4588595792
Prefer .yaml over .yml in the docs (#6436) 2025-06-19 19:20:15 +00:00
Laurenz
0bc68df2a9
Hint for label in both document and bibliography (#6457) 2025-06-19 07:29:38 +00:00
Laurenz
f32cd5b3e1
Ensure that label repr is syntactically valid (#6456) 2025-06-19 07:29:35 +00:00
Laurenz
64d0a564bf
Better error message for compile time string interning failure (#6439) 2025-06-12 14:11:18 +00:00
cAttte
4a638f41cd
Consume data argument in pdf.embed() (#6435) 2025-06-12 14:10:04 +00:00
cAttte
f9897479d2
Unify EvalMode and LexMode into SyntaxMode (#6432) 2025-06-12 14:09:37 +00:00
Ilia
bd41fb9427
Check that all translation files are added to TRANSLATIONS and ends with newline (#6424)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-12 10:30:53 +00:00
161 changed files with 3709 additions and 1010 deletions

View File

@ -81,6 +81,7 @@ jobs:
- run: cargo clippy --workspace --all-targets --no-default-features - run: cargo clippy --workspace --all-targets --no-default-features
- run: cargo fmt --check --all - run: cargo fmt --check --all
- run: cargo doc --workspace --no-deps - run: cargo doc --workspace --no-deps
- run: git diff --exit-code
min-version: min-version:
name: Check minimum Rust version name: Check minimum Rust version

24
Cargo.lock generated
View File

@ -413,7 +413,7 @@ dependencies = [
[[package]] [[package]]
name = "codex" name = "codex"
version = "0.1.1" version = "0.1.1"
source = "git+https://github.com/typst/codex?rev=56eb217#56eb2172fc0670f4c1c8b79a63d11f9354e5babe" source = "git+https://github.com/typst/codex?rev=a5428cb#a5428cb9c81a41354d44b44dbd5a16a710bbd928"
[[package]] [[package]]
name = "color-print" name = "color-print"
@ -786,9 +786,9 @@ checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
[[package]] [[package]]
name = "font-types" name = "font-types"
version = "0.8.4" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa6a5e5a77b5f3f7f9e32879f484aa5b3632ddfbe568a16266c904a6f32cdaf" checksum = "02a596f5713680923a2080d86de50fe472fb290693cf0f701187a1c8b36996b7"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
] ]
@ -1367,8 +1367,7 @@ dependencies = [
[[package]] [[package]]
name = "krilla" name = "krilla"
version = "0.4.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/LaurenzV/krilla?rev=20c14fe#20c14fefee5002566b3d6668b338bbe2168784e7"
checksum = "69ee6128ebf52d7ce684613b6431ead2959f2be9ff8cf776eeaaad0427c953e9"
dependencies = [ dependencies = [
"base64", "base64",
"bumpalo", "bumpalo",
@ -1396,8 +1395,7 @@ dependencies = [
[[package]] [[package]]
name = "krilla-svg" name = "krilla-svg"
version = "0.1.0" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/LaurenzV/krilla?rev=20c14fe#20c14fefee5002566b3d6668b338bbe2168784e7"
checksum = "3462989578155cf620ef8035f8921533cc95c28e2a0c75de172f7219e6aba84e"
dependencies = [ dependencies = [
"flate2", "flate2",
"fontdb", "fontdb",
@ -2106,9 +2104,9 @@ dependencies = [
[[package]] [[package]]
name = "read-fonts" name = "read-fonts"
version = "0.28.0" version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "600e807b48ac55bad68a8cb75cc3c7739f139b9248f7e003e01e080f589b5288" checksum = "192735ef611aac958468e670cb98432c925426f3cb71521fda202130f7388d91"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"font-types", "font-types",
@ -2434,9 +2432,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]] [[package]]
name = "skrifa" name = "skrifa"
version = "0.30.0" version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fa1e5622e4f7b98877e8a19890efddcac1230cec6198bd9de91ec0e00010dc8" checksum = "e6d632b5a73f566303dbeabd344dc3e716fd4ddc9a70d6fc8ea8e6f06617da97"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"read-fonts", "read-fonts",
@ -2863,7 +2861,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-assets" name = "typst-assets"
version = "0.13.1" version = "0.13.1"
source = "git+https://github.com/typst/typst-assets?rev=c74e539#c74e539b090070a0c66fd007c550f5b6d3b724bd" source = "git+https://github.com/typst/typst-assets?rev=c1089b4#c1089b46c461bdde579c55caa941a3cc7dec3e8a"
[[package]] [[package]]
name = "typst-cli" name = "typst-cli"
@ -2913,7 +2911,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-dev-assets" name = "typst-dev-assets"
version = "0.13.1" version = "0.13.1"
source = "git+https://github.com/typst/typst-dev-assets?rev=fddbf8b#fddbf8b99506bc370ac0edcd4959add603a7fc92" source = "git+https://github.com/typst/typst-dev-assets?rev=bfa947f#bfa947f3433d7d13a995168c40ae788a2ebfe648"
[[package]] [[package]]
name = "typst-docs" name = "typst-docs"

View File

@ -32,8 +32,8 @@ typst-svg = { path = "crates/typst-svg", version = "0.13.1" }
typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" } typst-syntax = { path = "crates/typst-syntax", version = "0.13.1" }
typst-timing = { path = "crates/typst-timing", version = "0.13.1" } typst-timing = { path = "crates/typst-timing", version = "0.13.1" }
typst-utils = { path = "crates/typst-utils", version = "0.13.1" } typst-utils = { path = "crates/typst-utils", version = "0.13.1" }
typst-assets = { git = "https://github.com/typst/typst-assets", rev = "c74e539" } typst-assets = { git = "https://github.com/typst/typst-assets", rev = "c1089b4" }
typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "fddbf8b" } typst-dev-assets = { git = "https://github.com/typst/typst-dev-assets", rev = "bfa947f" }
arrayvec = "0.7.4" arrayvec = "0.7.4"
az = "1.2" az = "1.2"
base64 = "0.22" base64 = "0.22"
@ -47,7 +47,7 @@ clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
clap_complete = "4.2.1" clap_complete = "4.2.1"
clap_mangen = "0.2.10" clap_mangen = "0.2.10"
codespan-reporting = "0.11" codespan-reporting = "0.11"
codex = { git = "https://github.com/typst/codex", rev = "56eb217" } codex = { git = "https://github.com/typst/codex", rev = "a5428cb" }
color-print = "0.3.6" color-print = "0.3.6"
comemo = "0.4" comemo = "0.4"
csv = "1" csv = "1"
@ -73,8 +73,8 @@ image = { version = "0.25.5", default-features = false, features = ["png", "jpeg
indexmap = { version = "2", features = ["serde"] } indexmap = { version = "2", features = ["serde"] }
infer = { version = "0.19.0", default-features = false } infer = { version = "0.19.0", default-features = false }
kamadak-exif = "0.6" kamadak-exif = "0.6"
krilla = { version = "0.4.0", default-features = false, features = ["raster-images", "comemo", "rayon"] } krilla = { git = "https://github.com/LaurenzV/krilla", rev = "20c14fe", default-features = false, features = ["raster-images", "comemo", "rayon"] }
krilla-svg = "0.1.0" krilla-svg = { git = "https://github.com/LaurenzV/krilla", rev = "20c14fe" }
kurbo = "0.11" kurbo = "0.11"
libfuzzer-sys = "0.4" libfuzzer-sys = "0.4"
lipsum = "0.9" lipsum = "0.9"

View File

@ -5,9 +5,9 @@ use typst::diag::{bail, HintedStrResult, StrResult, Warned};
use typst::engine::Sink; use typst::engine::Sink;
use typst::foundations::{Content, IntoValue, LocatableSelector, Scope}; use typst::foundations::{Content, IntoValue, LocatableSelector, Scope};
use typst::layout::PagedDocument; use typst::layout::PagedDocument;
use typst::syntax::Span; use typst::syntax::{Span, SyntaxMode};
use typst::World; use typst::World;
use typst_eval::{eval_string, EvalMode}; use typst_eval::eval_string;
use crate::args::{QueryCommand, SerializationFormat}; use crate::args::{QueryCommand, SerializationFormat};
use crate::compile::print_diagnostics; use crate::compile::print_diagnostics;
@ -63,7 +63,7 @@ fn retrieve(
Sink::new().track_mut(), Sink::new().track_mut(),
&command.selector, &command.selector,
Span::detached(), Span::detached(),
EvalMode::Code, SyntaxMode::Code,
Scope::default(), Scope::default(),
) )
.map_err(|errors| { .map_err(|errors| {

View File

@ -18,7 +18,6 @@ pub use self::call::{eval_closure, CapturesVisitor};
pub use self::flow::FlowEvent; pub use self::flow::FlowEvent;
pub use self::import::import; pub use self::import::import;
pub use self::vm::Vm; pub use self::vm::Vm;
pub use typst_library::routines::EvalMode;
use self::access::*; use self::access::*;
use self::binding::*; use self::binding::*;
@ -32,7 +31,7 @@ use typst_library::introspection::Introspector;
use typst_library::math::EquationElem; use typst_library::math::EquationElem;
use typst_library::routines::Routines; use typst_library::routines::Routines;
use typst_library::World; use typst_library::World;
use typst_syntax::{ast, parse, parse_code, parse_math, Source, Span}; use typst_syntax::{ast, parse, parse_code, parse_math, Source, Span, SyntaxMode};
/// Evaluate a source file and return the resulting module. /// Evaluate a source file and return the resulting module.
#[comemo::memoize] #[comemo::memoize]
@ -104,13 +103,13 @@ pub fn eval_string(
sink: TrackedMut<Sink>, sink: TrackedMut<Sink>,
string: &str, string: &str,
span: Span, span: Span,
mode: EvalMode, mode: SyntaxMode,
scope: Scope, scope: Scope,
) -> SourceResult<Value> { ) -> SourceResult<Value> {
let mut root = match mode { let mut root = match mode {
EvalMode::Code => parse_code(string), SyntaxMode::Code => parse_code(string),
EvalMode::Markup => parse(string), SyntaxMode::Markup => parse(string),
EvalMode::Math => parse_math(string), SyntaxMode::Math => parse_math(string),
}; };
root.synthesize(span); root.synthesize(span);
@ -141,11 +140,11 @@ pub fn eval_string(
// Evaluate the code. // Evaluate the code.
let output = match mode { let output = match mode {
EvalMode::Code => root.cast::<ast::Code>().unwrap().eval(&mut vm)?, SyntaxMode::Code => root.cast::<ast::Code>().unwrap().eval(&mut vm)?,
EvalMode::Markup => { SyntaxMode::Markup => {
Value::Content(root.cast::<ast::Markup>().unwrap().eval(&mut vm)?) Value::Content(root.cast::<ast::Markup>().unwrap().eval(&mut vm)?)
} }
EvalMode::Math => Value::Content( SyntaxMode::Math => Value::Content(
EquationElem::new(root.cast::<ast::Math>().unwrap().eval(&mut vm)?) EquationElem::new(root.cast::<ast::Math>().unwrap().eval(&mut vm)?)
.with_block(false) .with_block(false)
.pack() .pack()

View File

@ -205,7 +205,9 @@ impl Eval for ast::Label<'_> {
type Output = Value; type Output = Value;
fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> { fn eval(self, _: &mut Vm) -> SourceResult<Self::Output> {
Ok(Value::Label(Label::new(PicoStr::intern(self.get())))) Ok(Value::Label(
Label::new(PicoStr::intern(self.get())).expect("unexpected empty label"),
))
} }
} }
@ -213,7 +215,8 @@ impl Eval for ast::Ref<'_> {
type Output = Content; type Output = Content;
fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> { fn eval(self, vm: &mut Vm) -> SourceResult<Self::Output> {
let target = Label::new(PicoStr::intern(self.target())); let target = Label::new(PicoStr::intern(self.target()))
.expect("unexpected empty reference");
let mut elem = RefElem::new(target); let mut elem = RefElem::new(target);
if let Some(supplement) = self.supplement() { if let Some(supplement) = self.supplement() {
elem.push_supplement(Smart::Custom(Some(Supplement::Content( elem.push_supplement(Smart::Custom(Some(Supplement::Content(

View File

@ -2,8 +2,9 @@ use std::fmt::Write;
use typst_library::diag::{bail, At, SourceResult, StrResult}; use typst_library::diag::{bail, At, SourceResult, StrResult};
use typst_library::foundations::Repr; use typst_library::foundations::Repr;
use typst_library::html::{charsets, tag, HtmlDocument, HtmlElement, HtmlNode, HtmlTag}; use typst_library::html::{
use typst_library::layout::Frame; attr, charsets, tag, HtmlDocument, HtmlElement, HtmlFrame, HtmlNode, HtmlTag,
};
use typst_syntax::Span; use typst_syntax::Span;
/// Encodes an HTML document into a string. /// Encodes an HTML document into a string.
@ -28,7 +29,7 @@ struct Writer {
pretty: bool, pretty: bool,
} }
/// Write a newline and indent, if pretty printing is enabled. /// Writes a newline and indent, if pretty printing is enabled.
fn write_indent(w: &mut Writer) { fn write_indent(w: &mut Writer) {
if w.pretty { if w.pretty {
w.buf.push('\n'); w.buf.push('\n');
@ -38,7 +39,7 @@ fn write_indent(w: &mut Writer) {
} }
} }
/// Encode an HTML node into the writer. /// Encodes an HTML node into the writer.
fn write_node(w: &mut Writer, node: &HtmlNode) -> SourceResult<()> { fn write_node(w: &mut Writer, node: &HtmlNode) -> SourceResult<()> {
match node { match node {
HtmlNode::Tag(_) => {} HtmlNode::Tag(_) => {}
@ -49,7 +50,7 @@ fn write_node(w: &mut Writer, node: &HtmlNode) -> SourceResult<()> {
Ok(()) Ok(())
} }
/// Encode plain text into the writer. /// Encodes plain text into the writer.
fn write_text(w: &mut Writer, text: &str, span: Span) -> SourceResult<()> { fn write_text(w: &mut Writer, text: &str, span: Span) -> SourceResult<()> {
for c in text.chars() { for c in text.chars() {
if charsets::is_valid_in_normal_element_text(c) { if charsets::is_valid_in_normal_element_text(c) {
@ -61,7 +62,7 @@ fn write_text(w: &mut Writer, text: &str, span: Span) -> SourceResult<()> {
Ok(()) Ok(())
} }
/// Encode one element into the write. /// Encodes one element into the writer.
fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> { fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
w.buf.push('<'); w.buf.push('<');
w.buf.push_str(&element.tag.resolve()); w.buf.push_str(&element.tag.resolve());
@ -69,6 +70,10 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
for (attr, value) in &element.attrs.0 { for (attr, value) in &element.attrs.0 {
w.buf.push(' '); w.buf.push(' ');
w.buf.push_str(&attr.resolve()); w.buf.push_str(&attr.resolve());
// If the string is empty, we can use shorthand syntax.
// `<elem attr="">..</div` is equivalent to `<elem attr>..</div>`
if !value.is_empty() {
w.buf.push('='); w.buf.push('=');
w.buf.push('"'); w.buf.push('"');
for c in value.chars() { for c in value.chars() {
@ -80,15 +85,38 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
} }
w.buf.push('"'); w.buf.push('"');
} }
}
w.buf.push('>'); w.buf.push('>');
if tag::is_void(element.tag) { if tag::is_void(element.tag) {
if !element.children.is_empty() {
bail!(element.span, "HTML void elements must not have children");
}
return Ok(()); return Ok(());
} }
if tag::is_raw(element.tag) {
write_raw(w, element)?;
} else if !element.children.is_empty() {
write_children(w, element)?;
}
w.buf.push_str("</");
w.buf.push_str(&element.tag.resolve());
w.buf.push('>');
Ok(())
}
/// Encodes the children of an element.
fn write_children(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
// See HTML spec § 13.1.2.5.
if matches!(element.tag, tag::pre | tag::textarea) && starts_with_newline(element) {
w.buf.push('\n');
}
let pretty = w.pretty; let pretty = w.pretty;
if !element.children.is_empty() {
let pretty_inside = allows_pretty_inside(element.tag) let pretty_inside = allows_pretty_inside(element.tag)
&& element.children.iter().any(|node| match node { && element.children.iter().any(|node| match node {
HtmlNode::Element(child) => wants_pretty_around(child.tag), HtmlNode::Element(child) => wants_pretty_around(child.tag),
@ -115,16 +143,125 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
w.level -= 1; w.level -= 1;
write_indent(w); write_indent(w);
}
w.pretty = pretty; w.pretty = pretty;
w.buf.push_str("</"); Ok(())
w.buf.push_str(&element.tag.resolve()); }
w.buf.push('>');
/// Whether the first character in the element is a newline.
fn starts_with_newline(element: &HtmlElement) -> bool {
for child in &element.children {
match child {
HtmlNode::Tag(_) => {}
HtmlNode::Text(text, _) => return text.starts_with(['\n', '\r']),
_ => return false,
}
}
false
}
/// Encodes the contents of a raw text element.
fn write_raw(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
let text = collect_raw_text(element)?;
if let Some(closing) = find_closing_tag(&text, element.tag) {
bail!(
element.span,
"HTML raw text element cannot contain its own closing tag";
hint: "the sequence `{closing}` appears in the raw text",
)
}
let mode = if w.pretty { RawMode::of(element, &text) } else { RawMode::Keep };
match mode {
RawMode::Keep => {
w.buf.push_str(&text);
}
RawMode::Wrap => {
w.buf.push('\n');
w.buf.push_str(&text);
write_indent(w);
}
RawMode::Indent => {
w.level += 1;
for line in text.lines() {
write_indent(w);
w.buf.push_str(line);
}
w.level -= 1;
write_indent(w);
}
}
Ok(()) Ok(())
} }
/// Collects the textual contents of a raw text element.
fn collect_raw_text(element: &HtmlElement) -> SourceResult<String> {
let mut output = String::new();
for c in &element.children {
match c {
HtmlNode::Tag(_) => continue,
HtmlNode::Text(text, _) => output.push_str(text),
HtmlNode::Element(_) | HtmlNode::Frame(_) => {
let span = match c {
HtmlNode::Element(child) => child.span,
_ => element.span,
};
bail!(span, "HTML raw text element cannot have non-text children")
}
};
}
Ok(output)
}
/// Finds a closing sequence for the given tag in the text, if it exists.
///
/// See HTML spec § 13.1.2.6.
fn find_closing_tag(text: &str, tag: HtmlTag) -> Option<&str> {
let s = tag.resolve();
let len = s.len();
text.match_indices("</").find_map(|(i, _)| {
let rest = &text[i + 2..];
let disallowed = rest.len() >= len
&& rest[..len].eq_ignore_ascii_case(&s)
&& rest[len..].starts_with(['\t', '\n', '\u{c}', '\r', ' ', '>', '/']);
disallowed.then(|| &text[i..i + 2 + len])
})
}
/// How to format the contents of a raw text element.
enum RawMode {
/// Just don't touch it.
Keep,
/// Newline after the opening and newline + indent before the closing tag.
Wrap,
/// Newlines after opening and before closing tag and each line indented.
Indent,
}
impl RawMode {
fn of(element: &HtmlElement, text: &str) -> Self {
match element.tag {
tag::script
if !element.attrs.0.iter().any(|(attr, value)| {
*attr == attr::r#type && value != "text/javascript"
}) =>
{
// Template literals can be multi-line, so indent may change
// the semantics of the JavaScript.
if text.contains('`') {
Self::Wrap
} else {
Self::Indent
}
}
tag::style => Self::Indent,
_ => Self::Keep,
}
}
}
/// Whether we are allowed to add an extra newline at the start and end of the /// Whether we are allowed to add an extra newline at the start and end of the
/// element's contents. /// element's contents.
/// ///
@ -160,15 +297,21 @@ fn write_escape(w: &mut Writer, c: char) -> StrResult<()> {
c if charsets::is_w3c_text_char(c) && c != '\r' => { c if charsets::is_w3c_text_char(c) && c != '\r' => {
write!(w.buf, "&#x{:x};", c as u32).unwrap() write!(w.buf, "&#x{:x};", c as u32).unwrap()
} }
_ => bail!("the character {} cannot be encoded in HTML", c.repr()), _ => bail!("the character `{}` cannot be encoded in HTML", c.repr()),
} }
Ok(()) Ok(())
} }
/// Encode a laid out frame into the writer. /// Encode a laid out frame into the writer.
fn write_frame(w: &mut Writer, frame: &Frame) { fn write_frame(w: &mut Writer, frame: &HtmlFrame) {
// FIXME: This string replacement is obviously a hack. // FIXME: This string replacement is obviously a hack.
let svg = typst_svg::svg_frame(frame) let svg = typst_svg::svg_frame(&frame.inner).replace(
.replace("<svg class", "<svg style=\"overflow: visible;\" class"); "<svg class",
&format!(
"<svg style=\"overflow: visible; width: {}em; height: {}em;\" class",
frame.inner.width() / frame.text_size,
frame.inner.height() / frame.text_size,
),
);
w.buf.push_str(&svg); w.buf.push_str(&svg);
} }

View File

@ -9,7 +9,7 @@ use typst_library::diag::{bail, warning, At, SourceResult};
use typst_library::engine::{Engine, Route, Sink, Traced}; use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{Content, StyleChain, Target, TargetElem}; use typst_library::foundations::{Content, StyleChain, Target, TargetElem};
use typst_library::html::{ use typst_library::html::{
attr, tag, FrameElem, HtmlDocument, HtmlElem, HtmlElement, HtmlNode, attr, tag, FrameElem, HtmlDocument, HtmlElem, HtmlElement, HtmlFrame, HtmlNode,
}; };
use typst_library::introspection::{ use typst_library::introspection::{
Introspector, Locator, LocatorLink, SplitLocator, TagElem, Introspector, Locator, LocatorLink, SplitLocator, TagElem,
@ -180,9 +180,6 @@ fn handle(
if let Some(body) = elem.body(styles) { if let Some(body) = elem.body(styles) {
children = html_fragment(engine, body, locator.next(&elem.span()), styles)?; children = html_fragment(engine, body, locator.next(&elem.span()), styles)?;
} }
if tag::is_void(elem.tag) && !children.is_empty() {
bail!(elem.span(), "HTML void elements may not have children");
}
let element = HtmlElement { let element = HtmlElement {
tag: elem.tag, tag: elem.tag,
attrs: elem.attrs(styles).clone(), attrs: elem.attrs(styles).clone(),
@ -249,7 +246,10 @@ fn handle(
styles.chain(&style), styles.chain(&style),
Region::new(Size::splat(Abs::inf()), Axes::splat(false)), Region::new(Size::splat(Abs::inf()), Axes::splat(false)),
)?; )?;
output.push(HtmlNode::Frame(frame)); output.push(HtmlNode::Frame(HtmlFrame {
inner: frame,
text_size: TextElem::size_in(styles),
}));
} else { } else {
engine.sink.warn(warning!( engine.sink.warn(warning!(
child.span(), child.span(),

View File

@ -448,7 +448,7 @@ fn field_access_completions(
match value { match value {
Value::Symbol(symbol) => { Value::Symbol(symbol) => {
for modifier in symbol.modifiers() { for modifier in symbol.modifiers() {
if let Ok(modified) = symbol.clone().modified(modifier) { if let Ok(modified) = symbol.clone().modified((), modifier) {
ctx.completions.push(Completion { ctx.completions.push(Completion {
kind: CompletionKind::Symbol(modified.get()), kind: CompletionKind::Symbol(modified.get()),
label: modifier.into(), label: modifier.into(),
@ -701,7 +701,10 @@ fn complete_params(ctx: &mut CompletionContext) -> bool {
let mut deciding = ctx.leaf.clone(); let mut deciding = ctx.leaf.clone();
while !matches!( while !matches!(
deciding.kind(), deciding.kind(),
SyntaxKind::LeftParen | SyntaxKind::Comma | SyntaxKind::Colon SyntaxKind::LeftParen
| SyntaxKind::RightParen
| SyntaxKind::Comma
| SyntaxKind::Colon
) { ) {
let Some(prev) = deciding.prev_leaf() else { break }; let Some(prev) = deciding.prev_leaf() else { break };
deciding = prev; deciding = prev;
@ -1734,6 +1737,8 @@ mod tests {
test("#numbering(\"foo\", 1, )", -2) test("#numbering(\"foo\", 1, )", -2)
.must_include(["integer"]) .must_include(["integer"])
.must_exclude(["string"]); .must_exclude(["string"]);
// After argument list no completions.
test("#numbering()", -1).must_exclude(["string"]);
} }
/// Test that autocompletion for values of known type picks up nested /// Test that autocompletion for values of known type picks up nested
@ -1829,18 +1834,27 @@ mod tests {
#[test] #[test]
fn test_autocomplete_fonts() { fn test_autocomplete_fonts() {
test("#text(font:)", -1) test("#text(font:)", -2)
.must_include(["\"Libertinus Serif\"", "\"New Computer Modern Math\""]); .must_include(["\"Libertinus Serif\"", "\"New Computer Modern Math\""]);
test("#show link: set text(font: )", -1) test("#show link: set text(font: )", -2)
.must_include(["\"Libertinus Serif\"", "\"New Computer Modern Math\""]); .must_include(["\"Libertinus Serif\"", "\"New Computer Modern Math\""]);
test("#show math.equation: set text(font: )", -1) test("#show math.equation: set text(font: )", -2)
.must_include(["\"New Computer Modern Math\""]) .must_include(["\"New Computer Modern Math\""])
.must_exclude(["\"Libertinus Serif\""]); .must_exclude(["\"Libertinus Serif\""]);
test("#show math.equation: it => { set text(font: )\nit }", -6) test("#show math.equation: it => { set text(font: )\nit }", -7)
.must_include(["\"New Computer Modern Math\""]) .must_include(["\"New Computer Modern Math\""])
.must_exclude(["\"Libertinus Serif\""]); .must_exclude(["\"Libertinus Serif\""]);
} }
#[test]
fn test_autocomplete_typed_html() {
test("#html.div(translate: )", -2)
.must_include(["true", "false"])
.must_exclude(["\"yes\"", "\"no\""]);
test("#html.input(value: )", -2).must_include(["float", "string", "red", "blue"]);
test("#html.div(role: )", -2).must_include(["\"alertdialog\""]);
}
} }

View File

@ -72,7 +72,8 @@ pub fn definition(
// Try to jump to the referenced content. // Try to jump to the referenced content.
DerefTarget::Ref(node) => { DerefTarget::Ref(node) => {
let label = Label::new(PicoStr::intern(node.cast::<ast::Ref>()?.target())); let label = Label::new(PicoStr::intern(node.cast::<ast::Ref>()?.target()))
.expect("unexpected empty reference");
let selector = Selector::Label(label); let selector = Selector::Label(label);
let elem = document?.introspector.query_first(&selector)?; let elem = document?.introspector.query_first(&selector)?;
return Some(Definition::Span(elem.span())); return Some(Definition::Span(elem.span()));

View File

@ -10,7 +10,7 @@ use typst::syntax::package::{PackageSpec, PackageVersion};
use typst::syntax::{FileId, Source, VirtualPath}; use typst::syntax::{FileId, Source, VirtualPath};
use typst::text::{Font, FontBook, TextElem, TextSize}; use typst::text::{Font, FontBook, TextElem, TextSize};
use typst::utils::{singleton, LazyHash}; use typst::utils::{singleton, LazyHash};
use typst::{Library, World}; use typst::{Feature, Library, World};
use crate::IdeWorld; use crate::IdeWorld;
@ -168,7 +168,9 @@ fn library() -> Library {
// Set page width to 120pt with 10pt margins, so that the inner page is // Set page width to 120pt with 10pt margins, so that the inner page is
// exactly 100pt wide. Page height is unbounded and font size is 10pt so // exactly 100pt wide. Page height is unbounded and font size is 10pt so
// that it multiplies to nice round numbers. // that it multiplies to nice round numbers.
let mut lib = typst::Library::default(); let mut lib = typst::Library::builder()
.with_features([Feature::Html].into_iter().collect())
.build();
lib.styles lib.styles
.set(PageElem::set_width(Smart::Custom(Abs::pt(120.0).into()))); .set(PageElem::set_width(Smart::Custom(Abs::pt(120.0).into())));
lib.styles.set(PageElem::set_height(Smart::Auto)); lib.styles.set(PageElem::set_height(Smart::Auto));
@ -202,7 +204,8 @@ impl WorldLike for &str {
} }
} }
/// Specifies a position in a file for a test. /// Specifies a position in a file for a test. Negative numbers index from the
/// back. `-1` is at the very back.
pub trait FilePos { pub trait FilePos {
fn resolve(self, world: &TestWorld) -> (Source, usize); fn resolve(self, world: &TestWorld) -> (Source, usize);
} }

View File

@ -6,6 +6,7 @@ use typst_library::foundations::{Resolve, StyleChain};
use typst_library::layout::grid::resolve::{ use typst_library::layout::grid::resolve::{
Cell, CellGrid, Header, LinePosition, Repeatable, Cell, CellGrid, Header, LinePosition, Repeatable,
}; };
use typst_library::layout::resolve::Footer;
use typst_library::layout::{ use typst_library::layout::{
Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel, Abs, Axes, Dir, Fr, Fragment, Frame, FrameItem, Length, Point, Region, Regions, Rel,
Size, Sizing, Size, Sizing,
@ -60,6 +61,16 @@ pub struct GridLayouter<'a> {
pub(super) pending_headers: &'a [Repeatable<Header>], pub(super) pending_headers: &'a [Repeatable<Header>],
/// Next headers to be processed. /// Next headers to be processed.
pub(super) upcoming_headers: &'a [Repeatable<Header>], pub(super) upcoming_headers: &'a [Repeatable<Header>],
/// Currently repeating footers, one per level. Sorted by increasing
/// levels.
///
/// Note that some levels may be absent, in particular level 0, which does
/// not exist (so all levels are >= 1).
pub(super) repeating_footers: Vec<&'a Footer>,
/// Next footers to be processed.
pub(super) upcoming_footers: &'a [Repeatable<Footer>],
/// Next footers sorted by when they start repeating.
pub(super) upcoming_sorted_footers: &'a [Repeatable<Footer>],
/// State of the row being currently laid out. /// State of the row being currently laid out.
/// ///
/// This is kept as a field to avoid passing down too many parameters from /// This is kept as a field to avoid passing down too many parameters from
@ -155,6 +166,12 @@ pub(super) struct Current {
/// when finding a new header and causing existing repeating headers to /// when finding a new header and causing existing repeating headers to
/// stop. /// stop.
pub(super) repeating_header_heights: Vec<Abs>, pub(super) repeating_header_heights: Vec<Abs>,
/// The height for each repeating footer that will be placed in this region.
///
/// This is used to know how much to update `repeating_footer_height` by
/// when finding a footer and causing existing repeating footers to
/// stop (and new ones to start).
pub(super) repeating_footer_heights: Vec<Abs>,
/// The simulated footer height for this region. /// The simulated footer height for this region.
/// ///
/// The simulation occurs before any rows are laid out for a region. /// The simulation occurs before any rows are laid out for a region.
@ -215,7 +232,7 @@ pub(super) enum Row {
impl Row { impl Row {
/// Returns the `y` index of this row. /// Returns the `y` index of this row.
fn index(&self) -> usize { pub(super) fn index(&self) -> usize {
match self { match self {
Self::Frame(_, y, _) => *y, Self::Frame(_, y, _) => *y,
Self::Fr(_, y, _) => *y, Self::Fr(_, y, _) => *y,
@ -253,6 +270,10 @@ impl<'a> GridLayouter<'a> {
repeating_headers: vec![], repeating_headers: vec![],
upcoming_headers: &grid.headers, upcoming_headers: &grid.headers,
pending_headers: Default::default(), pending_headers: Default::default(),
// This is updated on layout
repeating_footers: vec![],
upcoming_footers: &grid.footers,
upcoming_sorted_footers: &grid.sorted_footers,
row_state: RowState::default(), row_state: RowState::default(),
current: Current { current: Current {
initial: regions.size, initial: regions.size,
@ -264,6 +285,7 @@ impl<'a> GridLayouter<'a> {
lrows_orphan_snapshot: None, lrows_orphan_snapshot: None,
repeating_header_height: Abs::zero(), repeating_header_height: Abs::zero(),
repeating_header_heights: vec![], repeating_header_heights: vec![],
repeating_footer_heights: vec![],
footer_height: Abs::zero(), footer_height: Abs::zero(),
}, },
span, span,
@ -274,15 +296,7 @@ impl<'a> GridLayouter<'a> {
pub fn layout(mut self, engine: &mut Engine) -> SourceResult<Fragment> { pub fn layout(mut self, engine: &mut Engine) -> SourceResult<Fragment> {
self.measure_columns(engine)?; self.measure_columns(engine)?;
if let Some(footer) = &self.grid.footer { self.prepare_next_repeating_footers(true, engine)?;
if footer.repeated {
// Ensure rows in the first region will be aware of the
// possible presence of the footer.
self.prepare_footer(footer, engine, 0)?;
self.regions.size.y -= self.current.footer_height;
self.current.initial_after_repeats = self.regions.size.y;
}
}
let mut y = 0; let mut y = 0;
let mut consecutive_header_count = 0; let mut consecutive_header_count = 0;
@ -298,13 +312,15 @@ impl<'a> GridLayouter<'a> {
} }
} }
if let Some(footer) = &self.grid.footer { if let [next_footer, other_footers @ ..] = self.upcoming_footers {
if footer.repeated && y >= footer.start { // TODO(subfooters): effective range (consider gutter before
if y == footer.start { // if it was removed)
self.layout_footer(footer, engine, self.finished.len())?; if next_footer.range.contains(&y) {
self.upcoming_footers = other_footers;
self.place_new_footer(engine, next_footer)?;
self.flush_orphans(); self.flush_orphans();
} y = next_footer.range.end;
y = footer.end;
continue; continue;
} }
} }
@ -1566,26 +1582,34 @@ impl<'a> GridLayouter<'a> {
// TODO(subfooters): explicitly check for short-lived footers. // TODO(subfooters): explicitly check for short-lived footers.
// TODO(subfooters): widow prevention for non-repeated footers with a // TODO(subfooters): widow prevention for non-repeated footers with a
// similar mechanism / when implementing multiple footers. // similar mechanism / when implementing multiple footers.
let footer_would_be_widow = matches!(&self.grid.footer, Some(footer) if footer.repeated) // TODO(subfooters): could progress check must be replaced to consider
&& self.current.lrows.is_empty() // the presence of non-repeating footer (then always true).
&& self.current.could_progress_at_top; let may_place_footers = !self.repeating_footers.is_empty()
&& (!self.current.lrows.is_empty() || !self.current.could_progress_at_top);
let mut laid_out_footer_start = None; if may_place_footers {
if !footer_would_be_widow {
if let Some(footer) = &self.grid.footer {
// Don't layout the footer if it would be alone with the header // Don't layout the footer if it would be alone with the header
// in the page (hence the widow check), and don't layout it // in the page (hence the widow check), and don't layout it
// twice (check below). // twice (it is removed from repeating_footers once it is
// reached).
// //
// TODO(subfooters): this check can be replaced by a vector of // Use index for iteration to avoid borrow conflict.
// repeating footers in the future, and/or some "pending //
// footers" vector for footers we're about to place. // Note that repeating footers are in reverse order.
if footer.repeated //
&& self.current.lrows.iter().all(|row| row.index() < footer.start) // TODO(subfooters): "pending footers" vector for footers we're
// about to place. Needed for widow prevention of non-repeated
// footers.
let mut i = 0;
while let Some(footer_index) = self.repeating_footers.len().checked_sub(1 + i)
{ {
laid_out_footer_start = Some(footer.start); self.layout_footer(
self.layout_footer(footer, engine, self.finished.len())?; self.repeating_footers[footer_index],
} false,
engine,
self.finished.len(),
)?;
i += 1;
} }
} }
@ -1684,11 +1708,23 @@ impl<'a> GridLayouter<'a> {
// laid out at the first frame of the row). // laid out at the first frame of the row).
// Any rowspans ending before this row are laid out even // Any rowspans ending before this row are laid out even
// on this row's first frame. // on this row's first frame.
if laid_out_footer_start.is_none_or(|footer_start| { if (!may_place_footers
|| self.repeating_footers.iter().all(|footer| {
// If this is a footer row, then only lay out this rowspan // If this is a footer row, then only lay out this rowspan
// if the rowspan is contained within the footer. // if the rowspan is contained within the footer.
y < footer_start || rowspan.y >= footer_start // Since the footer is a row from "the future", it
}) && (rowspan.y + rowspan.rowspan < y + 1 // always has a larger Y than all active rowspans,
// so we must not interpret a rowspan before it to have
// already ended because we saw a repeated footer.
//
// Of course, not a concern for non-repeated or
// short-lived footers as they only appear once.
//
// TODO(subfooters): use effective range
// (what about the gutter?).
!footer.range.contains(&y) || footer.range.contains(&rowspan.y)
}))
&& (rowspan.y + rowspan.rowspan < y + 1
|| rowspan.y + rowspan.rowspan == y + 1 && is_last) || rowspan.y + rowspan.rowspan == y + 1 && is_last)
{ {
// Rowspan ends at this or an earlier row, so we take // Rowspan ends at this or an earlier row, so we take
@ -1732,25 +1768,18 @@ impl<'a> GridLayouter<'a> {
); );
if !last { if !last {
self.current.repeated_header_rows = 0;
self.current.last_repeated_header_end = 0;
self.current.repeating_header_height = Abs::zero();
self.current.repeating_header_heights.clear();
let disambiguator = self.finished.len(); let disambiguator = self.finished.len();
if let Some(footer) = if !self.repeating_footers.is_empty() {
self.grid.footer.as_ref().and_then(Repeatable::as_repeated) // TODO(subfooters): let's not...
{ let footers = self.repeating_footers.clone();
self.prepare_footer(footer, engine, disambiguator)?; self.prepare_repeating_footers(
footers.iter().copied(),
true,
engine,
disambiguator,
)?;
} }
// Ensure rows don't try to overrun the footer.
// Note that header layout will only subtract this again if it has
// to skip regions to fit headers, so there is no risk of
// subtracting this twice.
self.regions.size.y -= self.current.footer_height;
self.current.initial_after_repeats = self.regions.size.y;
if !self.repeating_headers.is_empty() || !self.pending_headers.is_empty() { if !self.repeating_headers.is_empty() || !self.pending_headers.is_empty() {
// Add headers to the new region. // Add headers to the new region.
self.layout_active_headers(engine)?; self.layout_active_headers(engine)?;
@ -1780,6 +1809,13 @@ impl<'a> GridLayouter<'a> {
self.current.could_progress_at_top = self.regions.may_progress(); self.current.could_progress_at_top = self.regions.may_progress();
self.current.repeated_header_rows = 0;
self.current.last_repeated_header_end = 0;
self.current.repeating_header_height = Abs::zero();
self.current.repeating_header_heights.clear();
self.current.footer_height = Abs::zero();
self.current.repeating_footer_heights.clear();
if !self.grid.headers.is_empty() { if !self.grid.headers.is_empty() {
self.finished_header_rows.push(header_row_info); self.finished_header_rows.push(header_row_info);
} }

View File

@ -512,15 +512,18 @@ pub fn hline_stroke_at_column(
); );
// Prioritize the footer's top stroke as well where applicable. // Prioritize the footer's top stroke as well where applicable.
// TODO(subfooters): do this properly (store footer rows)
let bottom_stroke_comes_from_footer = grid let bottom_stroke_comes_from_footer = grid
.footer .footers
.as_ref() .last()
.and_then(Repeatable::as_repeated) .and_then(Repeatable::as_repeated)
.is_some_and(|footer| { .is_some_and(|footer| {
// Ensure the row below us is a repeated footer. // Ensure the row below us is a repeated footer.
// FIXME: Make this check more robust when footers at arbitrary // FIXME: Make this check more robust when footers at arbitrary
// positions are added. // positions are added.
local_top_y.unwrap_or(0) + 1 < footer.start && y >= footer.start footer.range.end == grid.rows.len()
&& local_top_y.unwrap_or(0) + 1 < footer.range.start
&& y >= footer.range.start
}); });
let (prioritized_cell_stroke, deprioritized_cell_stroke) = let (prioritized_cell_stroke, deprioritized_cell_stroke) =
@ -638,7 +641,7 @@ mod test {
vec![], vec![],
vec![], vec![],
vec![], vec![],
None, vec![],
entries, entries,
) )
} }
@ -1176,7 +1179,7 @@ mod test {
vec![], vec![],
vec![], vec![],
vec![], vec![],
None, vec![],
entries, entries,
) )
} }

View File

@ -240,16 +240,18 @@ impl<'a> GridLayouter<'a> {
self.current.initial_after_repeats = self.regions.size.y; self.current.initial_after_repeats = self.regions.size.y;
} }
if let Some(footer) = &self.grid.footer { if !self.repeating_footers.is_empty() && skipped_region {
if footer.repeated && skipped_region {
// Simulate the footer again; the region's 'full' might have // Simulate the footer again; the region's 'full' might have
// changed. // changed.
self.regions.size.y += self.current.footer_height; let (footer_height, footer_heights) = self.simulate_footer_heights(
self.current.footer_height = self self.repeating_footers.iter().copied(),
.simulate_footer(footer, &self.regions, engine, disambiguator)? &self.regions,
.height; engine,
self.regions.size.y -= self.current.footer_height; disambiguator,
} )?;
self.current.footer_height = footer_height;
self.current.repeating_footer_heights.extend(footer_heights);
} }
let repeating_header_rows = let repeating_header_rows =
@ -463,74 +465,243 @@ impl<'a> GridLayouter<'a> {
) )
} }
/// Updates `self.footer_height` by simulating the footer, and skips to fitting region. /// Place a footer we have reached through normal row layout.
pub fn prepare_footer( pub fn place_new_footer(
&mut self, &mut self,
footer: &Footer, engine: &mut Engine,
footer: &Repeatable<Footer>,
) -> SourceResult<()> {
// TODO(subfooters): short-lived check
if !footer.repeated {
// TODO(subfooters): widow prevention for this.
// Will need some lookahead. For now, act as short-lived.
let footer_height =
self.simulate_footer(footer, &self.regions, engine, 0)?.height;
// Skip to fitting region where only this footer fits.
while self.unbreakable_rows_left == 0
&& !self.regions.size.y.fits(footer_height)
&& self.may_progress_with_repeats()
{
// Advance regions until we can place the footer.
// Treat as a normal row group.
self.finish_region(engine, false)?;
}
self.layout_footer(footer, true, engine, 0)?;
} else {
// Placing a non-short-lived repeating footer, so it must be
// the latest one in the repeating footers vector.
let latest_repeating_footer = self.repeating_footers.pop().unwrap();
assert_eq!(latest_repeating_footer.range.start, footer.range.start);
let expected_footer_height =
self.current.repeating_footer_heights.pop().unwrap();
// Ensure upcoming rows won't see that this footer will occupy
// any space in future regions anymore.
self.current.footer_height -= expected_footer_height;
// Ensure footer rows have their own expected height
// available. While not that relevant for them, as they will be
// laid out as an unbreakable row group, it's relevant for any
// further rows in the same region.
self.regions.size.y += expected_footer_height;
self.layout_footer(footer, false, engine, self.finished.len())?;
}
// If the next group of footers would conflict with other repeating
// footers, wait for them to finish repeating before adding more to
// repeat.
if self.repeating_footers.is_empty()
|| self
.upcoming_sorted_footers
.first()
.is_some_and(|f| f.level >= footer.level)
{
self.prepare_next_repeating_footers(false, engine)?;
}
Ok(())
}
/// Takes all non-conflicting consecutive footers which are about to start
/// repeating, skips to the first region where they all fit, and pushes
/// them to `repeating_footers`, sorted by ascending levels.
pub fn prepare_next_repeating_footers(
&mut self,
first_footers: bool,
engine: &mut Engine,
) -> SourceResult<()> {
let [next_footer, other_footers @ ..] = self.upcoming_sorted_footers else {
// No footers to take.
return Ok(());
};
// TODO(subfooters): also ignore short-lived footers.
if !next_footer.repeated {
// Skip this footer and don't do anything until we get to it.
//
// TODO(subfooters): grouping and laying out non-repeated with
// repeated, with widow prevention.
self.upcoming_sorted_footers = other_footers;
return Ok(());
}
// Collect upcoming consecutive footers, they will start repeating with
// this one if compatible
let mut min_level = next_footer.level;
let first_conflicting_index = other_footers
.iter()
.take_while(|f| {
// TODO(subfooters): check for short-lived
let compatible = f.repeated && f.level > min_level;
min_level = f.level;
compatible
})
.count()
+ 1;
let (next_repeating_footers, new_upcoming_footers) =
self.upcoming_sorted_footers.split_at(first_conflicting_index);
self.upcoming_sorted_footers = new_upcoming_footers;
self.prepare_repeating_footers(
next_repeating_footers.iter().map(Repeatable::deref),
first_footers,
engine,
0,
)?;
self.repeating_footers
.extend(next_repeating_footers.iter().filter_map(Repeatable::as_repeated));
Ok(())
}
/// Updates `self.current.repeating_footer_height` by simulating repeating
/// footers, and skips to fitting region.
pub fn prepare_repeating_footers(
&mut self,
footers: impl ExactSizeIterator<Item = &'a Footer> + Clone,
at_region_top: bool,
engine: &mut Engine, engine: &mut Engine,
disambiguator: usize, disambiguator: usize,
) -> SourceResult<()> { ) -> SourceResult<()> {
let footer_height = self let (mut expected_footer_height, mut expected_footer_heights) = self
.simulate_footer(footer, &self.regions, engine, disambiguator)? .simulate_footer_heights(
.height; footers.clone(),
&self.regions,
engine,
disambiguator,
)?;
// Skip to fitting region where all of them fit at once.
//
// Can't be widows: they are assumed to not be short-lived, so
// there is at least one non-footer before them, and this
// function is called right after placing a new footer, but
// before the next non-footer, or at the top of the region,
// at which point we haven't reached the row before the highest
// level footer yet since the footer itself won't cause a
// region break.
let mut skipped_region = false; let mut skipped_region = false;
while self.unbreakable_rows_left == 0 while self.unbreakable_rows_left == 0
&& !self.regions.size.y.fits(footer_height) && !self.regions.size.y.fits(expected_footer_height)
&& self.regions.may_progress() && self.regions.may_progress()
{ {
// Advance regions without any output until we can place the // Advance regions without any output until we can place the
// footer. // footer.
if at_region_top {
self.finish_region_internal( self.finish_region_internal(
Frame::soft(Axes::splat(Abs::zero())), Frame::soft(Axes::splat(Abs::zero())),
vec![], vec![],
Default::default(), Default::default(),
); );
} else {
self.finish_region(engine, false)?;
}
skipped_region = true; skipped_region = true;
} }
// TODO(subfooters): Consider resetting header height etc. if we skip if skipped_region {
// region. (Maybe move that step to `finish_region_internal`.)
//
// That is unnecessary at the moment as 'prepare_footers' is only
// called at the start of the region, so header height is always zero
// and no headers were placed so far, but what about when we can have
// footers in the middle of the region? Let's think about this then.
self.current.footer_height = if skipped_region {
// Simulate the footer again; the region's 'full' might have // Simulate the footer again; the region's 'full' might have
// changed. // changed, and the vector of heights was cleared.
self.simulate_footer(footer, &self.regions, engine, disambiguator)? (expected_footer_height, expected_footer_heights) = self
.height .simulate_footer_heights(footers, &self.regions, engine, disambiguator)?;
} else { }
footer_height
}; // Ensure rows don't try to overrun the new footers.
// Note that header layout will only subtract this again if it has
// to skip regions to fit headers, so there is no risk of
// subtracting this twice.
self.regions.size.y -= expected_footer_height;
self.current.footer_height += expected_footer_height;
self.current.repeating_footer_heights.extend(expected_footer_heights);
if at_region_top {
self.current.initial_after_repeats = self.regions.size.y;
}
Ok(()) Ok(())
} }
pub fn simulate_footer_heights(
&self,
footers: impl ExactSizeIterator<Item = &'a Footer>,
regions: &Regions<'_>,
engine: &mut Engine,
disambiguator: usize,
) -> SourceResult<(Abs, Vec<Abs>)> {
let mut total_footer_height = Abs::zero();
let mut footer_heights = Vec::with_capacity(footers.len());
for footer in footers {
let footer_height =
self.simulate_footer(footer, regions, engine, disambiguator)?.height;
total_footer_height += footer_height;
footer_heights.push(footer_height);
}
Ok((total_footer_height, footer_heights))
}
/// Lays out all rows in the footer. /// Lays out all rows in the footer.
/// They are unbreakable. /// They are unbreakable.
pub fn layout_footer( pub fn layout_footer(
&mut self, &mut self,
footer: &Footer, footer: &Footer,
as_short_lived: bool,
engine: &mut Engine, engine: &mut Engine,
disambiguator: usize, disambiguator: usize,
) -> SourceResult<()> { ) -> SourceResult<()> {
// Ensure footer rows have their own height available. let footer_len = footer.range.end - footer.range.start;
// Won't change much as we're creating an unbreakable row group
// anyway, so this is mostly for correctness.
self.regions.size.y += self.current.footer_height;
let repeats = self.grid.footer.as_ref().is_some_and(|f| f.repeated);
let footer_len = self.grid.rows.len() - footer.start;
self.unbreakable_rows_left += footer_len; self.unbreakable_rows_left += footer_len;
for y in footer.start..self.grid.rows.len() { let footer_start = if self.grid.is_gutter_track(footer.range.start)
&& self
.current
.lrows
.last()
.is_none_or(|r| self.grid.is_gutter_track(r.index()))
{
// Skip gutter at the top of footer if there's already a gutter
// from a repeated header right before it in the current region.
// Normally, that shouldn't happen as it indicates we have a widow,
// but we can't fully prevent widows anyway.
footer.range.start + 1
} else {
footer.range.start
};
for y in footer_start..footer.range.end {
self.layout_row_with_state( self.layout_row_with_state(
y, y,
engine, engine,
disambiguator, disambiguator,
RowState { RowState {
in_active_repeatable: repeats, in_active_repeatable: !as_short_lived,
..Default::default() ..Default::default()
}, },
)?; )?;
@ -553,8 +724,8 @@ impl<'a> GridLayouter<'a> {
// assume that the amount of unbreakable rows following the first row // assume that the amount of unbreakable rows following the first row
// in the footer will be precisely the rows in the footer. // in the footer will be precisely the rows in the footer.
self.simulate_unbreakable_row_group( self.simulate_unbreakable_row_group(
footer.start, footer.range.start,
Some(footer.end - footer.start), Some(footer.range.end - footer.range.start),
regions, regions,
engine, engine,
disambiguator, disambiguator,

View File

@ -234,24 +234,12 @@ impl GridLayouter<'_> {
engine: &mut Engine, engine: &mut Engine,
) -> SourceResult<()> { ) -> SourceResult<()> {
if self.unbreakable_rows_left == 0 { if self.unbreakable_rows_left == 0 {
let row_group = self.simulate_unbreakable_row_group(
current_row,
// By default, the amount of unbreakable rows starting at the // By default, the amount of unbreakable rows starting at the
// current row is dynamic and depends on the amount of upcoming // current row is dynamic and depends on the amount of upcoming
// unbreakable cells (with or without a rowspan setting). // unbreakable cells (with or without a rowspan setting).
let mut amount_unbreakable_rows = None; None,
if let Some(footer) = &self.grid.footer {
if !footer.repeated && current_row >= footer.start {
// Non-repeated footer, so keep it unbreakable.
//
// TODO(subfooters): This will become unnecessary
// once non-repeated footers are treated differently and
// have widow prevention.
amount_unbreakable_rows = Some(self.grid.rows.len() - footer.start);
}
}
let row_group = self.simulate_unbreakable_row_group(
current_row,
amount_unbreakable_rows,
&self.regions, &self.regions,
engine, engine,
0, 0,
@ -400,7 +388,8 @@ impl GridLayouter<'_> {
if breakable if breakable
&& (!self.repeating_headers.is_empty() && (!self.repeating_headers.is_empty()
|| !self.pending_headers.is_empty() || !self.pending_headers.is_empty()
|| matches!(&self.grid.footer, Some(footer) if footer.repeated)) // TODO(subfooters): pending footers
|| !self.repeating_footers.is_empty())
{ {
// Subtract header and footer height from all upcoming regions // Subtract header and footer height from all upcoming regions
// when measuring the cell, including the last repeated region. // when measuring the cell, including the last repeated region.
@ -1176,14 +1165,23 @@ impl<'a> RowspanSimulator<'a> {
(None, Abs::zero()) (None, Abs::zero())
}; };
let footer_height = if let Some(footer) = let (repeating_footers, footer_height) = if layouter.repeating_footers.is_empty()
layouter.grid.footer.as_ref().and_then(Repeatable::as_repeated)
{ {
layouter (None, Abs::zero())
.simulate_footer(footer, &self.regions, engine, disambiguator)?
.height
} else { } else {
Abs::zero() // Only repeating footers have survived after the first region
// break.
// TODO(subfooters): consider pending footers
let repeating_footers = layouter.repeating_footers.iter().copied();
let (footer_height, _) = layouter.simulate_footer_heights(
repeating_footers.clone(),
&self.regions,
engine,
disambiguator,
)?;
(Some(repeating_footers), footer_height)
}; };
let mut skipped_region = false; let mut skipped_region = false;
@ -1212,15 +1210,18 @@ impl<'a> RowspanSimulator<'a> {
}; };
} }
if let Some(footer) = if let Some(repeating_footers) = repeating_footers {
layouter.grid.footer.as_ref().and_then(Repeatable::as_repeated)
{
self.footer_height = if skipped_region { self.footer_height = if skipped_region {
// Simulate footers again, at the new region, as // Simulate footers again, at the new region, as
// the full region height may change. // the full region height may change.
layouter layouter
.simulate_footer(footer, &self.regions, engine, disambiguator)? .simulate_footer_heights(
.height repeating_footers,
&self.regions,
engine,
disambiguator,
)?
.0
} else { } else {
footer_height footer_height
}; };

View File

@ -219,7 +219,7 @@ fn collect_items<'a>(
// Add fallback text to expand the line height, if necessary. // Add fallback text to expand the line height, if necessary.
if !items.iter().any(|item| matches!(item, Item::Text(_))) { if !items.iter().any(|item| matches!(item, Item::Text(_))) {
if let Some(fallback) = fallback { if let Some(fallback) = fallback {
items.push(fallback); items.push(fallback, usize::MAX);
} }
} }
@ -270,10 +270,10 @@ fn collect_range<'a>(
items: &mut Items<'a>, items: &mut Items<'a>,
fallback: &mut Option<ItemEntry<'a>>, fallback: &mut Option<ItemEntry<'a>>,
) { ) {
for (subrange, item) in p.slice(range.clone()) { for (idx, (subrange, item)) in p.slice(range.clone()).enumerate() {
// All non-text items are just kept, they can't be split. // All non-text items are just kept, they can't be split.
let Item::Text(shaped) = item else { let Item::Text(shaped) = item else {
items.push(item); items.push(item, idx);
continue; continue;
}; };
@ -293,10 +293,10 @@ fn collect_range<'a>(
} else if split { } else if split {
// When the item is split in half, reshape it. // When the item is split in half, reshape it.
let reshaped = shaped.reshape(engine, sliced); let reshaped = shaped.reshape(engine, sliced);
items.push(Item::Text(reshaped)); items.push(Item::Text(reshaped), idx);
} else { } else {
// When the item is fully contained, just keep it. // When the item is fully contained, just keep it.
items.push(item); items.push(item, idx);
} }
} }
} }
@ -499,16 +499,16 @@ pub fn commit(
// Build the frames and determine the height and baseline. // Build the frames and determine the height and baseline.
let mut frames = vec![]; let mut frames = vec![];
for item in line.items.iter() { for &(idx, ref item) in line.items.indexed_iter() {
let mut push = |offset: &mut Abs, frame: Frame| { let mut push = |offset: &mut Abs, frame: Frame, idx: usize| {
let width = frame.width(); let width = frame.width();
top.set_max(frame.baseline()); top.set_max(frame.baseline());
bottom.set_max(frame.size().y - frame.baseline()); bottom.set_max(frame.size().y - frame.baseline());
frames.push((*offset, frame)); frames.push((*offset, frame, idx));
*offset += width; *offset += width;
}; };
match item { match &**item {
Item::Absolute(v, _) => { Item::Absolute(v, _) => {
offset += *v; offset += *v;
} }
@ -520,7 +520,7 @@ pub fn commit(
layout_box(elem, engine, loc.relayout(), styles, region) layout_box(elem, engine, loc.relayout(), styles, region)
})?; })?;
apply_baseline_shift(&mut frame, *styles); apply_baseline_shift(&mut frame, *styles);
push(&mut offset, frame); push(&mut offset, frame, idx);
} else { } else {
offset += amount; offset += amount;
} }
@ -532,15 +532,15 @@ pub fn commit(
justification_ratio, justification_ratio,
extra_justification, extra_justification,
); );
push(&mut offset, frame); push(&mut offset, frame, idx);
} }
Item::Frame(frame) => { Item::Frame(frame) => {
push(&mut offset, frame.clone()); push(&mut offset, frame.clone(), idx);
} }
Item::Tag(tag) => { Item::Tag(tag) => {
let mut frame = Frame::soft(Size::zero()); let mut frame = Frame::soft(Size::zero());
frame.push(Point::zero(), FrameItem::Tag((*tag).clone())); frame.push(Point::zero(), FrameItem::Tag((*tag).clone()));
frames.push((offset, frame)); frames.push((offset, frame, idx));
} }
Item::Skip(_) => {} Item::Skip(_) => {}
} }
@ -559,8 +559,13 @@ pub fn commit(
add_par_line_marker(&mut output, marker, engine, locator, top); add_par_line_marker(&mut output, marker, engine, locator, top);
} }
// Ensure that the final frame's items are in logical order rather than in
// visual order. This is important because it affects the order of elements
// during introspection and thus things like counters.
frames.sort_unstable_by_key(|(_, _, idx)| *idx);
// Construct the line's frame. // Construct the line's frame.
for (offset, frame) in frames { for (offset, frame, _) in frames {
let x = offset + p.config.align.position(remaining); let x = offset + p.config.align.position(remaining);
let y = top - frame.baseline(); let y = top - frame.baseline();
output.push_frame(Point::new(x, y), frame); output.push_frame(Point::new(x, y), frame);
@ -627,7 +632,7 @@ fn overhang(c: char) -> f64 {
} }
/// A collection of owned or borrowed inline items. /// A collection of owned or borrowed inline items.
pub struct Items<'a>(Vec<ItemEntry<'a>>); pub struct Items<'a>(Vec<(usize, ItemEntry<'a>)>);
impl<'a> Items<'a> { impl<'a> Items<'a> {
/// Create empty items. /// Create empty items.
@ -636,33 +641,38 @@ impl<'a> Items<'a> {
} }
/// Push a new item. /// Push a new item.
pub fn push(&mut self, entry: impl Into<ItemEntry<'a>>) { pub fn push(&mut self, entry: impl Into<ItemEntry<'a>>, idx: usize) {
self.0.push(entry.into()); self.0.push((idx, entry.into()));
} }
/// Iterate over the items /// Iterate over the items.
pub fn iter(&self) -> impl Iterator<Item = &Item<'a>> { pub fn iter(&self) -> impl Iterator<Item = &Item<'a>> {
self.0.iter().map(|item| &**item) self.0.iter().map(|(_, item)| &**item)
}
/// Iterate over the items with indices
pub fn indexed_iter(&self) -> impl Iterator<Item = &(usize, ItemEntry<'a>)> {
self.0.iter()
} }
/// Access the first item. /// Access the first item.
pub fn first(&self) -> Option<&Item<'a>> { pub fn first(&self) -> Option<&Item<'a>> {
self.0.first().map(|item| &**item) self.0.first().map(|(_, item)| &**item)
} }
/// Access the last item. /// Access the last item.
pub fn last(&self) -> Option<&Item<'a>> { pub fn last(&self) -> Option<&Item<'a>> {
self.0.last().map(|item| &**item) self.0.last().map(|(_, item)| &**item)
} }
/// Access the first item mutably, if it is text. /// Access the first item mutably, if it is text.
pub fn first_text_mut(&mut self) -> Option<&mut ShapedText<'a>> { pub fn first_text_mut(&mut self) -> Option<&mut ShapedText<'a>> {
self.0.first_mut()?.text_mut() self.0.first_mut()?.1.text_mut()
} }
/// Access the last item mutably, if it is text. /// Access the last item mutably, if it is text.
pub fn last_text_mut(&mut self) -> Option<&mut ShapedText<'a>> { pub fn last_text_mut(&mut self) -> Option<&mut ShapedText<'a>> {
self.0.last_mut()?.text_mut() self.0.last_mut()?.1.text_mut()
} }
/// Reorder the items starting at the given index to RTL. /// Reorder the items starting at the given index to RTL.
@ -673,12 +683,12 @@ impl<'a> Items<'a> {
impl<'a> FromIterator<ItemEntry<'a>> for Items<'a> { impl<'a> FromIterator<ItemEntry<'a>> for Items<'a> {
fn from_iter<I: IntoIterator<Item = ItemEntry<'a>>>(iter: I) -> Self { fn from_iter<I: IntoIterator<Item = ItemEntry<'a>>>(iter: I) -> Self {
Self(iter.into_iter().collect()) Self(iter.into_iter().enumerate().collect())
} }
} }
impl<'a> Deref for Items<'a> { impl<'a> Deref for Items<'a> {
type Target = Vec<ItemEntry<'a>>; type Target = Vec<(usize, ItemEntry<'a>)>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.0 &self.0
@ -698,6 +708,10 @@ impl Debug for Items<'_> {
} }
/// A reference to or a boxed item. /// A reference to or a boxed item.
///
/// This is conceptually similar to a [`Cow<'a, Item<'a>>`][std::borrow::Cow],
/// but we box owned items since an [`Item`] is much bigger than
/// a box.
pub enum ItemEntry<'a> { pub enum ItemEntry<'a> {
Ref(&'a Item<'a>), Ref(&'a Item<'a>),
Box(Box<Item<'a>>), Box(Box<Item<'a>>),

View File

@ -11,8 +11,8 @@ use typst_library::layout::{
}; };
use typst_library::visualize::{ use typst_library::visualize::{
CircleElem, CloseMode, Curve, CurveComponent, CurveElem, EllipseElem, FillRule, CircleElem, CloseMode, Curve, CurveComponent, CurveElem, EllipseElem, FillRule,
FixedStroke, Geometry, LineElem, Paint, PathElem, PathVertex, PolygonElem, RectElem, FixedStroke, Geometry, LineCap, LineElem, Paint, PathElem, PathVertex, PolygonElem,
Shape, SquareElem, Stroke, RectElem, Shape, SquareElem, Stroke,
}; };
use typst_syntax::Span; use typst_syntax::Span;
use typst_utils::{Get, Numeric}; use typst_utils::{Get, Numeric};
@ -889,7 +889,13 @@ fn segmented_rect(
let end = current; let end = current;
last = current; last = current;
let Some(stroke) = strokes.get_ref(start.side_cw()) else { continue }; let Some(stroke) = strokes.get_ref(start.side_cw()) else { continue };
let (shape, ontop) = segment(start, end, &corners, stroke); let start_cap = stroke.cap;
let end_cap = match strokes.get_ref(end.side_ccw()) {
Some(stroke) => stroke.cap,
None => start_cap,
};
let (shape, ontop) =
segment(start, end, start_cap, end_cap, &corners, stroke);
if ontop { if ontop {
res.push(shape); res.push(shape);
} else { } else {
@ -899,7 +905,14 @@ fn segmented_rect(
} }
} else if let Some(stroke) = &strokes.top { } else if let Some(stroke) = &strokes.top {
// single segment // single segment
let (shape, _) = segment(Corner::TopLeft, Corner::TopLeft, &corners, stroke); let (shape, _) = segment(
Corner::TopLeft,
Corner::TopLeft,
stroke.cap,
stroke.cap,
&corners,
stroke,
);
res.push(shape); res.push(shape);
} }
res res
@ -946,6 +959,8 @@ fn curve_segment(
fn segment( fn segment(
start: Corner, start: Corner,
end: Corner, end: Corner,
start_cap: LineCap,
end_cap: LineCap,
corners: &Corners<ControlPoints>, corners: &Corners<ControlPoints>,
stroke: &FixedStroke, stroke: &FixedStroke,
) -> (Shape, bool) { ) -> (Shape, bool) {
@ -979,7 +994,7 @@ fn segment(
let use_fill = solid && fill_corners(start, end, corners); let use_fill = solid && fill_corners(start, end, corners);
let shape = if use_fill { let shape = if use_fill {
fill_segment(start, end, corners, stroke) fill_segment(start, end, start_cap, end_cap, corners, stroke)
} else { } else {
stroke_segment(start, end, corners, stroke.clone()) stroke_segment(start, end, corners, stroke.clone())
}; };
@ -1010,6 +1025,8 @@ fn stroke_segment(
fn fill_segment( fn fill_segment(
start: Corner, start: Corner,
end: Corner, end: Corner,
start_cap: LineCap,
end_cap: LineCap,
corners: &Corners<ControlPoints>, corners: &Corners<ControlPoints>,
stroke: &FixedStroke, stroke: &FixedStroke,
) -> Shape { ) -> Shape {
@ -1035,8 +1052,7 @@ fn fill_segment(
if c.arc_outer() { if c.arc_outer() {
curve.arc_line(c.mid_outer(), c.center_outer(), c.end_outer()); curve.arc_line(c.mid_outer(), c.center_outer(), c.end_outer());
} else { } else {
curve.line(c.outer()); c.start_cap(&mut curve, start_cap);
curve.line(c.end_outer());
} }
} }
@ -1079,7 +1095,7 @@ fn fill_segment(
if c.arc_inner() { if c.arc_inner() {
curve.arc_line(c.mid_inner(), c.center_inner(), c.start_inner()); curve.arc_line(c.mid_inner(), c.center_inner(), c.start_inner());
} else { } else {
curve.line(c.center_inner()); c.end_cap(&mut curve, end_cap);
} }
} }
@ -1134,6 +1150,16 @@ struct ControlPoints {
} }
impl ControlPoints { impl ControlPoints {
/// Rotate point around the origin, relative to the top-left.
fn rotate_centered(&self, point: Point) -> Point {
match self.corner {
Corner::TopLeft => point,
Corner::TopRight => Point { x: -point.y, y: point.x },
Corner::BottomRight => Point { x: -point.x, y: -point.y },
Corner::BottomLeft => Point { x: point.y, y: -point.x },
}
}
/// Move and rotate the point from top-left to the required corner. /// Move and rotate the point from top-left to the required corner.
fn rotate(&self, point: Point) -> Point { fn rotate(&self, point: Point) -> Point {
match self.corner { match self.corner {
@ -1280,6 +1306,77 @@ impl ControlPoints {
y: self.stroke_after, y: self.stroke_after,
}) })
} }
/// Draw the cap at the beginning of the segment.
///
/// If this corner has a stroke before it,
/// a default "butt" cap is used.
///
/// NOTE: doesn't support the case where the corner has a radius.
pub fn start_cap(&self, curve: &mut Curve, cap_type: LineCap) {
if self.stroke_before != Abs::zero()
|| self.radius != Abs::zero()
|| cap_type == LineCap::Butt
{
// Just the default cap.
curve.line(self.outer());
} else if cap_type == LineCap::Square {
// Extend by the stroke width.
let offset =
self.rotate_centered(Point { x: -self.stroke_after, y: Abs::zero() });
curve.line(self.end_inner() + offset);
curve.line(self.outer() + offset);
} else if cap_type == LineCap::Round {
// We push the center by a little bit to ensure the correct
// half of the circle gets drawn. If it is perfectly centered
// the `arc` function just degenerates into a line, which we
// do not want in this case.
curve.arc(
self.end_inner(),
(self.end_inner()
+ self.rotate_centered(Point { x: Abs::raw(1.0), y: Abs::zero() })
+ self.outer())
/ 2.,
self.outer(),
);
}
curve.line(self.end_outer());
}
/// Draw the cap at the end of the segment.
///
/// If this corner has a stroke before it,
/// a default "butt" cap is used.
///
/// NOTE: doesn't support the case where the corner has a radius.
pub fn end_cap(&self, curve: &mut Curve, cap_type: LineCap) {
if self.stroke_after != Abs::zero()
|| self.radius != Abs::zero()
|| cap_type == LineCap::Butt
{
// Just the default cap.
curve.line(self.center_inner());
} else if cap_type == LineCap::Square {
// Extend by the stroke width.
let offset =
self.rotate_centered(Point { x: Abs::zero(), y: -self.stroke_before });
curve.line(self.outer() + offset);
curve.line(self.center_inner() + offset);
} else if cap_type == LineCap::Round {
// We push the center by a little bit to ensure the correct
// half of the circle gets drawn. If it is perfectly centered
// the `arc` function just degenerates into a line, which we
// do not want in this case.
curve.arc(
self.outer(),
(self.outer()
+ self.rotate_centered(Point { x: Abs::zero(), y: Abs::raw(1.0) })
+ self.center_inner())
/ 2.,
self.center_inner(),
);
}
}
} }
/// Helper to draw arcs with Bézier curves. /// Helper to draw arcs with Bézier curves.

View File

@ -9,7 +9,7 @@ use std::ops::Add;
use ecow::eco_format; use ecow::eco_format;
use smallvec::SmallVec; use smallvec::SmallVec;
use typst_syntax::{Span, Spanned}; use typst_syntax::{Span, Spanned, SyntaxMode};
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult}; use crate::diag::{At, HintedStrResult, HintedString, SourceResult, StrResult};
@ -459,6 +459,21 @@ impl FromValue for Never {
} }
} }
cast! {
SyntaxMode,
self => IntoValue::into_value(match self {
SyntaxMode::Markup => "markup",
SyntaxMode::Math => "math",
SyntaxMode::Code => "code",
}),
/// Evaluate as markup, as in a Typst file.
"markup" => SyntaxMode::Markup,
/// Evaluate as math, as in an equation.
"math" => SyntaxMode::Math,
/// Evaluate as code, as after a hash.
"code" => SyntaxMode::Code,
}
cast! { cast! {
MathClass, MathClass,
self => IntoValue::into_value(match self { self => IntoValue::into_value(match self {

View File

@ -16,6 +16,21 @@ impl Duration {
pub fn is_zero(&self) -> bool { pub fn is_zero(&self) -> bool {
self.0.is_zero() self.0.is_zero()
} }
/// Decomposes the time into whole weeks, days, hours, minutes, and seconds.
pub fn decompose(&self) -> [i64; 5] {
let mut tmp = self.0;
let weeks = tmp.whole_weeks();
tmp -= weeks.weeks();
let days = tmp.whole_days();
tmp -= days.days();
let hours = tmp.whole_hours();
tmp -= hours.hours();
let minutes = tmp.whole_minutes();
tmp -= minutes.minutes();
let seconds = tmp.whole_seconds();
[weeks, days, hours, minutes, seconds]
}
} }
#[scope] #[scope]
@ -118,34 +133,25 @@ impl Debug for Duration {
impl Repr for Duration { impl Repr for Duration {
fn repr(&self) -> EcoString { fn repr(&self) -> EcoString {
let mut tmp = self.0; let [weeks, days, hours, minutes, seconds] = self.decompose();
let mut vec = Vec::with_capacity(5); let mut vec = Vec::with_capacity(5);
let weeks = tmp.whole_seconds() / 604_800.0 as i64;
if weeks != 0 { if weeks != 0 {
vec.push(eco_format!("weeks: {}", weeks.repr())); vec.push(eco_format!("weeks: {}", weeks.repr()));
} }
tmp -= weeks.weeks();
let days = tmp.whole_days();
if days != 0 { if days != 0 {
vec.push(eco_format!("days: {}", days.repr())); vec.push(eco_format!("days: {}", days.repr()));
} }
tmp -= days.days();
let hours = tmp.whole_hours();
if hours != 0 { if hours != 0 {
vec.push(eco_format!("hours: {}", hours.repr())); vec.push(eco_format!("hours: {}", hours.repr()));
} }
tmp -= hours.hours();
let minutes = tmp.whole_minutes();
if minutes != 0 { if minutes != 0 {
vec.push(eco_format!("minutes: {}", minutes.repr())); vec.push(eco_format!("minutes: {}", minutes.repr()));
} }
tmp -= minutes.minutes();
let seconds = tmp.whole_seconds();
if seconds != 0 { if seconds != 0 {
vec.push(eco_format!("seconds: {}", seconds.repr())); vec.push(eco_format!("seconds: {}", seconds.repr()));
} }

View File

@ -210,3 +210,25 @@ cast! {
fn parse_float(s: EcoString) -> Result<f64, ParseFloatError> { fn parse_float(s: EcoString) -> Result<f64, ParseFloatError> {
s.replace(repr::MINUS_SIGN, "-").parse() s.replace(repr::MINUS_SIGN, "-").parse()
} }
/// A floating-point number that must be positive (strictly larger than zero).
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
pub struct PositiveF64(f64);
impl PositiveF64 {
/// Wrap a float if it is positive.
pub fn new(value: f64) -> Option<Self> {
(value > 0.0).then_some(Self(value))
}
/// Get the underlying value.
pub fn get(self) -> f64 {
self.0
}
}
cast! {
PositiveF64,
self => self.get().into_value(),
v: f64 => Self::new(v).ok_or("number must be positive")?,
}

View File

@ -307,7 +307,7 @@ impl Func {
) -> SourceResult<Value> { ) -> SourceResult<Value> {
match &self.repr { match &self.repr {
Repr::Native(native) => { Repr::Native(native) => {
let value = (native.function)(engine, context, &mut args)?; let value = (native.function.0)(engine, context, &mut args)?;
args.finish()?; args.finish()?;
Ok(value) Ok(value)
} }
@ -491,8 +491,8 @@ pub trait NativeFunc {
/// Defines a native function. /// Defines a native function.
#[derive(Debug)] #[derive(Debug)]
pub struct NativeFuncData { pub struct NativeFuncData {
/// Invokes the function from Typst. /// The implementation of the function.
pub function: fn(&mut Engine, Tracked<Context>, &mut Args) -> SourceResult<Value>, pub function: NativeFuncPtr,
/// The function's normal name (e.g. `align`), as exposed to Typst. /// The function's normal name (e.g. `align`), as exposed to Typst.
pub name: &'static str, pub name: &'static str,
/// The function's title case name (e.g. `Align`). /// The function's title case name (e.g. `Align`).
@ -504,11 +504,11 @@ pub struct NativeFuncData {
/// Whether this function makes use of context. /// Whether this function makes use of context.
pub contextual: bool, pub contextual: bool,
/// Definitions in the scope of the function. /// Definitions in the scope of the function.
pub scope: LazyLock<Scope>, pub scope: DynLazyLock<Scope>,
/// A list of parameter information for each parameter. /// A list of parameter information for each parameter.
pub params: LazyLock<Vec<ParamInfo>>, pub params: DynLazyLock<Vec<ParamInfo>>,
/// Information about the return value of this function. /// Information about the return value of this function.
pub returns: LazyLock<CastInfo>, pub returns: DynLazyLock<CastInfo>,
} }
cast! { cast! {
@ -516,6 +516,28 @@ cast! {
self => Func::from(self).into_value(), self => Func::from(self).into_value(),
} }
/// A pointer to a native function's implementation.
pub struct NativeFuncPtr(pub &'static NativeFuncSignature);
/// The signature of a native function's implementation.
type NativeFuncSignature =
dyn Fn(&mut Engine, Tracked<Context>, &mut Args) -> SourceResult<Value> + Send + Sync;
impl Debug for NativeFuncPtr {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.pad("NativeFuncPtr(..)")
}
}
/// A `LazyLock` that uses a static closure for initialization instead of only
/// working with function pointers.
///
/// Can be created from a normal function or closure by prepending with a `&`,
/// e.g. `LazyLock::new(&|| "hello")`. Can be created from a dynamic closure
/// by allocating and then leaking it. This is equivalent to having it
/// statically allocated, but allows for it to be generated at runtime.
type DynLazyLock<T> = LazyLock<T, &'static (dyn Fn() -> T + Send + Sync)>;
/// Describes a function parameter. /// Describes a function parameter.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ParamInfo { pub struct ParamInfo {

View File

@ -1,7 +1,8 @@
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use typst_utils::{PicoStr, ResolvedPicoStr}; use typst_utils::{PicoStr, ResolvedPicoStr};
use crate::foundations::{func, scope, ty, Repr, Str}; use crate::diag::StrResult;
use crate::foundations::{bail, func, scope, ty, Repr, Str};
/// A label for an element. /// A label for an element.
/// ///
@ -27,7 +28,8 @@ use crate::foundations::{func, scope, ty, Repr, Str};
/// # Syntax /// # Syntax
/// This function also has dedicated syntax: You can create a label by enclosing /// This function also has dedicated syntax: You can create a label by enclosing
/// its name in angle brackets. This works both in markup and code. A label's /// its name in angle brackets. This works both in markup and code. A label's
/// name can contain letters, numbers, `_`, `-`, `:`, and `.`. /// name can contain letters, numbers, `_`, `-`, `:`, and `.`. A label cannot
/// be empty.
/// ///
/// Note that there is a syntactical difference when using the dedicated syntax /// Note that there is a syntactical difference when using the dedicated syntax
/// for this function. In the code below, the `[<a>]` terminates the heading and /// for this function. In the code below, the `[<a>]` terminates the heading and
@ -50,8 +52,11 @@ pub struct Label(PicoStr);
impl Label { impl Label {
/// Creates a label from an interned string. /// Creates a label from an interned string.
pub fn new(name: PicoStr) -> Self { ///
Self(name) /// Returns `None` if the given string is empty.
pub fn new(name: PicoStr) -> Option<Self> {
const EMPTY: PicoStr = PicoStr::constant("");
(name != EMPTY).then_some(Self(name))
} }
/// Resolves the label to a string. /// Resolves the label to a string.
@ -70,16 +75,25 @@ impl Label {
/// Creates a label from a string. /// Creates a label from a string.
#[func(constructor)] #[func(constructor)]
pub fn construct( pub fn construct(
/// The name of the label. /// The name of the label. Must not be empty.
name: Str, name: Str,
) -> Label { ) -> StrResult<Label> {
Self(PicoStr::intern(name.as_str())) if name.is_empty() {
bail!("label name must not be empty");
}
Ok(Self(PicoStr::intern(name.as_str())))
} }
} }
impl Repr for Label { impl Repr for Label {
fn repr(&self) -> EcoString { fn repr(&self) -> EcoString {
eco_format!("<{}>", self.resolve()) let resolved = self.resolve();
if typst_syntax::is_valid_label_literal_id(&resolved) {
eco_format!("<{resolved}>")
} else {
eco_format!("label({})", resolved.repr())
}
} }
} }

View File

@ -69,6 +69,7 @@ pub use self::ty::*;
pub use self::value::*; pub use self::value::*;
pub use self::version::*; pub use self::version::*;
pub use typst_macros::{scope, ty}; pub use typst_macros::{scope, ty};
use typst_syntax::SyntaxMode;
#[rustfmt::skip] #[rustfmt::skip]
#[doc(hidden)] #[doc(hidden)]
@ -83,7 +84,6 @@ use typst_syntax::Spanned;
use crate::diag::{bail, SourceResult, StrResult}; use crate::diag::{bail, SourceResult, StrResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::routines::EvalMode;
use crate::{Feature, Features}; use crate::{Feature, Features};
/// Hook up all `foundations` definitions. /// Hook up all `foundations` definitions.
@ -273,8 +273,8 @@ pub fn eval(
/// #eval("1_2^3", mode: "math") /// #eval("1_2^3", mode: "math")
/// ``` /// ```
#[named] #[named]
#[default(EvalMode::Code)] #[default(SyntaxMode::Code)]
mode: EvalMode, mode: SyntaxMode,
/// A scope of definitions that are made available. /// A scope of definitions that are made available.
/// ///
/// ```example /// ```example

View File

@ -19,11 +19,8 @@ use crate::foundations::{repr, ty, Content, Scope, Value};
/// ///
/// You can access definitions from the module using [field access /// You can access definitions from the module using [field access
/// notation]($scripting/#fields) and interact with it using the [import and /// notation]($scripting/#fields) and interact with it using the [import and
/// include syntaxes]($scripting/#modules). Alternatively, it is possible to /// include syntaxes]($scripting/#modules).
/// convert a module to a dictionary, and therefore access its contents
/// dynamically, using the [dictionary constructor]($dictionary/#constructor).
/// ///
/// # Example
/// ```example /// ```example
/// <<< #import "utils.typ" /// <<< #import "utils.typ"
/// <<< #utils.add(2, 5) /// <<< #utils.add(2, 5)
@ -34,6 +31,20 @@ use crate::foundations::{repr, ty, Content, Scope, Value};
/// >>> /// >>>
/// >>> #(-3) /// >>> #(-3)
/// ``` /// ```
///
/// You can check whether a definition is present in a module using the `{in}`
/// operator, with a string on the left-hand side. This can be useful to
/// [conditionally access]($category/foundations/std/#conditional-access)
/// definitions in a module.
///
/// ```example
/// #("table" in std) \
/// #("nope" in std)
/// ```
///
/// Alternatively, it is possible to convert a module to a dictionary, and
/// therefore access its contents dynamically, using the [dictionary
/// constructor]($dictionary/#constructor).
#[ty(cast)] #[ty(cast)]
#[derive(Clone, Hash)] #[derive(Clone, Hash)]
#[allow(clippy::derived_hash_with_manual_eq)] #[allow(clippy::derived_hash_with_manual_eq)]

View File

@ -558,6 +558,7 @@ pub fn contains(lhs: &Value, rhs: &Value) -> Option<bool> {
(Str(a), Str(b)) => Some(b.as_str().contains(a.as_str())), (Str(a), Str(b)) => Some(b.as_str().contains(a.as_str())),
(Dyn(a), Str(b)) => a.downcast::<Regex>().map(|regex| regex.is_match(b)), (Dyn(a), Str(b)) => a.downcast::<Regex>().map(|regex| regex.is_match(b)),
(Str(a), Dict(b)) => Some(b.contains(a)), (Str(a), Dict(b)) => Some(b.contains(a)),
(Str(a), Module(b)) => Some(b.scope().get(a).is_some()),
(a, Array(b)) => Some(b.contains(a.clone())), (a, Array(b)) => Some(b.contains(a.clone())),
_ => Option::None, _ => Option::None,

View File

@ -8,7 +8,7 @@ use serde::{Serialize, Serializer};
use typst_syntax::{is_ident, Span, Spanned}; use typst_syntax::{is_ident, Span, Spanned};
use typst_utils::hash128; use typst_utils::hash128;
use crate::diag::{bail, SourceResult, StrResult}; use crate::diag::{bail, DeprecationSink, SourceResult, StrResult};
use crate::foundations::{ use crate::foundations::{
cast, elem, func, scope, ty, Array, Content, Func, NativeElement, NativeFunc, Packed, cast, elem, func, scope, ty, Array, Content, Func, NativeElement, NativeFunc, Packed,
PlainText, Repr as _, PlainText, Repr as _,
@ -54,18 +54,22 @@ enum Repr {
/// A native symbol that has no named variant. /// A native symbol that has no named variant.
Single(char), Single(char),
/// A native symbol with multiple named variants. /// A native symbol with multiple named variants.
Complex(&'static [(ModifierSet<&'static str>, char)]), Complex(&'static [Variant<&'static str>]),
/// A symbol with multiple named variants, where some modifiers may have /// A symbol with multiple named variants, where some modifiers may have
/// been applied. Also used for symbols defined at runtime by the user with /// been applied. Also used for symbols defined at runtime by the user with
/// no modifier applied. /// no modifier applied.
Modified(Arc<(List, ModifierSet<EcoString>)>), Modified(Arc<(List, ModifierSet<EcoString>)>),
} }
/// A symbol variant, consisting of a set of modifiers, a character, and an
/// optional deprecation message.
type Variant<S> = (ModifierSet<S>, char, Option<S>);
/// A collection of symbols. /// A collection of symbols.
#[derive(Clone, Eq, PartialEq, Hash)] #[derive(Clone, Eq, PartialEq, Hash)]
enum List { enum List {
Static(&'static [(ModifierSet<&'static str>, char)]), Static(&'static [Variant<&'static str>]),
Runtime(Box<[(ModifierSet<EcoString>, char)]>), Runtime(Box<[Variant<EcoString>]>),
} }
impl Symbol { impl Symbol {
@ -76,14 +80,14 @@ impl Symbol {
/// Create a symbol with a static variant list. /// Create a symbol with a static variant list.
#[track_caller] #[track_caller]
pub const fn list(list: &'static [(ModifierSet<&'static str>, char)]) -> Self { pub const fn list(list: &'static [Variant<&'static str>]) -> Self {
debug_assert!(!list.is_empty()); debug_assert!(!list.is_empty());
Self(Repr::Complex(list)) Self(Repr::Complex(list))
} }
/// Create a symbol with a runtime variant list. /// Create a symbol with a runtime variant list.
#[track_caller] #[track_caller]
pub fn runtime(list: Box<[(ModifierSet<EcoString>, char)]>) -> Self { pub fn runtime(list: Box<[Variant<EcoString>]>) -> Self {
debug_assert!(!list.is_empty()); debug_assert!(!list.is_empty());
Self(Repr::Modified(Arc::new((List::Runtime(list), ModifierSet::default())))) Self(Repr::Modified(Arc::new((List::Runtime(list), ModifierSet::default()))))
} }
@ -93,9 +97,11 @@ impl Symbol {
match &self.0 { match &self.0 {
Repr::Single(c) => *c, Repr::Single(c) => *c,
Repr::Complex(_) => ModifierSet::<&'static str>::default() Repr::Complex(_) => ModifierSet::<&'static str>::default()
.best_match_in(self.variants()) .best_match_in(self.variants().map(|(m, c, _)| (m, c)))
.unwrap(), .unwrap(),
Repr::Modified(arc) => arc.1.best_match_in(self.variants()).unwrap(), Repr::Modified(arc) => {
arc.1.best_match_in(self.variants().map(|(m, c, _)| (m, c))).unwrap()
}
} }
} }
@ -128,7 +134,11 @@ impl Symbol {
} }
/// Apply a modifier to the symbol. /// Apply a modifier to the symbol.
pub fn modified(mut self, modifier: &str) -> StrResult<Self> { pub fn modified(
mut self,
sink: impl DeprecationSink,
modifier: &str,
) -> StrResult<Self> {
if let Repr::Complex(list) = self.0 { if let Repr::Complex(list) = self.0 {
self.0 = self.0 =
Repr::Modified(Arc::new((List::Static(list), ModifierSet::default()))); Repr::Modified(Arc::new((List::Static(list), ModifierSet::default())));
@ -137,7 +147,12 @@ impl Symbol {
if let Repr::Modified(arc) = &mut self.0 { if let Repr::Modified(arc) = &mut self.0 {
let (list, modifiers) = Arc::make_mut(arc); let (list, modifiers) = Arc::make_mut(arc);
modifiers.insert_raw(modifier); modifiers.insert_raw(modifier);
if modifiers.best_match_in(list.variants()).is_some() { if let Some(deprecation) =
modifiers.best_match_in(list.variants().map(|(m, _, d)| (m, d)))
{
if let Some(message) = deprecation {
sink.emit(message)
}
return Ok(self); return Ok(self);
} }
} }
@ -146,7 +161,7 @@ impl Symbol {
} }
/// The characters that are covered by this symbol. /// The characters that are covered by this symbol.
pub fn variants(&self) -> impl Iterator<Item = (ModifierSet<&str>, char)> { pub fn variants(&self) -> impl Iterator<Item = Variant<&str>> {
match &self.0 { match &self.0 {
Repr::Single(c) => Variants::Single(Some(*c).into_iter()), Repr::Single(c) => Variants::Single(Some(*c).into_iter()),
Repr::Complex(list) => Variants::Static(list.iter()), Repr::Complex(list) => Variants::Static(list.iter()),
@ -161,7 +176,7 @@ impl Symbol {
_ => ModifierSet::default(), _ => ModifierSet::default(),
}; };
self.variants() self.variants()
.flat_map(|(m, _)| m) .flat_map(|(m, _, _)| m)
.filter(|modifier| !modifier.is_empty() && !modifiers.contains(modifier)) .filter(|modifier| !modifier.is_empty() && !modifiers.contains(modifier))
.collect::<BTreeSet<_>>() .collect::<BTreeSet<_>>()
.into_iter() .into_iter()
@ -256,7 +271,7 @@ impl Symbol {
let list = variants let list = variants
.into_iter() .into_iter()
.map(|s| (ModifierSet::from_raw_dotted(s.v.0), s.v.1)) .map(|s| (ModifierSet::from_raw_dotted(s.v.0), s.v.1, None))
.collect(); .collect();
Ok(Symbol::runtime(list)) Ok(Symbol::runtime(list))
} }
@ -316,17 +331,17 @@ impl crate::foundations::Repr for Symbol {
} }
fn repr_variants<'a>( fn repr_variants<'a>(
variants: impl Iterator<Item = (ModifierSet<&'a str>, char)>, variants: impl Iterator<Item = Variant<&'a str>>,
applied_modifiers: ModifierSet<&str>, applied_modifiers: ModifierSet<&str>,
) -> String { ) -> String {
crate::foundations::repr::pretty_array_like( crate::foundations::repr::pretty_array_like(
&variants &variants
.filter(|(modifiers, _)| { .filter(|(modifiers, _, _)| {
// Only keep variants that can still be accessed, i.e., variants // Only keep variants that can still be accessed, i.e., variants
// that contain all applied modifiers. // that contain all applied modifiers.
applied_modifiers.iter().all(|am| modifiers.contains(am)) applied_modifiers.iter().all(|am| modifiers.contains(am))
}) })
.map(|(modifiers, c)| { .map(|(modifiers, c, _)| {
let trimmed_modifiers = let trimmed_modifiers =
modifiers.into_iter().filter(|&m| !applied_modifiers.contains(m)); modifiers.into_iter().filter(|&m| !applied_modifiers.contains(m));
if trimmed_modifiers.clone().all(|m| m.is_empty()) { if trimmed_modifiers.clone().all(|m| m.is_empty()) {
@ -379,18 +394,20 @@ cast! {
/// Iterator over variants. /// Iterator over variants.
enum Variants<'a> { enum Variants<'a> {
Single(std::option::IntoIter<char>), Single(std::option::IntoIter<char>),
Static(std::slice::Iter<'static, (ModifierSet<&'static str>, char)>), Static(std::slice::Iter<'static, Variant<&'static str>>),
Runtime(std::slice::Iter<'a, (ModifierSet<EcoString>, char)>), Runtime(std::slice::Iter<'a, Variant<EcoString>>),
} }
impl<'a> Iterator for Variants<'a> { impl<'a> Iterator for Variants<'a> {
type Item = (ModifierSet<&'a str>, char); type Item = Variant<&'a str>;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
match self { match self {
Self::Single(iter) => Some((ModifierSet::default(), iter.next()?)), Self::Single(iter) => Some((ModifierSet::default(), iter.next()?, None)),
Self::Static(list) => list.next().copied(), Self::Static(list) => list.next().copied(),
Self::Runtime(list) => list.next().map(|(m, c)| (m.as_deref(), *c)), Self::Runtime(list) => {
list.next().map(|(m, c, d)| (m.as_deref(), *c, d.as_deref()))
}
} }
} }
} }

View File

@ -157,7 +157,9 @@ impl Value {
/// Try to access a field on the value. /// Try to access a field on the value.
pub fn field(&self, field: &str, sink: impl DeprecationSink) -> StrResult<Value> { pub fn field(&self, field: &str, sink: impl DeprecationSink) -> StrResult<Value> {
match self { match self {
Self::Symbol(symbol) => symbol.clone().modified(field).map(Self::Symbol), Self::Symbol(symbol) => {
symbol.clone().modified(sink, field).map(Self::Symbol)
}
Self::Version(version) => version.component(field).map(Self::Int), Self::Version(version) => version.component(field).map(Self::Int),
Self::Dict(dict) => dict.get(field).cloned(), Self::Dict(dict) => dict.get(field).cloned(),
Self::Content(content) => content.field_by_name(field), Self::Content(content) => content.field_by_name(field),

View File

@ -7,7 +7,7 @@ use typst_utils::{PicoStr, ResolvedPicoStr};
use crate::diag::{bail, HintedStrResult, StrResult}; use crate::diag::{bail, HintedStrResult, StrResult};
use crate::foundations::{cast, Dict, Repr, Str}; use crate::foundations::{cast, Dict, Repr, Str};
use crate::introspection::{Introspector, Tag}; use crate::introspection::{Introspector, Tag};
use crate::layout::Frame; use crate::layout::{Abs, Frame};
use crate::model::DocumentInfo; use crate::model::DocumentInfo;
/// An HTML document. /// An HTML document.
@ -30,8 +30,8 @@ pub enum HtmlNode {
Text(EcoString, Span), Text(EcoString, Span),
/// Another element. /// Another element.
Element(HtmlElement), Element(HtmlElement),
/// A frame that will be displayed as an embedded SVG. /// Layouted content that will be embedded into HTML as an SVG.
Frame(Frame), Frame(HtmlFrame),
} }
impl HtmlNode { impl HtmlNode {
@ -188,7 +188,7 @@ cast! {
.collect::<HintedStrResult<_>>()?), .collect::<HintedStrResult<_>>()?),
} }
/// An attribute of an HTML. /// An attribute of an HTML element.
#[derive(Copy, Clone, Eq, PartialEq, Hash)] #[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct HtmlAttr(PicoStr); pub struct HtmlAttr(PicoStr);
@ -263,6 +263,17 @@ cast! {
v: Str => Self::intern(&v)?, v: Str => Self::intern(&v)?,
} }
/// Layouted content that will be embedded into HTML as an SVG.
#[derive(Debug, Clone, Hash)]
pub struct HtmlFrame {
/// The frame that will be displayed as an SVG.
pub inner: Frame,
/// The text size where the frame was defined. This is used to size the
/// frame with em units to make text in and outside of the frame sized
/// consistently.
pub text_size: Abs,
}
/// Defines syntactical properties of HTML tags, attributes, and text. /// Defines syntactical properties of HTML tags, attributes, and text.
pub mod charsets { pub mod charsets {
/// Check whether a character is in a tag name. /// Check whether a character is in a tag name.
@ -347,135 +358,124 @@ pub mod charsets {
} }
/// Predefined constants for HTML tags. /// Predefined constants for HTML tags.
#[allow(non_upper_case_globals)]
pub mod tag { pub mod tag {
use super::HtmlTag; use super::HtmlTag;
macro_rules! tags { pub const a: HtmlTag = HtmlTag::constant("a");
($($tag:ident)*) => { pub const abbr: HtmlTag = HtmlTag::constant("abbr");
$(#[allow(non_upper_case_globals)] pub const address: HtmlTag = HtmlTag::constant("address");
pub const $tag: HtmlTag = HtmlTag::constant( pub const area: HtmlTag = HtmlTag::constant("area");
stringify!($tag) pub const article: HtmlTag = HtmlTag::constant("article");
);)* pub const aside: HtmlTag = HtmlTag::constant("aside");
} pub const audio: HtmlTag = HtmlTag::constant("audio");
} pub const b: HtmlTag = HtmlTag::constant("b");
pub const base: HtmlTag = HtmlTag::constant("base");
pub const bdi: HtmlTag = HtmlTag::constant("bdi");
pub const bdo: HtmlTag = HtmlTag::constant("bdo");
pub const blockquote: HtmlTag = HtmlTag::constant("blockquote");
pub const body: HtmlTag = HtmlTag::constant("body");
pub const br: HtmlTag = HtmlTag::constant("br");
pub const button: HtmlTag = HtmlTag::constant("button");
pub const canvas: HtmlTag = HtmlTag::constant("canvas");
pub const caption: HtmlTag = HtmlTag::constant("caption");
pub const cite: HtmlTag = HtmlTag::constant("cite");
pub const code: HtmlTag = HtmlTag::constant("code");
pub const col: HtmlTag = HtmlTag::constant("col");
pub const colgroup: HtmlTag = HtmlTag::constant("colgroup");
pub const data: HtmlTag = HtmlTag::constant("data");
pub const datalist: HtmlTag = HtmlTag::constant("datalist");
pub const dd: HtmlTag = HtmlTag::constant("dd");
pub const del: HtmlTag = HtmlTag::constant("del");
pub const details: HtmlTag = HtmlTag::constant("details");
pub const dfn: HtmlTag = HtmlTag::constant("dfn");
pub const dialog: HtmlTag = HtmlTag::constant("dialog");
pub const div: HtmlTag = HtmlTag::constant("div");
pub const dl: HtmlTag = HtmlTag::constant("dl");
pub const dt: HtmlTag = HtmlTag::constant("dt");
pub const em: HtmlTag = HtmlTag::constant("em");
pub const embed: HtmlTag = HtmlTag::constant("embed");
pub const fieldset: HtmlTag = HtmlTag::constant("fieldset");
pub const figcaption: HtmlTag = HtmlTag::constant("figcaption");
pub const figure: HtmlTag = HtmlTag::constant("figure");
pub const footer: HtmlTag = HtmlTag::constant("footer");
pub const form: HtmlTag = HtmlTag::constant("form");
pub const h1: HtmlTag = HtmlTag::constant("h1");
pub const h2: HtmlTag = HtmlTag::constant("h2");
pub const h3: HtmlTag = HtmlTag::constant("h3");
pub const h4: HtmlTag = HtmlTag::constant("h4");
pub const h5: HtmlTag = HtmlTag::constant("h5");
pub const h6: HtmlTag = HtmlTag::constant("h6");
pub const head: HtmlTag = HtmlTag::constant("head");
pub const header: HtmlTag = HtmlTag::constant("header");
pub const hgroup: HtmlTag = HtmlTag::constant("hgroup");
pub const hr: HtmlTag = HtmlTag::constant("hr");
pub const html: HtmlTag = HtmlTag::constant("html");
pub const i: HtmlTag = HtmlTag::constant("i");
pub const iframe: HtmlTag = HtmlTag::constant("iframe");
pub const img: HtmlTag = HtmlTag::constant("img");
pub const input: HtmlTag = HtmlTag::constant("input");
pub const ins: HtmlTag = HtmlTag::constant("ins");
pub const kbd: HtmlTag = HtmlTag::constant("kbd");
pub const label: HtmlTag = HtmlTag::constant("label");
pub const legend: HtmlTag = HtmlTag::constant("legend");
pub const li: HtmlTag = HtmlTag::constant("li");
pub const link: HtmlTag = HtmlTag::constant("link");
pub const main: HtmlTag = HtmlTag::constant("main");
pub const map: HtmlTag = HtmlTag::constant("map");
pub const mark: HtmlTag = HtmlTag::constant("mark");
pub const menu: HtmlTag = HtmlTag::constant("menu");
pub const meta: HtmlTag = HtmlTag::constant("meta");
pub const meter: HtmlTag = HtmlTag::constant("meter");
pub const nav: HtmlTag = HtmlTag::constant("nav");
pub const noscript: HtmlTag = HtmlTag::constant("noscript");
pub const object: HtmlTag = HtmlTag::constant("object");
pub const ol: HtmlTag = HtmlTag::constant("ol");
pub const optgroup: HtmlTag = HtmlTag::constant("optgroup");
pub const option: HtmlTag = HtmlTag::constant("option");
pub const output: HtmlTag = HtmlTag::constant("output");
pub const p: HtmlTag = HtmlTag::constant("p");
pub const picture: HtmlTag = HtmlTag::constant("picture");
pub const pre: HtmlTag = HtmlTag::constant("pre");
pub const progress: HtmlTag = HtmlTag::constant("progress");
pub const q: HtmlTag = HtmlTag::constant("q");
pub const rp: HtmlTag = HtmlTag::constant("rp");
pub const rt: HtmlTag = HtmlTag::constant("rt");
pub const ruby: HtmlTag = HtmlTag::constant("ruby");
pub const s: HtmlTag = HtmlTag::constant("s");
pub const samp: HtmlTag = HtmlTag::constant("samp");
pub const script: HtmlTag = HtmlTag::constant("script");
pub const search: HtmlTag = HtmlTag::constant("search");
pub const section: HtmlTag = HtmlTag::constant("section");
pub const select: HtmlTag = HtmlTag::constant("select");
pub const slot: HtmlTag = HtmlTag::constant("slot");
pub const small: HtmlTag = HtmlTag::constant("small");
pub const source: HtmlTag = HtmlTag::constant("source");
pub const span: HtmlTag = HtmlTag::constant("span");
pub const strong: HtmlTag = HtmlTag::constant("strong");
pub const style: HtmlTag = HtmlTag::constant("style");
pub const sub: HtmlTag = HtmlTag::constant("sub");
pub const summary: HtmlTag = HtmlTag::constant("summary");
pub const sup: HtmlTag = HtmlTag::constant("sup");
pub const table: HtmlTag = HtmlTag::constant("table");
pub const tbody: HtmlTag = HtmlTag::constant("tbody");
pub const td: HtmlTag = HtmlTag::constant("td");
pub const template: HtmlTag = HtmlTag::constant("template");
pub const textarea: HtmlTag = HtmlTag::constant("textarea");
pub const tfoot: HtmlTag = HtmlTag::constant("tfoot");
pub const th: HtmlTag = HtmlTag::constant("th");
pub const thead: HtmlTag = HtmlTag::constant("thead");
pub const time: HtmlTag = HtmlTag::constant("time");
pub const title: HtmlTag = HtmlTag::constant("title");
pub const tr: HtmlTag = HtmlTag::constant("tr");
pub const track: HtmlTag = HtmlTag::constant("track");
pub const u: HtmlTag = HtmlTag::constant("u");
pub const ul: HtmlTag = HtmlTag::constant("ul");
pub const var: HtmlTag = HtmlTag::constant("var");
pub const video: HtmlTag = HtmlTag::constant("video");
pub const wbr: HtmlTag = HtmlTag::constant("wbr");
tags! { /// Whether this is a void tag whose associated element may not have
a
abbr
address
area
article
aside
audio
b
base
bdi
bdo
blockquote
body
br
button
canvas
caption
cite
code
col
colgroup
data
datalist
dd
del
details
dfn
dialog
div
dl
dt
em
embed
fieldset
figcaption
figure
footer
form
h1
h2
h3
h4
h5
h6
head
header
hgroup
hr
html
i
iframe
img
input
ins
kbd
label
legend
li
link
main
map
mark
menu
meta
meter
nav
noscript
object
ol
optgroup
option
output
p
param
picture
pre
progress
q
rp
rt
ruby
s
samp
script
search
section
select
slot
small
source
span
strong
style
sub
summary
sup
table
tbody
td
template
textarea
tfoot
th
thead
time
title
tr
track
u
ul
var
video
wbr
}
/// Whether this is a void tag whose associated element may not have a
/// children. /// children.
pub fn is_void(tag: HtmlTag) -> bool { pub fn is_void(tag: HtmlTag) -> bool {
matches!( matches!(
@ -490,7 +490,6 @@ pub mod tag {
| self::input | self::input
| self::link | self::link
| self::meta | self::meta
| self::param
| self::source | self::source
| self::track | self::track
| self::wbr | self::wbr
@ -629,36 +628,196 @@ pub mod tag {
} }
} }
/// Predefined constants for HTML attributes.
///
/// Note: These are very incomplete.
#[allow(non_upper_case_globals)] #[allow(non_upper_case_globals)]
#[rustfmt::skip]
pub mod attr { pub mod attr {
use super::HtmlAttr; use crate::html::HtmlAttr;
pub const abbr: HtmlAttr = HtmlAttr::constant("abbr");
macro_rules! attrs { pub const accept: HtmlAttr = HtmlAttr::constant("accept");
($($attr:ident)*) => { pub const accept_charset: HtmlAttr = HtmlAttr::constant("accept-charset");
$(#[allow(non_upper_case_globals)] pub const accesskey: HtmlAttr = HtmlAttr::constant("accesskey");
pub const $attr: HtmlAttr = HtmlAttr::constant( pub const action: HtmlAttr = HtmlAttr::constant("action");
stringify!($attr) pub const allow: HtmlAttr = HtmlAttr::constant("allow");
);)* pub const allowfullscreen: HtmlAttr = HtmlAttr::constant("allowfullscreen");
} pub const alpha: HtmlAttr = HtmlAttr::constant("alpha");
} pub const alt: HtmlAttr = HtmlAttr::constant("alt");
pub const aria_activedescendant: HtmlAttr = HtmlAttr::constant("aria-activedescendant");
attrs! { pub const aria_atomic: HtmlAttr = HtmlAttr::constant("aria-atomic");
charset pub const aria_autocomplete: HtmlAttr = HtmlAttr::constant("aria-autocomplete");
cite pub const aria_busy: HtmlAttr = HtmlAttr::constant("aria-busy");
colspan pub const aria_checked: HtmlAttr = HtmlAttr::constant("aria-checked");
content pub const aria_colcount: HtmlAttr = HtmlAttr::constant("aria-colcount");
href pub const aria_colindex: HtmlAttr = HtmlAttr::constant("aria-colindex");
name pub const aria_colspan: HtmlAttr = HtmlAttr::constant("aria-colspan");
reversed pub const aria_controls: HtmlAttr = HtmlAttr::constant("aria-controls");
role pub const aria_current: HtmlAttr = HtmlAttr::constant("aria-current");
rowspan pub const aria_describedby: HtmlAttr = HtmlAttr::constant("aria-describedby");
start pub const aria_details: HtmlAttr = HtmlAttr::constant("aria-details");
style pub const aria_disabled: HtmlAttr = HtmlAttr::constant("aria-disabled");
value pub const aria_errormessage: HtmlAttr = HtmlAttr::constant("aria-errormessage");
} pub const aria_expanded: HtmlAttr = HtmlAttr::constant("aria-expanded");
pub const aria_flowto: HtmlAttr = HtmlAttr::constant("aria-flowto");
pub const aria_haspopup: HtmlAttr = HtmlAttr::constant("aria-haspopup");
pub const aria_hidden: HtmlAttr = HtmlAttr::constant("aria-hidden");
pub const aria_invalid: HtmlAttr = HtmlAttr::constant("aria-invalid");
pub const aria_keyshortcuts: HtmlAttr = HtmlAttr::constant("aria-keyshortcuts");
pub const aria_label: HtmlAttr = HtmlAttr::constant("aria-label");
pub const aria_labelledby: HtmlAttr = HtmlAttr::constant("aria-labelledby");
pub const aria_level: HtmlAttr = HtmlAttr::constant("aria-level"); pub const aria_level: HtmlAttr = HtmlAttr::constant("aria-level");
pub const aria_live: HtmlAttr = HtmlAttr::constant("aria-live");
pub const aria_modal: HtmlAttr = HtmlAttr::constant("aria-modal");
pub const aria_multiline: HtmlAttr = HtmlAttr::constant("aria-multiline");
pub const aria_multiselectable: HtmlAttr = HtmlAttr::constant("aria-multiselectable");
pub const aria_orientation: HtmlAttr = HtmlAttr::constant("aria-orientation");
pub const aria_owns: HtmlAttr = HtmlAttr::constant("aria-owns");
pub const aria_placeholder: HtmlAttr = HtmlAttr::constant("aria-placeholder");
pub const aria_posinset: HtmlAttr = HtmlAttr::constant("aria-posinset");
pub const aria_pressed: HtmlAttr = HtmlAttr::constant("aria-pressed");
pub const aria_readonly: HtmlAttr = HtmlAttr::constant("aria-readonly");
pub const aria_relevant: HtmlAttr = HtmlAttr::constant("aria-relevant");
pub const aria_required: HtmlAttr = HtmlAttr::constant("aria-required");
pub const aria_roledescription: HtmlAttr = HtmlAttr::constant("aria-roledescription");
pub const aria_rowcount: HtmlAttr = HtmlAttr::constant("aria-rowcount");
pub const aria_rowindex: HtmlAttr = HtmlAttr::constant("aria-rowindex");
pub const aria_rowspan: HtmlAttr = HtmlAttr::constant("aria-rowspan");
pub const aria_selected: HtmlAttr = HtmlAttr::constant("aria-selected");
pub const aria_setsize: HtmlAttr = HtmlAttr::constant("aria-setsize");
pub const aria_sort: HtmlAttr = HtmlAttr::constant("aria-sort");
pub const aria_valuemax: HtmlAttr = HtmlAttr::constant("aria-valuemax");
pub const aria_valuemin: HtmlAttr = HtmlAttr::constant("aria-valuemin");
pub const aria_valuenow: HtmlAttr = HtmlAttr::constant("aria-valuenow");
pub const aria_valuetext: HtmlAttr = HtmlAttr::constant("aria-valuetext");
pub const r#as: HtmlAttr = HtmlAttr::constant("as");
pub const r#async: HtmlAttr = HtmlAttr::constant("async");
pub const autocapitalize: HtmlAttr = HtmlAttr::constant("autocapitalize");
pub const autocomplete: HtmlAttr = HtmlAttr::constant("autocomplete");
pub const autocorrect: HtmlAttr = HtmlAttr::constant("autocorrect");
pub const autofocus: HtmlAttr = HtmlAttr::constant("autofocus");
pub const autoplay: HtmlAttr = HtmlAttr::constant("autoplay");
pub const blocking: HtmlAttr = HtmlAttr::constant("blocking");
pub const charset: HtmlAttr = HtmlAttr::constant("charset");
pub const checked: HtmlAttr = HtmlAttr::constant("checked");
pub const cite: HtmlAttr = HtmlAttr::constant("cite");
pub const class: HtmlAttr = HtmlAttr::constant("class");
pub const closedby: HtmlAttr = HtmlAttr::constant("closedby");
pub const color: HtmlAttr = HtmlAttr::constant("color");
pub const colorspace: HtmlAttr = HtmlAttr::constant("colorspace");
pub const cols: HtmlAttr = HtmlAttr::constant("cols");
pub const colspan: HtmlAttr = HtmlAttr::constant("colspan");
pub const command: HtmlAttr = HtmlAttr::constant("command");
pub const commandfor: HtmlAttr = HtmlAttr::constant("commandfor");
pub const content: HtmlAttr = HtmlAttr::constant("content");
pub const contenteditable: HtmlAttr = HtmlAttr::constant("contenteditable");
pub const controls: HtmlAttr = HtmlAttr::constant("controls");
pub const coords: HtmlAttr = HtmlAttr::constant("coords");
pub const crossorigin: HtmlAttr = HtmlAttr::constant("crossorigin");
pub const data: HtmlAttr = HtmlAttr::constant("data");
pub const datetime: HtmlAttr = HtmlAttr::constant("datetime");
pub const decoding: HtmlAttr = HtmlAttr::constant("decoding");
pub const default: HtmlAttr = HtmlAttr::constant("default");
pub const defer: HtmlAttr = HtmlAttr::constant("defer");
pub const dir: HtmlAttr = HtmlAttr::constant("dir");
pub const dirname: HtmlAttr = HtmlAttr::constant("dirname");
pub const disabled: HtmlAttr = HtmlAttr::constant("disabled");
pub const download: HtmlAttr = HtmlAttr::constant("download");
pub const draggable: HtmlAttr = HtmlAttr::constant("draggable");
pub const enctype: HtmlAttr = HtmlAttr::constant("enctype");
pub const enterkeyhint: HtmlAttr = HtmlAttr::constant("enterkeyhint");
pub const fetchpriority: HtmlAttr = HtmlAttr::constant("fetchpriority");
pub const r#for: HtmlAttr = HtmlAttr::constant("for");
pub const form: HtmlAttr = HtmlAttr::constant("form");
pub const formaction: HtmlAttr = HtmlAttr::constant("formaction");
pub const formenctype: HtmlAttr = HtmlAttr::constant("formenctype");
pub const formmethod: HtmlAttr = HtmlAttr::constant("formmethod");
pub const formnovalidate: HtmlAttr = HtmlAttr::constant("formnovalidate");
pub const formtarget: HtmlAttr = HtmlAttr::constant("formtarget");
pub const headers: HtmlAttr = HtmlAttr::constant("headers");
pub const height: HtmlAttr = HtmlAttr::constant("height");
pub const hidden: HtmlAttr = HtmlAttr::constant("hidden");
pub const high: HtmlAttr = HtmlAttr::constant("high");
pub const href: HtmlAttr = HtmlAttr::constant("href");
pub const hreflang: HtmlAttr = HtmlAttr::constant("hreflang");
pub const http_equiv: HtmlAttr = HtmlAttr::constant("http-equiv");
pub const id: HtmlAttr = HtmlAttr::constant("id");
pub const imagesizes: HtmlAttr = HtmlAttr::constant("imagesizes");
pub const imagesrcset: HtmlAttr = HtmlAttr::constant("imagesrcset");
pub const inert: HtmlAttr = HtmlAttr::constant("inert");
pub const inputmode: HtmlAttr = HtmlAttr::constant("inputmode");
pub const integrity: HtmlAttr = HtmlAttr::constant("integrity");
pub const is: HtmlAttr = HtmlAttr::constant("is");
pub const ismap: HtmlAttr = HtmlAttr::constant("ismap");
pub const itemid: HtmlAttr = HtmlAttr::constant("itemid");
pub const itemprop: HtmlAttr = HtmlAttr::constant("itemprop");
pub const itemref: HtmlAttr = HtmlAttr::constant("itemref");
pub const itemscope: HtmlAttr = HtmlAttr::constant("itemscope");
pub const itemtype: HtmlAttr = HtmlAttr::constant("itemtype");
pub const kind: HtmlAttr = HtmlAttr::constant("kind");
pub const label: HtmlAttr = HtmlAttr::constant("label");
pub const lang: HtmlAttr = HtmlAttr::constant("lang");
pub const list: HtmlAttr = HtmlAttr::constant("list");
pub const loading: HtmlAttr = HtmlAttr::constant("loading");
pub const r#loop: HtmlAttr = HtmlAttr::constant("loop");
pub const low: HtmlAttr = HtmlAttr::constant("low");
pub const max: HtmlAttr = HtmlAttr::constant("max");
pub const maxlength: HtmlAttr = HtmlAttr::constant("maxlength");
pub const media: HtmlAttr = HtmlAttr::constant("media");
pub const method: HtmlAttr = HtmlAttr::constant("method");
pub const min: HtmlAttr = HtmlAttr::constant("min");
pub const minlength: HtmlAttr = HtmlAttr::constant("minlength");
pub const multiple: HtmlAttr = HtmlAttr::constant("multiple");
pub const muted: HtmlAttr = HtmlAttr::constant("muted");
pub const name: HtmlAttr = HtmlAttr::constant("name");
pub const nomodule: HtmlAttr = HtmlAttr::constant("nomodule");
pub const nonce: HtmlAttr = HtmlAttr::constant("nonce");
pub const novalidate: HtmlAttr = HtmlAttr::constant("novalidate");
pub const open: HtmlAttr = HtmlAttr::constant("open");
pub const optimum: HtmlAttr = HtmlAttr::constant("optimum");
pub const pattern: HtmlAttr = HtmlAttr::constant("pattern");
pub const ping: HtmlAttr = HtmlAttr::constant("ping");
pub const placeholder: HtmlAttr = HtmlAttr::constant("placeholder");
pub const playsinline: HtmlAttr = HtmlAttr::constant("playsinline");
pub const popover: HtmlAttr = HtmlAttr::constant("popover");
pub const popovertarget: HtmlAttr = HtmlAttr::constant("popovertarget");
pub const popovertargetaction: HtmlAttr = HtmlAttr::constant("popovertargetaction");
pub const poster: HtmlAttr = HtmlAttr::constant("poster");
pub const preload: HtmlAttr = HtmlAttr::constant("preload");
pub const readonly: HtmlAttr = HtmlAttr::constant("readonly");
pub const referrerpolicy: HtmlAttr = HtmlAttr::constant("referrerpolicy");
pub const rel: HtmlAttr = HtmlAttr::constant("rel");
pub const required: HtmlAttr = HtmlAttr::constant("required");
pub const reversed: HtmlAttr = HtmlAttr::constant("reversed");
pub const role: HtmlAttr = HtmlAttr::constant("role");
pub const rows: HtmlAttr = HtmlAttr::constant("rows");
pub const rowspan: HtmlAttr = HtmlAttr::constant("rowspan");
pub const sandbox: HtmlAttr = HtmlAttr::constant("sandbox");
pub const scope: HtmlAttr = HtmlAttr::constant("scope");
pub const selected: HtmlAttr = HtmlAttr::constant("selected");
pub const shadowrootclonable: HtmlAttr = HtmlAttr::constant("shadowrootclonable");
pub const shadowrootcustomelementregistry: HtmlAttr = HtmlAttr::constant("shadowrootcustomelementregistry");
pub const shadowrootdelegatesfocus: HtmlAttr = HtmlAttr::constant("shadowrootdelegatesfocus");
pub const shadowrootmode: HtmlAttr = HtmlAttr::constant("shadowrootmode");
pub const shadowrootserializable: HtmlAttr = HtmlAttr::constant("shadowrootserializable");
pub const shape: HtmlAttr = HtmlAttr::constant("shape");
pub const size: HtmlAttr = HtmlAttr::constant("size");
pub const sizes: HtmlAttr = HtmlAttr::constant("sizes");
pub const slot: HtmlAttr = HtmlAttr::constant("slot");
pub const span: HtmlAttr = HtmlAttr::constant("span");
pub const spellcheck: HtmlAttr = HtmlAttr::constant("spellcheck");
pub const src: HtmlAttr = HtmlAttr::constant("src");
pub const srcdoc: HtmlAttr = HtmlAttr::constant("srcdoc");
pub const srclang: HtmlAttr = HtmlAttr::constant("srclang");
pub const srcset: HtmlAttr = HtmlAttr::constant("srcset");
pub const start: HtmlAttr = HtmlAttr::constant("start");
pub const step: HtmlAttr = HtmlAttr::constant("step");
pub const style: HtmlAttr = HtmlAttr::constant("style");
pub const tabindex: HtmlAttr = HtmlAttr::constant("tabindex");
pub const target: HtmlAttr = HtmlAttr::constant("target");
pub const title: HtmlAttr = HtmlAttr::constant("title");
pub const translate: HtmlAttr = HtmlAttr::constant("translate");
pub const r#type: HtmlAttr = HtmlAttr::constant("type");
pub const usemap: HtmlAttr = HtmlAttr::constant("usemap");
pub const value: HtmlAttr = HtmlAttr::constant("value");
pub const width: HtmlAttr = HtmlAttr::constant("width");
pub const wrap: HtmlAttr = HtmlAttr::constant("wrap");
pub const writingsuggestions: HtmlAttr = HtmlAttr::constant("writingsuggestions");
} }

View File

@ -1,6 +1,7 @@
//! HTML output. //! HTML output.
mod dom; mod dom;
mod typed;
pub use self::dom::*; pub use self::dom::*;
@ -14,6 +15,7 @@ pub fn module() -> Module {
html.start_category(crate::Category::Html); html.start_category(crate::Category::Html);
html.define_elem::<HtmlElem>(); html.define_elem::<HtmlElem>();
html.define_elem::<FrameElem>(); html.define_elem::<FrameElem>();
self::typed::define(&mut html);
Module::new("html", html) Module::new("html", html)
} }

View File

@ -0,0 +1,868 @@
//! The typed HTML element API (e.g. `html.div`).
//!
//! The typed API is backed by generated data derived from the HTML
//! specification. See [generated] and `tools/codegen`.
use std::fmt::Write;
use std::num::{NonZeroI64, NonZeroU64};
use std::sync::LazyLock;
use bumpalo::Bump;
use comemo::Tracked;
use ecow::{eco_format, eco_vec, EcoString};
use typst_assets::html as data;
use typst_macros::cast;
use crate::diag::{bail, At, Hint, HintedStrResult, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
Args, Array, AutoValue, CastInfo, Content, Context, Datetime, Dict, Duration,
FromValue, IntoValue, NativeFuncData, NativeFuncPtr, NoneValue, ParamInfo,
PositiveF64, Reflect, Scope, Str, Type, Value,
};
use crate::html::tag;
use crate::html::{HtmlAttr, HtmlAttrs, HtmlElem, HtmlTag};
use crate::layout::{Axes, Axis, Dir, Length};
use crate::visualize::Color;
/// Hook up all typed HTML definitions.
pub(super) fn define(html: &mut Scope) {
for data in FUNCS.iter() {
html.define_func_with_data(data);
}
}
/// Lazily created functions for all typed HTML constructors.
static FUNCS: LazyLock<Vec<NativeFuncData>> = LazyLock::new(|| {
// Leaking is okay here. It's not meaningfully different from having
// memory-managed values as `FUNCS` is a static.
let bump = Box::leak(Box::new(Bump::new()));
data::ELEMS.iter().map(|info| create_func_data(info, bump)).collect()
});
/// Creates metadata for a native HTML element constructor function.
fn create_func_data(
element: &'static data::ElemInfo,
bump: &'static Bump,
) -> NativeFuncData {
NativeFuncData {
function: NativeFuncPtr(bump.alloc(
move |_: &mut Engine, _: Tracked<Context>, args: &mut Args| {
construct(element, args)
},
)),
name: element.name,
title: {
let title = bump.alloc_str(element.name);
title[0..1].make_ascii_uppercase();
title
},
docs: element.docs,
keywords: &[],
contextual: false,
scope: LazyLock::new(&|| Scope::new()),
params: LazyLock::new(bump.alloc(move || create_param_info(element))),
returns: LazyLock::new(&|| CastInfo::Type(Type::of::<Content>())),
}
}
/// Creates parameter signature metadata for an element.
fn create_param_info(element: &'static data::ElemInfo) -> Vec<ParamInfo> {
let mut params = vec![];
for attr in element.attributes() {
params.push(ParamInfo {
name: attr.name,
docs: attr.docs,
input: AttrType::convert(attr.ty).input(),
default: None,
positional: false,
named: true,
variadic: false,
required: false,
settable: false,
});
}
let tag = HtmlTag::constant(element.name);
if !tag::is_void(tag) {
params.push(ParamInfo {
name: "body",
docs: "The contents of the HTML element.",
input: CastInfo::Type(Type::of::<Content>()),
default: None,
positional: true,
named: false,
variadic: false,
required: false,
settable: false,
});
}
params
}
/// The native constructor function shared by all HTML elements.
fn construct(element: &'static data::ElemInfo, args: &mut Args) -> SourceResult<Value> {
let mut attrs = HtmlAttrs::default();
let mut errors = eco_vec![];
args.items.retain(|item| {
let Some(name) = &item.name else { return true };
let Some(attr) = element.get_attr(name) else { return true };
let span = item.value.span;
let value = std::mem::take(&mut item.value.v);
let ty = AttrType::convert(attr.ty);
match ty.cast(value).at(span) {
Ok(Some(string)) => attrs.push(HtmlAttr::constant(attr.name), string),
Ok(None) => {}
Err(diags) => errors.extend(diags),
}
false
});
if !errors.is_empty() {
return Err(errors);
}
let tag = HtmlTag::constant(element.name);
let mut elem = HtmlElem::new(tag);
if !attrs.0.is_empty() {
elem.push_attrs(attrs);
}
if !tag::is_void(tag) {
let body = args.eat::<Content>()?;
elem.push_body(body);
}
Ok(elem.into_value())
}
/// A dynamic representation of an attribute's type.
///
/// See the documentation of [`data::Type`] for more details on variants.
enum AttrType {
Presence,
Native(NativeType),
Strings(StringsType),
Union(UnionType),
List(ListType),
}
impl AttrType {
/// Converts the type definition into a representation suitable for casting
/// and reflection.
const fn convert(ty: data::Type) -> AttrType {
use data::Type;
match ty {
Type::Presence => Self::Presence,
Type::None => Self::of::<NoneValue>(),
Type::NoneEmpty => Self::of::<NoneEmpty>(),
Type::NoneUndefined => Self::of::<NoneUndefined>(),
Type::Auto => Self::of::<AutoValue>(),
Type::TrueFalse => Self::of::<TrueFalseBool>(),
Type::YesNo => Self::of::<YesNoBool>(),
Type::OnOff => Self::of::<OnOffBool>(),
Type::Int => Self::of::<i64>(),
Type::NonNegativeInt => Self::of::<u64>(),
Type::PositiveInt => Self::of::<NonZeroU64>(),
Type::Float => Self::of::<f64>(),
Type::PositiveFloat => Self::of::<PositiveF64>(),
Type::Str => Self::of::<Str>(),
Type::Char => Self::of::<char>(),
Type::Datetime => Self::of::<Datetime>(),
Type::Duration => Self::of::<Duration>(),
Type::Color => Self::of::<Color>(),
Type::HorizontalDir => Self::of::<HorizontalDir>(),
Type::IconSize => Self::of::<IconSize>(),
Type::ImageCandidate => Self::of::<ImageCandidate>(),
Type::SourceSize => Self::of::<SourceSize>(),
Type::Strings(start, end) => Self::Strings(StringsType { start, end }),
Type::Union(variants) => Self::Union(UnionType(variants)),
Type::List(inner, separator, shorthand) => {
Self::List(ListType { inner, separator, shorthand })
}
}
}
/// Produces the dynamic representation of an attribute type backed by a
/// native Rust type.
const fn of<T: IntoAttr>() -> Self {
Self::Native(NativeType::of::<T>())
}
/// See [`Reflect::input`].
fn input(&self) -> CastInfo {
match self {
Self::Presence => bool::input(),
Self::Native(ty) => (ty.input)(),
Self::Union(ty) => ty.input(),
Self::Strings(ty) => ty.input(),
Self::List(ty) => ty.input(),
}
}
/// See [`Reflect::castable`].
fn castable(&self, value: &Value) -> bool {
match self {
Self::Presence => bool::castable(value),
Self::Native(ty) => (ty.castable)(value),
Self::Union(ty) => ty.castable(value),
Self::Strings(ty) => ty.castable(value),
Self::List(ty) => ty.castable(value),
}
}
/// Tries to cast the value into this attribute's type and serialize it into
/// an HTML attribute string.
fn cast(&self, value: Value) -> HintedStrResult<Option<EcoString>> {
match self {
Self::Presence => value.cast::<bool>().map(|b| b.then(EcoString::new)),
Self::Native(ty) => (ty.cast)(value),
Self::Union(ty) => ty.cast(value),
Self::Strings(ty) => ty.cast(value),
Self::List(ty) => ty.cast(value),
}
}
}
/// An enumeration with generated string variants.
///
/// `start` and `end` are used to index into `data::ATTR_STRINGS`.
struct StringsType {
start: usize,
end: usize,
}
impl StringsType {
fn input(&self) -> CastInfo {
CastInfo::Union(
self.strings()
.iter()
.map(|(val, desc)| CastInfo::Value(val.into_value(), desc))
.collect(),
)
}
fn castable(&self, value: &Value) -> bool {
match value {
Value::Str(s) => self.strings().iter().any(|&(v, _)| v == s.as_str()),
_ => false,
}
}
fn cast(&self, value: Value) -> HintedStrResult<Option<EcoString>> {
if self.castable(&value) {
value.cast().map(Some)
} else {
Err(self.input().error(&value))
}
}
fn strings(&self) -> &'static [(&'static str, &'static str)] {
&data::ATTR_STRINGS[self.start..self.end]
}
}
/// A type that accepts any of the contained types.
struct UnionType(&'static [data::Type]);
impl UnionType {
fn input(&self) -> CastInfo {
CastInfo::Union(self.iter().map(|ty| ty.input()).collect())
}
fn castable(&self, value: &Value) -> bool {
self.iter().any(|ty| ty.castable(value))
}
fn cast(&self, value: Value) -> HintedStrResult<Option<EcoString>> {
for item in self.iter() {
if item.castable(&value) {
return item.cast(value);
}
}
Err(self.input().error(&value))
}
fn iter(&self) -> impl Iterator<Item = AttrType> {
self.0.iter().map(|&ty| AttrType::convert(ty))
}
}
/// A list of items separated by a specific separator char.
///
/// - <https://html.spec.whatwg.org/#space-separated-tokens>
/// - <https://html.spec.whatwg.org/#comma-separated-tokens>
struct ListType {
inner: &'static data::Type,
separator: char,
shorthand: bool,
}
impl ListType {
fn input(&self) -> CastInfo {
if self.shorthand {
Array::input() + self.inner().input()
} else {
Array::input()
}
}
fn castable(&self, value: &Value) -> bool {
Array::castable(value) || (self.shorthand && self.inner().castable(value))
}
fn cast(&self, value: Value) -> HintedStrResult<Option<EcoString>> {
let ty = self.inner();
if Array::castable(&value) {
let array = value.cast::<Array>()?;
let mut out = EcoString::new();
for (i, item) in array.into_iter().enumerate() {
let item = ty.cast(item)?.unwrap();
if item.as_str().contains(self.separator) {
let buf;
let name = match self.separator {
' ' => "space",
',' => "comma",
_ => {
buf = eco_format!("'{}'", self.separator);
buf.as_str()
}
};
bail!(
"array item may not contain a {name}";
hint: "the array attribute will be encoded as a \
{name}-separated string"
);
}
if i > 0 {
out.push(self.separator);
if self.separator == ',' {
out.push(' ');
}
}
out.push_str(&item);
}
Ok(Some(out))
} else if self.shorthand && ty.castable(&value) {
let item = ty.cast(value)?.unwrap();
Ok(Some(item))
} else {
Err(self.input().error(&value))
}
}
fn inner(&self) -> AttrType {
AttrType::convert(*self.inner)
}
}
/// A dynamic representation of attribute backed by a native type implementing
/// - the standard `Reflect` and `FromValue` traits for casting from a value,
/// - the special `IntoAttr` trait for conversion into an attribute string.
#[derive(Copy, Clone)]
struct NativeType {
input: fn() -> CastInfo,
cast: fn(Value) -> HintedStrResult<Option<EcoString>>,
castable: fn(&Value) -> bool,
}
impl NativeType {
/// Creates a dynamic native type from a native Rust type.
const fn of<T: IntoAttr>() -> Self {
Self {
cast: |value| {
let this = value.cast::<T>()?;
Ok(Some(this.into_attr()))
},
input: T::input,
castable: T::castable,
}
}
}
/// Casts a native type into an HTML attribute.
pub trait IntoAttr: FromValue {
/// Turn the value into an attribute string.
fn into_attr(self) -> EcoString;
}
impl IntoAttr for Str {
fn into_attr(self) -> EcoString {
self.into()
}
}
/// A boolean that is encoded as a string:
/// - `false` is encoded as `"false"`
/// - `true` is encoded as `"true"`
pub struct TrueFalseBool(pub bool);
cast! {
TrueFalseBool,
v: bool => Self(v),
}
impl IntoAttr for TrueFalseBool {
fn into_attr(self) -> EcoString {
if self.0 { "true" } else { "false" }.into()
}
}
/// A boolean that is encoded as a string:
/// - `false` is encoded as `"no"`
/// - `true` is encoded as `"yes"`
pub struct YesNoBool(pub bool);
cast! {
YesNoBool,
v: bool => Self(v),
}
impl IntoAttr for YesNoBool {
fn into_attr(self) -> EcoString {
if self.0 { "yes" } else { "no" }.into()
}
}
/// A boolean that is encoded as a string:
/// - `false` is encoded as `"off"`
/// - `true` is encoded as `"on"`
pub struct OnOffBool(pub bool);
cast! {
OnOffBool,
v: bool => Self(v),
}
impl IntoAttr for OnOffBool {
fn into_attr(self) -> EcoString {
if self.0 { "on" } else { "off" }.into()
}
}
impl IntoAttr for AutoValue {
fn into_attr(self) -> EcoString {
"auto".into()
}
}
impl IntoAttr for NoneValue {
fn into_attr(self) -> EcoString {
"none".into()
}
}
/// A `none` value that turns into an empty string attribute.
struct NoneEmpty;
cast! {
NoneEmpty,
_: NoneValue => NoneEmpty,
}
impl IntoAttr for NoneEmpty {
fn into_attr(self) -> EcoString {
"".into()
}
}
/// A `none` value that turns into the string `"undefined"`.
struct NoneUndefined;
cast! {
NoneUndefined,
_: NoneValue => NoneUndefined,
}
impl IntoAttr for NoneUndefined {
fn into_attr(self) -> EcoString {
"undefined".into()
}
}
impl IntoAttr for char {
fn into_attr(self) -> EcoString {
eco_format!("{self}")
}
}
impl IntoAttr for i64 {
fn into_attr(self) -> EcoString {
eco_format!("{self}")
}
}
impl IntoAttr for u64 {
fn into_attr(self) -> EcoString {
eco_format!("{self}")
}
}
impl IntoAttr for NonZeroI64 {
fn into_attr(self) -> EcoString {
eco_format!("{self}")
}
}
impl IntoAttr for NonZeroU64 {
fn into_attr(self) -> EcoString {
eco_format!("{self}")
}
}
impl IntoAttr for f64 {
fn into_attr(self) -> EcoString {
// HTML float literal allows all the things that Rust's float `Display`
// impl produces.
eco_format!("{self}")
}
}
impl IntoAttr for PositiveF64 {
fn into_attr(self) -> EcoString {
self.get().into_attr()
}
}
impl IntoAttr for Color {
fn into_attr(self) -> EcoString {
eco_format!("{}", css::color(self))
}
}
impl IntoAttr for Duration {
fn into_attr(self) -> EcoString {
// https://html.spec.whatwg.org/#valid-duration-string
let mut out = EcoString::new();
macro_rules! part {
($s:literal) => {
if !out.is_empty() {
out.push(' ');
}
write!(out, $s).unwrap();
};
}
let [weeks, days, hours, minutes, seconds] = self.decompose();
if weeks > 0 {
part!("{weeks}w");
}
if days > 0 {
part!("{days}d");
}
if hours > 0 {
part!("{hours}h");
}
if minutes > 0 {
part!("{minutes}m");
}
if seconds > 0 || out.is_empty() {
part!("{seconds}s");
}
out
}
}
impl IntoAttr for Datetime {
fn into_attr(self) -> EcoString {
let fmt = typst_utils::display(|f| match self {
Self::Date(date) => datetime::date(f, date),
Self::Time(time) => datetime::time(f, time),
Self::Datetime(datetime) => datetime::datetime(f, datetime),
});
eco_format!("{fmt}")
}
}
mod datetime {
use std::fmt::{self, Formatter, Write};
pub fn datetime(f: &mut Formatter, datetime: time::PrimitiveDateTime) -> fmt::Result {
// https://html.spec.whatwg.org/#valid-global-date-and-time-string
date(f, datetime.date())?;
f.write_char('T')?;
time(f, datetime.time())
}
pub fn date(f: &mut Formatter, date: time::Date) -> fmt::Result {
// https://html.spec.whatwg.org/#valid-date-string
write!(f, "{:04}-{:02}-{:02}", date.year(), date.month() as u8, date.day())
}
pub fn time(f: &mut Formatter, time: time::Time) -> fmt::Result {
// https://html.spec.whatwg.org/#valid-time-string
write!(f, "{:02}:{:02}", time.hour(), time.minute())?;
if time.second() > 0 {
write!(f, ":{:02}", time.second())?;
}
Ok(())
}
}
/// A direction on the X axis: `ltr` or `rtl`.
pub struct HorizontalDir(Dir);
cast! {
HorizontalDir,
v: Dir => {
if v.axis() == Axis::Y {
bail!("direction must be horizontal");
}
Self(v)
},
}
impl IntoAttr for HorizontalDir {
fn into_attr(self) -> EcoString {
self.0.into_attr()
}
}
impl IntoAttr for Dir {
fn into_attr(self) -> EcoString {
match self {
Self::LTR => "ltr".into(),
Self::RTL => "rtl".into(),
Self::TTB => "ttb".into(),
Self::BTT => "btt".into(),
}
}
}
/// A width/height pair for `<link rel="icon" sizes="..." />`.
pub struct IconSize(Axes<u64>);
cast! {
IconSize,
v: Axes<u64> => Self(v),
}
impl IntoAttr for IconSize {
fn into_attr(self) -> EcoString {
eco_format!("{}x{}", self.0.x, self.0.y)
}
}
/// <https://html.spec.whatwg.org/#image-candidate-string>
pub struct ImageCandidate(EcoString);
cast! {
ImageCandidate,
mut v: Dict => {
let src = v.take("src")?.cast::<EcoString>()?;
let width: Option<NonZeroU64> =
v.take("width").ok().map(Value::cast).transpose()?;
let density: Option<PositiveF64> =
v.take("density").ok().map(Value::cast).transpose()?;
v.finish(&["src", "width", "density"])?;
if src.is_empty() {
bail!("`src` must not be empty");
} else if src.starts_with(',') || src.ends_with(',') {
bail!("`src` must not start or end with a comma");
}
let mut out = src;
match (width, density) {
(None, None) => {}
(Some(width), None) => write!(out, " {width}w").unwrap(),
(None, Some(density)) => write!(out, " {}d", density.get()).unwrap(),
(Some(_), Some(_)) => bail!("cannot specify both `width` and `density`"),
}
Self(out)
},
}
impl IntoAttr for ImageCandidate {
fn into_attr(self) -> EcoString {
self.0
}
}
/// <https://html.spec.whatwg.org/multipage/images.html#valid-source-size-list>
pub struct SourceSize(EcoString);
cast! {
SourceSize,
mut v: Dict => {
let condition = v.take("condition")?.cast::<EcoString>()?;
let size = v
.take("size")?
.cast::<Length>()
.hint("CSS lengths that are not expressible as Typst lengths are not yet supported")
.hint("you can use `html.elem` to create a raw attribute")?;
Self(eco_format!("({condition}) {}", css::length(size)))
},
}
impl IntoAttr for SourceSize {
fn into_attr(self) -> EcoString {
self.0
}
}
/// Conversion from Typst data types into CSS data types.
///
/// This can be moved elsewhere once we start supporting more CSS stuff.
mod css {
use std::fmt::{self, Display};
use typst_utils::Numeric;
use crate::layout::Length;
use crate::visualize::{Color, Hsl, LinearRgb, Oklab, Oklch, Rgb};
pub fn length(length: Length) -> impl Display {
typst_utils::display(move |f| match (length.abs.is_zero(), length.em.is_zero()) {
(false, false) => {
write!(f, "calc({}pt + {}em)", length.abs.to_pt(), length.em.get())
}
(true, false) => write!(f, "{}em", length.em.get()),
(_, true) => write!(f, "{}pt", length.abs.to_pt()),
})
}
pub fn color(color: Color) -> impl Display {
typst_utils::display(move |f| match color {
Color::Rgb(_) | Color::Cmyk(_) | Color::Luma(_) => rgb(f, color.to_rgb()),
Color::Oklab(v) => oklab(f, v),
Color::Oklch(v) => oklch(f, v),
Color::LinearRgb(v) => linear_rgb(f, v),
Color::Hsl(_) | Color::Hsv(_) => hsl(f, color.to_hsl()),
})
}
fn oklab(f: &mut fmt::Formatter<'_>, v: Oklab) -> fmt::Result {
write!(
f,
"oklab({} {} {}{})",
percent(v.l),
number(v.a),
number(v.b),
alpha(v.alpha)
)
}
fn oklch(f: &mut fmt::Formatter<'_>, v: Oklch) -> fmt::Result {
write!(
f,
"oklch({} {} {}deg{})",
percent(v.l),
number(v.chroma),
number(v.hue.into_degrees()),
alpha(v.alpha)
)
}
fn rgb(f: &mut fmt::Formatter<'_>, v: Rgb) -> fmt::Result {
if let Some(v) = rgb_to_8_bit_lossless(v) {
let (r, g, b, a) = v.into_components();
write!(f, "#{r:02x}{g:02x}{b:02x}")?;
if a != u8::MAX {
write!(f, "{a:02x}")?;
}
Ok(())
} else {
write!(
f,
"rgb({} {} {}{})",
percent(v.red),
percent(v.green),
percent(v.blue),
alpha(v.alpha)
)
}
}
/// Converts an f32 RGBA color to its 8-bit representation if the result is
/// [very close](is_very_close) to the original.
fn rgb_to_8_bit_lossless(
v: Rgb,
) -> Option<palette::rgb::Rgba<palette::encoding::Srgb, u8>> {
let l = v.into_format::<u8, u8>();
let h = l.into_format::<f32, f32>();
(is_very_close(v.red, h.red)
&& is_very_close(v.blue, h.blue)
&& is_very_close(v.green, h.green)
&& is_very_close(v.alpha, h.alpha))
.then_some(l)
}
fn linear_rgb(f: &mut fmt::Formatter<'_>, v: LinearRgb) -> fmt::Result {
write!(
f,
"color(srgb-linear {} {} {}{})",
percent(v.red),
percent(v.green),
percent(v.blue),
alpha(v.alpha),
)
}
fn hsl(f: &mut fmt::Formatter<'_>, v: Hsl) -> fmt::Result {
write!(
f,
"hsl({}deg {} {}{})",
number(v.hue.into_degrees()),
percent(v.saturation),
percent(v.lightness),
alpha(v.alpha),
)
}
/// Displays an alpha component if it not 1.
fn alpha(value: f32) -> impl Display {
typst_utils::display(move |f| {
if !is_very_close(value, 1.0) {
write!(f, " / {}", percent(value))?;
}
Ok(())
})
}
/// Displays a rounded percentage.
///
/// For a percentage, two significant digits after the comma gives us a
/// precision of 1/10_000, which is more than 12 bits (see `is_very_close`).
fn percent(ratio: f32) -> impl Display {
typst_utils::display(move |f| {
write!(f, "{}%", typst_utils::round_with_precision(ratio as f64 * 100.0, 2))
})
}
/// Rounds a number for display.
///
/// For a number between 0 and 1, four significant digits give us a
/// precision of 1/10_000, which is more than 12 bits (see `is_very_close`).
fn number(value: f32) -> impl Display {
typst_utils::round_with_precision(value as f64, 4)
}
/// Whether two component values are close enough that there is no
/// difference when encoding them with 12-bit. 12 bit is the highest
/// reasonable color bit depth found in the industry.
fn is_very_close(a: f32, b: f32) -> bool {
const MAX_BIT_DEPTH: u32 = 12;
const EPS: f32 = 0.5 / 2_i32.pow(MAX_BIT_DEPTH) as f32;
(a - b).abs() < EPS
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tags_and_attr_const_internible() {
for elem in data::ELEMS {
let _ = HtmlTag::constant(elem.name);
}
for attr in data::ATTRS {
let _ = HtmlAttr::constant(attr.name);
}
}
}

View File

@ -446,7 +446,7 @@ impl IntrospectorBuilder {
HtmlNode::Element(elem) => self.discover_in_html(sink, &elem.children), HtmlNode::Element(elem) => self.discover_in_html(sink, &elem.children),
HtmlNode::Frame(frame) => self.discover_in_frame( HtmlNode::Frame(frame) => self.discover_in_frame(
sink, sink,
frame, &frame.inner,
NonZeroUsize::ONE, NonZeroUsize::ONE,
Transform::identity(), Transform::identity(),
), ),

View File

@ -104,7 +104,7 @@ impl Show for Packed<AlignElem> {
} }
} }
/// Where to [align] something along an axis. /// Where to align something along an axis.
/// ///
/// Possible values are: /// Possible values are:
/// - `start`: Aligns at the [start]($direction.start) of the [text /// - `start`: Aligns at the [start]($direction.start) of the [text

View File

@ -4,9 +4,12 @@ use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref, Not};
use typst_utils::Get; use typst_utils::Get;
use crate::diag::bail; use crate::diag::{bail, HintedStrResult};
use crate::foundations::{array, cast, Array, Resolve, Smart, StyleChain}; use crate::foundations::{
use crate::layout::{Abs, Dir, Length, Ratio, Rel, Size}; array, cast, Array, CastInfo, FromValue, IntoValue, Reflect, Resolve, Smart,
StyleChain, Value,
};
use crate::layout::{Abs, Dir, Rel, Size};
/// A container with a horizontal and vertical component. /// A container with a horizontal and vertical component.
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
@ -275,40 +278,39 @@ impl BitAndAssign for Axes<bool> {
} }
} }
cast! { impl<T: Reflect> Reflect for Axes<T> {
Axes<Rel<Length>>, fn input() -> CastInfo {
self => array![self.x, self.y].into_value(), Array::input()
array: Array => { }
let mut iter = array.into_iter();
match (iter.next(), iter.next(), iter.next()) { fn output() -> CastInfo {
(Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?), Array::output()
_ => bail!("point array must contain exactly two entries"), }
fn castable(value: &Value) -> bool {
Array::castable(value)
} }
},
} }
cast! { impl<T: FromValue> FromValue for Axes<T> {
Axes<Ratio>, fn from_value(value: Value) -> HintedStrResult<Self> {
self => array![self.x, self.y].into_value(), let array = value.cast::<Array>()?;
array: Array => {
let mut iter = array.into_iter(); let mut iter = array.into_iter();
match (iter.next(), iter.next(), iter.next()) { match (iter.next(), iter.next(), iter.next()) {
(Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?), (Some(a), Some(b), None) => Ok(Axes::new(a.cast()?, b.cast()?)),
_ => bail!("ratio array must contain exactly two entries"), _ => bail!(
"array must contain exactly two items";
hint: "the first item determines the value for the X axis \
and the second item the value for the Y axis"
),
}
} }
},
} }
cast! { impl<T: IntoValue> IntoValue for Axes<T> {
Axes<Length>, fn into_value(self) -> Value {
self => array![self.x, self.y].into_value(), array![self.x.into_value(), self.y.into_value()].into_value()
array: Array => {
let mut iter = array.into_iter();
match (iter.next(), iter.next(), iter.next()) {
(Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?),
_ => bail!("length array must contain exactly two entries"),
} }
},
} }
impl<T: Resolve> Resolve for Axes<T> { impl<T: Resolve> Resolve for Axes<T> {

View File

@ -496,6 +496,16 @@ pub struct GridFooter {
#[default(true)] #[default(true)]
pub repeat: bool, pub repeat: bool,
/// The level of the footer. Must not be zero.
///
/// This allows repeating multiple footers at once. Footers with different
/// levels can repeat together, as long as they have descending levels.
///
/// Notably, when a footer with a lower level stops repeating, all higher
/// or equal level headers start repeating, replacing the previous footer.
#[default(NonZeroU32::ONE)]
pub level: NonZeroU32,
/// The cells and lines within the footer. /// The cells and lines within the footer.
#[variadic] #[variadic]
pub children: Vec<GridItem>, pub children: Vec<GridItem>,

View File

@ -54,6 +54,7 @@ pub fn grid_to_cellgrid<'a>(
}, },
GridChild::Footer(footer) => ResolvableGridChild::Footer { GridChild::Footer(footer) => ResolvableGridChild::Footer {
repeat: footer.repeat(styles), repeat: footer.repeat(styles),
level: footer.level(styles),
span: footer.span(), span: footer.span(),
items: footer.children.iter().map(resolve_item), items: footer.children.iter().map(resolve_item),
}, },
@ -108,6 +109,7 @@ pub fn table_to_cellgrid<'a>(
}, },
TableChild::Footer(footer) => ResolvableGridChild::Footer { TableChild::Footer(footer) => ResolvableGridChild::Footer {
repeat: footer.repeat(styles), repeat: footer.repeat(styles),
level: footer.level(styles),
span: footer.span(), span: footer.span(),
items: footer.children.iter().map(resolve_item), items: footer.children.iter().map(resolve_item),
}, },
@ -445,31 +447,22 @@ pub struct Header {
} }
/// A repeatable grid footer. Stops at the last row. /// A repeatable grid footer. Stops at the last row.
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct Footer { pub struct Footer {
/// The first row included in this footer. /// The range of rows included in this footer.
pub start: usize, pub range: Range<usize>,
/// The index after the last row included in this footer.
pub end: usize,
/// The footer's level. /// The footer's level.
/// ///
/// Used similarly to header level. /// Used similarly to header level.
pub level: u32, pub level: u32,
} }
impl Footer {
/// The footer's range of included rows.
#[inline]
pub fn range(&self) -> Range<usize> {
self.start..self.end
}
}
/// A possibly repeatable grid child (header or footer). /// A possibly repeatable grid child (header or footer).
/// ///
/// It still exists even when not repeatable, but must not have additional /// It still exists even when not repeatable, but must not have additional
/// considerations by grid layout, other than for consistency (such as making /// considerations by grid layout, other than for consistency (such as making
/// a certain group of rows unbreakable). /// a certain group of rows unbreakable).
#[derive(Debug, Clone)]
pub struct Repeatable<T> { pub struct Repeatable<T> {
inner: T, inner: T,
@ -656,7 +649,7 @@ impl<'a> Entry<'a> {
/// Any grid child, which can be either a header or an item. /// Any grid child, which can be either a header or an item.
pub enum ResolvableGridChild<T: ResolvableCell, I> { pub enum ResolvableGridChild<T: ResolvableCell, I> {
Header { repeat: bool, level: NonZeroU32, span: Span, items: I }, Header { repeat: bool, level: NonZeroU32, span: Span, items: I },
Footer { repeat: bool, span: Span, items: I }, Footer { repeat: bool, level: NonZeroU32, span: Span, items: I },
Item(ResolvableGridItem<T>), Item(ResolvableGridItem<T>),
} }
@ -678,8 +671,12 @@ pub struct CellGrid<'a> {
pub hlines: Vec<Vec<Line>>, pub hlines: Vec<Vec<Line>>,
/// The repeatable headers of this grid. /// The repeatable headers of this grid.
pub headers: Vec<Repeatable<Header>>, pub headers: Vec<Repeatable<Header>>,
/// The repeatable footer of this grid. /// The repeatable footers of this grid.
pub footer: Option<Repeatable<Footer>>, pub footers: Vec<Repeatable<Footer>>,
/// Footers sorted by order of when they start repeating, or should
/// otherwise be laid out for the first time (even if only once, for
/// non-repeating footers).
pub sorted_footers: Vec<Repeatable<Footer>>,
/// Whether this grid has gutters. /// Whether this grid has gutters.
pub has_gutter: bool, pub has_gutter: bool,
} }
@ -692,7 +689,7 @@ impl<'a> CellGrid<'a> {
cells: impl IntoIterator<Item = Cell<'a>>, cells: impl IntoIterator<Item = Cell<'a>>,
) -> Self { ) -> Self {
let entries = cells.into_iter().map(Entry::Cell).collect(); let entries = cells.into_iter().map(Entry::Cell).collect();
Self::new_internal(tracks, gutter, vec![], vec![], vec![], None, entries) Self::new_internal(tracks, gutter, vec![], vec![], vec![], vec![], entries)
} }
/// Generates the cell grid, given the tracks and resolved entries. /// Generates the cell grid, given the tracks and resolved entries.
@ -702,7 +699,7 @@ impl<'a> CellGrid<'a> {
vlines: Vec<Vec<Line>>, vlines: Vec<Vec<Line>>,
hlines: Vec<Vec<Line>>, hlines: Vec<Vec<Line>>,
headers: Vec<Repeatable<Header>>, headers: Vec<Repeatable<Header>>,
footer: Option<Repeatable<Footer>>, footers: Vec<Repeatable<Footer>>,
entries: Vec<Entry<'a>>, entries: Vec<Entry<'a>>,
) -> Self { ) -> Self {
let mut cols = vec![]; let mut cols = vec![];
@ -749,6 +746,8 @@ impl<'a> CellGrid<'a> {
rows.pop(); rows.pop();
} }
let sorted_footers = simulate_footer_repetition(&footers);
Self { Self {
cols, cols,
rows, rows,
@ -756,7 +755,8 @@ impl<'a> CellGrid<'a> {
vlines, vlines,
hlines, hlines,
headers, headers,
footer, footers,
sorted_footers,
has_gutter, has_gutter,
} }
} }
@ -895,6 +895,11 @@ impl<'a> CellGrid<'a> {
pub fn has_repeated_headers(&self) -> bool { pub fn has_repeated_headers(&self) -> bool {
self.headers.iter().any(|h| h.repeated) self.headers.iter().any(|h| h.repeated)
} }
#[inline]
pub fn has_repeated_footers(&self) -> bool {
self.footers.iter().any(|f| f.repeated)
}
} }
/// Resolves and positions all cells in the grid before creating it. /// Resolves and positions all cells in the grid before creating it.
@ -977,6 +982,7 @@ struct RowGroupData {
/// ///
/// This stays as `None` for fully empty headers and footers. /// This stays as `None` for fully empty headers and footers.
range: Option<Range<usize>>, range: Option<Range<usize>>,
#[allow(dead_code)] // TODO: should we remove this?
span: Span, span: Span,
kind: RowGroupKind, kind: RowGroupKind,
@ -1034,15 +1040,13 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
let has_gutter = self.gutter.any(|tracks| !tracks.is_empty()); let has_gutter = self.gutter.any(|tracks| !tracks.is_empty());
let mut headers: Vec<Repeatable<Header>> = vec![]; let mut headers: Vec<Repeatable<Header>> = vec![];
let mut footers: Vec<Repeatable<Footer>> = vec![];
// Stores where the footer is supposed to end, its span, and the // The first and last rows containing a cell outside a row group, that
// actual footer structure. // is, outside a header or footer. Headers after the last such row and
let mut footer: Option<(usize, Span, Footer)> = None; // footers before the first such row have no "children" cells and thus
let mut repeat_footer = false; // are not repeated.
let mut first_last_cell_rows = None;
// If true, there has been at least one cell besides headers and
// footers. When false, footers at the end are forced to not repeat.
let mut at_least_one_cell = false;
// We can't just use the cell's index in the 'cells' vector to // We can't just use the cell's index in the 'cells' vector to
// determine its automatic position, since cells could have arbitrary // determine its automatic position, since cells could have arbitrary
@ -1060,10 +1064,11 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
// automatically-positioned cell. // automatically-positioned cell.
let mut auto_index: usize = 0; let mut auto_index: usize = 0;
// The next header after the latest auto-positioned cell. This is used // The next header and footer after the latest auto-positioned cell.
// to avoid checking for collision with headers that were already // These are used to avoid checking for collision with headers that
// skipped. // were already skipped.
let mut next_header = 0; let mut next_header = 0;
let mut next_footer = 0;
// We have to rebuild the grid to account for fixed cell positions. // We have to rebuild the grid to account for fixed cell positions.
// //
@ -1086,12 +1091,12 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
&mut pending_hlines, &mut pending_hlines,
&mut pending_vlines, &mut pending_vlines,
&mut headers, &mut headers,
&mut footer, &mut footers,
&mut repeat_footer,
&mut auto_index, &mut auto_index,
&mut next_header, &mut next_header,
&mut next_footer,
&mut resolved_cells, &mut resolved_cells,
&mut at_least_one_cell, &mut first_last_cell_rows,
child, child,
)?; )?;
} }
@ -1107,13 +1112,12 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
row_amount, row_amount,
)?; )?;
let footer = self.finalize_headers_and_footers( self.finalize_headers_and_footers(
has_gutter, has_gutter,
&mut headers, &mut headers,
footer, &mut footers,
repeat_footer,
row_amount, row_amount,
at_least_one_cell, first_last_cell_rows,
)?; )?;
Ok(CellGrid::new_internal( Ok(CellGrid::new_internal(
@ -1122,7 +1126,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
vlines, vlines,
hlines, hlines,
headers, headers,
footer, footers,
resolved_cells, resolved_cells,
)) ))
} }
@ -1142,12 +1146,12 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
pending_hlines: &mut Vec<(Span, Line, bool)>, pending_hlines: &mut Vec<(Span, Line, bool)>,
pending_vlines: &mut Vec<(Span, Line)>, pending_vlines: &mut Vec<(Span, Line)>,
headers: &mut Vec<Repeatable<Header>>, headers: &mut Vec<Repeatable<Header>>,
footer: &mut Option<(usize, Span, Footer)>, footers: &mut Vec<Repeatable<Footer>>,
repeat_footer: &mut bool,
auto_index: &mut usize, auto_index: &mut usize,
next_header: &mut usize, next_header: &mut usize,
next_footer: &mut usize,
resolved_cells: &mut Vec<Option<Entry<'x>>>, resolved_cells: &mut Vec<Option<Entry<'x>>>,
at_least_one_cell: &mut bool, first_last_cell_rows: &mut Option<(usize, usize)>,
child: ResolvableGridChild<T, I>, child: ResolvableGridChild<T, I>,
) -> SourceResult<()> ) -> SourceResult<()>
where where
@ -1198,6 +1202,12 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
&mut (*next_header).clone() &mut (*next_header).clone()
}; };
let local_next_footer = if matches!(child, ResolvableGridChild::Item(_)) {
next_footer
} else {
&mut (*next_footer).clone()
};
// The first row in which this table group can fit. // The first row in which this table group can fit.
// //
// Within headers and footers, this will correspond to the first // Within headers and footers, this will correspond to the first
@ -1207,7 +1217,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
let mut first_available_row = 0; let mut first_available_row = 0;
let (header_footer_items, simple_item) = match child { let (header_footer_items, simple_item) = match child {
ResolvableGridChild::Header { repeat, level, span, items, .. } => { ResolvableGridChild::Header { repeat, level, span, items } => {
row_group_data = Some(RowGroupData { row_group_data = Some(RowGroupData {
range: None, range: None,
span, span,
@ -1234,17 +1244,13 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
(Some(items), None) (Some(items), None)
} }
ResolvableGridChild::Footer { repeat, span, items, .. } => { ResolvableGridChild::Footer { repeat, level, span, items } => {
if footer.is_some() {
bail!(span, "cannot have more than one footer");
}
row_group_data = Some(RowGroupData { row_group_data = Some(RowGroupData {
range: None, range: None,
span, span,
repeat, repeat,
kind: RowGroupKind::Footer, kind: RowGroupKind::Footer,
repeatable_level: NonZeroU32::ONE, repeatable_level: level,
top_hlines_start: pending_hlines.len(), top_hlines_start: pending_hlines.len(),
top_hlines_end: None, top_hlines_end: None,
}); });
@ -1256,13 +1262,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
(Some(items), None) (Some(items), None)
} }
ResolvableGridChild::Item(item) => { ResolvableGridChild::Item(item) => (None, Some(item)),
if matches!(item, ResolvableGridItem::Cell(_)) {
*at_least_one_cell = true;
}
(None, Some(item))
}
}; };
let items = header_footer_items.into_iter().flatten().chain(simple_item); let items = header_footer_items.into_iter().flatten().chain(simple_item);
@ -1382,10 +1382,11 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
colspan, colspan,
rowspan, rowspan,
headers, headers,
footer.as_ref(), footers,
resolved_cells, resolved_cells,
local_auto_index, local_auto_index,
local_next_header, local_next_header,
local_next_footer,
first_available_row, first_available_row,
columns, columns,
row_group_data.is_some(), row_group_data.is_some(),
@ -1443,6 +1444,13 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
// no longer appear at the top. // no longer appear at the top.
*top_hlines_end = Some(pending_hlines.len()); *top_hlines_end = Some(pending_hlines.len());
} }
} else {
// This is a cell outside a row group.
*first_last_cell_rows = Some(
first_last_cell_rows
.map(|(first, last)| (first.min(y), last.max(y)))
.unwrap_or((y, y)),
);
} }
// Let's resolve the cell so it can determine its own fields // Let's resolve the cell so it can determine its own fields
@ -1607,23 +1615,18 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
RowGroupKind::Footer => { RowGroupKind::Footer => {
// Only check if the footer is at the end later, once we know // Only check if the footer is at the end later, once we know
// the final amount of rows. // the final amount of rows.
*footer = Some(( let data = Footer {
group_range.end, // Later on, we have to correct this range in case there
row_group.span,
Footer {
// Later on, we have to correct this number in case there
// is gutter, but only once all cells have been analyzed // is gutter, but only once all cells have been analyzed
// and the header's and footer's exact boundaries are // and the header's and footer's exact boundaries are
// known. That is because the gutter row immediately // known. That is because the gutter row immediately
// before the footer might not be included as part of // before the footer might not be included as part of
// the footer if it is contained within the header. // the footer if it is contained within the header.
start: group_range.start, range: group_range,
end: group_range.end, level: row_group.repeatable_level.get(),
level: 1, };
},
));
*repeat_footer = row_group.repeat; footers.push(Repeatable { inner: data, repeated: row_group.repeat });
} }
} }
} }
@ -1788,36 +1791,41 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
/// an adjacent gutter row to be repeated alongside that header or /// an adjacent gutter row to be repeated alongside that header or
/// footer, if there is gutter; /// footer, if there is gutter;
/// 3. Wrap headers and footers in the correct [`Repeatable`] variant. /// 3. Wrap headers and footers in the correct [`Repeatable`] variant.
#[allow(clippy::type_complexity)]
fn finalize_headers_and_footers( fn finalize_headers_and_footers(
&self, &self,
has_gutter: bool, has_gutter: bool,
headers: &mut [Repeatable<Header>], headers: &mut [Repeatable<Header>],
footer: Option<(usize, Span, Footer)>, footers: &mut [Repeatable<Footer>],
repeat_footer: bool,
row_amount: usize, row_amount: usize,
at_least_one_cell: bool, first_last_cell_rows: Option<(usize, usize)>,
) -> SourceResult<Option<Repeatable<Footer>>> { ) -> SourceResult<()> {
// Mark consecutive headers right before the end of the table, or the // Mark consecutive headers right before the end of the table, or the
// final footer, as short lived, given that there are no normal rows // footers at the end, as short lived, given that there are no normal
// after them, so repeating them is pointless. // rows after them, so repeating them is pointless.
// //
// It is important to do this BEFORE we update header and footer ranges // Same for consecutive footers right after the start of the table or
// due to gutter below as 'row_amount' doesn't consider gutter. // any initial headers.
// if let Some((first_cell_row, last_cell_row)) = first_last_cell_rows {
// TODO(subfooters): take the last footer if it is at the end and for header in
// backtrack through consecutive footers until the first one in the headers.iter_mut().rev().take_while(|h| h.range.start > last_cell_row)
// sequence is found. If there is no footer at the end, there are no {
// haeders to turn short-lived. header.short_lived = true;
let mut consecutive_header_start = }
footer.as_ref().map(|(_, _, f)| f.start).unwrap_or(row_amount);
for header_at_the_end in headers.iter_mut().rev().take_while(move |h| {
let at_the_end = h.range.end == consecutive_header_start;
consecutive_header_start = h.range.start; for footer in footers.iter_mut().take_while(|f| f.range.end <= first_cell_row)
at_the_end {
}) { // TODO(subfooters): short lived
header_at_the_end.short_lived = true; footer.repeated = false;
}
} else {
// No cells outside headers or footers, so nobody repeats!
for header in &mut *headers {
header.short_lived = true;
}
for footer in &mut *footers {
// TODO(subfooters): short lived
footer.repeated = false;
}
} }
// Repeat the gutter below a header (hence why we don't // Repeat the gutter below a header (hence why we don't
@ -1849,14 +1857,8 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
let row_amount = (2 * row_amount).saturating_sub(1); let row_amount = (2 * row_amount).saturating_sub(1);
header.range.end = header.range.end.min(row_amount); header.range.end = header.range.end.min(row_amount);
} }
}
let footer = footer
.map(|(footer_end, footer_span, mut footer)| {
if footer_end != row_amount {
bail!(footer_span, "footer must end at the last row");
}
for footer in &mut *footers {
// TODO(subfooters): will need a global slice of headers and // TODO(subfooters): will need a global slice of headers and
// footers for when we have multiple footers // footers for when we have multiple footers
// Alternatively, never include the gutter in the footer's // Alternatively, never include the gutter in the footer's
@ -1869,15 +1871,15 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
// out the footer for real, the mechanism can be disabled. // out the footer for real, the mechanism can be disabled.
let last_header_end = headers.last().map(|header| header.range.end); let last_header_end = headers.last().map(|header| header.range.end);
if has_gutter {
// Convert the footer's start index to post-gutter coordinates. // Convert the footer's start index to post-gutter coordinates.
footer.start *= 2; footer.range.start *= 2;
// TODO: this probably has to change
// Include the gutter right before the footer, unless there is // Include the gutter right before the footer, unless there is
// none, or the gutter is already included in the header (no // none, or the gutter is already included in the header (no
// rows between the header and the footer). // rows between the header and the footer).
if last_header_end != Some(footer.start) { if last_header_end != Some(footer.range.start) {
footer.start = footer.start.saturating_sub(1); footer.range.start = footer.range.start.saturating_sub(1);
} }
// Adapt footer end but DO NOT include the gutter below it, // Adapt footer end but DO NOT include the gutter below it,
@ -1890,24 +1892,11 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
// //
// It also keeps us within the total amount of rows, so we // It also keeps us within the total amount of rows, so we
// don't need to '.min()' later. // don't need to '.min()' later.
footer.end = (2 * footer.end).saturating_sub(1); footer.range.end = (2 * footer.range.end).saturating_sub(1);
}
} }
Ok(footer) Ok(())
})
.transpose()?
.map(|footer| {
// Don't repeat footers when the table only has headers and
// footers.
// TODO(subfooters): Switch this to marking the last N
// consecutive footers as short lived.
Repeatable {
inner: footer,
repeated: repeat_footer && at_least_one_cell,
}
});
Ok(footer)
} }
/// Resolves the cell's fields based on grid-wide properties. /// Resolves the cell's fields based on grid-wide properties.
@ -2079,7 +2068,7 @@ fn expand_row_group(
/// Check if a cell's fixed row would conflict with a header or footer. /// Check if a cell's fixed row would conflict with a header or footer.
fn check_for_conflicting_cell_row( fn check_for_conflicting_cell_row(
headers: &[Repeatable<Header>], headers: &[Repeatable<Header>],
footer: Option<&(usize, Span, Footer)>, footers: &[Repeatable<Footer>],
cell_y: usize, cell_y: usize,
rowspan: usize, rowspan: usize,
) -> HintedStrResult<()> { ) -> HintedStrResult<()> {
@ -2098,14 +2087,15 @@ fn check_for_conflicting_cell_row(
); );
} }
if let Some((_, _, footer)) = footer { if footers
if cell_y < footer.end && cell_y + rowspan > footer.start { .iter()
.any(|footer| cell_y < footer.range.end && cell_y + rowspan > footer.range.start)
{
bail!( bail!(
"cell would conflict with footer spanning the same position"; "cell would conflict with footer spanning the same position";
hint: "try reducing the cell's rowspan or moving the footer" hint: "try reducing the cell's rowspan or moving the footer"
); );
} }
}
Ok(()) Ok(())
} }
@ -2126,10 +2116,11 @@ fn resolve_cell_position(
colspan: usize, colspan: usize,
rowspan: usize, rowspan: usize,
headers: &[Repeatable<Header>], headers: &[Repeatable<Header>],
footer: Option<&(usize, Span, Footer)>, footers: &[Repeatable<Footer>],
resolved_cells: &[Option<Entry>], resolved_cells: &[Option<Entry>],
auto_index: &mut usize, auto_index: &mut usize,
next_header: &mut usize, next_header: &mut usize,
next_footer: &mut usize,
first_available_row: usize, first_available_row: usize,
columns: usize, columns: usize,
in_row_group: bool, in_row_group: bool,
@ -2152,11 +2143,12 @@ fn resolve_cell_position(
// simply skipping existing cells, headers and footers. // simply skipping existing cells, headers and footers.
let resolved_index = find_next_available_position( let resolved_index = find_next_available_position(
headers, headers,
footer, footers,
resolved_cells, resolved_cells,
columns, columns,
*auto_index, *auto_index,
next_header, next_header,
next_footer,
false, false,
)?; )?;
@ -2193,7 +2185,7 @@ fn resolve_cell_position(
// footer (but only if it isn't already in one, otherwise there // footer (but only if it isn't already in one, otherwise there
// will already be a separate check). // will already be a separate check).
if !in_row_group { if !in_row_group {
check_for_conflicting_cell_row(headers, footer, cell_y, rowspan)?; check_for_conflicting_cell_row(headers, footers, cell_y, rowspan)?;
} }
cell_index(cell_x, cell_y) cell_index(cell_x, cell_y)
@ -2212,25 +2204,26 @@ fn resolve_cell_position(
// cell in. // cell in.
find_next_available_position( find_next_available_position(
headers, headers,
footer, footers,
resolved_cells, resolved_cells,
columns, columns,
initial_index, initial_index,
// Make our own copy of the 'next_header' counter, since it // Make new copies of the 'next_header/footer' counters,
// should only be updated by auto cells. However, we cannot // since they should only be updated by auto cells.
// start with the same value as we are searching from the // However, we cannot start with the same values as we are
// start, and not from 'auto_index', so auto cells might // searching from the start, and not from 'auto_index', so
// have skipped some headers already which this cell will // auto cells might have skipped some headers and footers
// also need to skip. // already which this cell will also need to skip.
// //
// We could, in theory, keep a separate 'next_header' // We could, in theory, keep separate 'next_header/footer'
// counter for cells with fixed columns. But then we would // counters for cells with fixed columns. But then we would
// need one for every column, and much like how there isn't // need one for every column, and much like how there isn't
// an index counter for each column either, the potential // an index counter for each column either, the potential
// speed gain seems less relevant for a less used feature. // speed gain seems less relevant for a less used feature.
// Still, it is something to consider for the future if // Still, it is something to consider for the future if
// this turns out to be a bottleneck in important cases. // this turns out to be a bottleneck in important cases.
&mut 0, &mut 0,
&mut 0,
true, true,
) )
} }
@ -2241,7 +2234,7 @@ fn resolve_cell_position(
// footer (but only if it isn't already in one, otherwise there // footer (but only if it isn't already in one, otherwise there
// will already be a separate check). // will already be a separate check).
if !in_row_group { if !in_row_group {
check_for_conflicting_cell_row(headers, footer, cell_y, rowspan)?; check_for_conflicting_cell_row(headers, footers, cell_y, rowspan)?;
} }
// Let's find the first column which has that row available. // Let's find the first column which has that row available.
@ -2276,14 +2269,16 @@ fn resolve_cell_position(
/// ///
/// When `skip_rows` is true, one row is skipped on each iteration, preserving /// When `skip_rows` is true, one row is skipped on each iteration, preserving
/// the column. That is used to find a position for a fixed column cell. /// the column. That is used to find a position for a fixed column cell.
#[allow(clippy::too_many_arguments)]
#[inline] #[inline]
fn find_next_available_position( fn find_next_available_position(
headers: &[Repeatable<Header>], headers: &[Repeatable<Header>],
footer: Option<&(usize, Span, Footer)>, footers: &[Repeatable<Footer>],
resolved_cells: &[Option<Entry<'_>>], resolved_cells: &[Option<Entry<'_>>],
columns: usize, columns: usize,
initial_index: usize, initial_index: usize,
next_header: &mut usize, next_header: &mut usize,
next_footer: &mut usize,
skip_rows: bool, skip_rows: bool,
) -> HintedStrResult<usize> { ) -> HintedStrResult<usize> {
let mut resolved_index = initial_index; let mut resolved_index = initial_index;
@ -2327,15 +2322,20 @@ fn find_next_available_position(
// From now on, only check the headers afterwards. // From now on, only check the headers afterwards.
*next_header += 1; *next_header += 1;
} else if let Some((footer_end, _, _)) = footer.filter(|(end, _, footer)| { } else if let Some(footer) = footers
resolved_index >= footer.start * columns && resolved_index < *end * columns .get(*next_footer)
}) { .filter(|footer| resolved_index >= footer.range.start * columns)
{
// Skip footer, for the same reason. // Skip footer, for the same reason.
resolved_index = *footer_end * columns; if resolved_index < footer.range.end * columns {
resolved_index = footer.range.end * columns;
if skip_rows { if skip_rows {
resolved_index += initial_index % columns; resolved_index += initial_index % columns;
} }
}
*next_footer += 1;
} else { } else {
return Ok(resolved_index); return Ok(resolved_index);
} }
@ -2389,3 +2389,50 @@ fn skip_auto_index_through_fully_merged_rows(
} }
} }
} }
/// Generates a vector where all footers are sorted ahead of time by the points
/// at which they start repeating. When a new footer is about to be laid out,
/// conflicting footers which come before it in this vector must stop
/// repeating.
fn simulate_footer_repetition(footers: &[Repeatable<Footer>]) -> Vec<Repeatable<Footer>> {
if footers.len() <= 1 {
return footers.to_vec();
}
let mut ordered_footers = Vec::with_capacity(footers.len());
let mut repeating_footers: Vec<&Repeatable<Footer>> = vec![];
// Read footers in reverse, using the same algorithm as headers to
// determine when a footer starts and stops repeating, but going from grid
// end to start. When it stops repeating, that's when it will start
// repeating in proper layout (from start to end), whereas it starts
// repeating here when it should stop repeating in practice. So,
// effectively, repeated footer layout is the same as for headers, but
// reversed, which we take advantage of by doing it reversed and then
// reversing it all back later.
for footer in footers.iter().rev() {
// Keep only lower level footers. Assume sorted by increasing levels.
let stopped_repeating = repeating_footers
.drain(repeating_footers.partition_point(|f| f.level < footer.level)..);
// If they stopped repeating here, that's when they will start
// repeating. We save them in reverse of the reverse order so they stay
// sorted by increasing levels when we reverse `ordered_footers` later.
ordered_footers.extend(stopped_repeating.rev().cloned());
if footer.repeated {
// Start repeating now. Vector stays sorted by increasing levels,
// as any higher-level footers stopped repeating now.
repeating_footers.push(footer);
} else {
// Immediately finishes repeating.
ordered_footers.push(footer.clone());
}
}
// Push remaining footers that repeat starting from the top of the grid
ordered_footers.extend(repeating_footers.into_iter().rev().cloned());
ordered_footers.reverse();
ordered_footers
}

View File

@ -148,7 +148,7 @@ pub struct Library {
/// The default style properties (for page size, font selection, and /// The default style properties (for page size, font selection, and
/// everything else configurable via set and show rules). /// everything else configurable via set and show rules).
pub styles: Styles, pub styles: Styles,
/// The standard library as a value. Used to provide the `std` variable. /// The standard library as a value. Used to provide the `std` module.
pub std: Binding, pub std: Binding,
/// In-development features that were enabled. /// In-development features that were enabled.
pub features: Features, pub features: Features,

View File

@ -1,3 +1,10 @@
use std::sync::LazyLock;
use icu_properties::maps::CodePointMapData;
use icu_properties::CanonicalCombiningClass;
use icu_provider::AsDeserializingBufferProvider;
use icu_provider_blob::BlobDataProvider;
use crate::diag::bail; use crate::diag::bail;
use crate::foundations::{cast, elem, func, Content, NativeElement, SymbolElem}; use crate::foundations::{cast, elem, func, Content, NativeElement, SymbolElem};
use crate::layout::{Length, Rel}; use crate::layout::{Length, Rel};
@ -81,17 +88,22 @@ impl Accent {
Self(Self::combine(c).unwrap_or(c)) Self(Self::combine(c).unwrap_or(c))
} }
/// List of bottom accents. Currently just a list of ones included in the
/// Unicode math class document.
const BOTTOM: &[char] = &[
'\u{0323}', '\u{032C}', '\u{032D}', '\u{032E}', '\u{032F}', '\u{0330}',
'\u{0331}', '\u{0332}', '\u{0333}', '\u{033A}', '\u{20E8}', '\u{20EC}',
'\u{20ED}', '\u{20EE}', '\u{20EF}',
];
/// Whether this accent is a bottom accent or not. /// Whether this accent is a bottom accent or not.
pub fn is_bottom(&self) -> bool { pub fn is_bottom(&self) -> bool {
Self::BOTTOM.contains(&self.0) static COMBINING_CLASS_DATA: LazyLock<CodePointMapData<CanonicalCombiningClass>> =
LazyLock::new(|| {
icu_properties::maps::load_canonical_combining_class(
&BlobDataProvider::try_new_from_static_blob(typst_assets::icu::ICU)
.unwrap()
.as_deserializing(),
)
.unwrap()
});
matches!(
COMBINING_CLASS_DATA.as_borrowed().get(self.0),
CanonicalCombiningClass::Below
)
} }
} }

View File

@ -16,7 +16,7 @@ use hayagriva::{
}; };
use indexmap::IndexMap; use indexmap::IndexMap;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use typst_syntax::{Span, Spanned}; use typst_syntax::{Span, Spanned, SyntaxMode};
use typst_utils::{Get, ManuallyHash, NonZeroExt, PicoStr}; use typst_utils::{Get, ManuallyHash, NonZeroExt, PicoStr};
use crate::diag::{ use crate::diag::{
@ -39,7 +39,7 @@ use crate::model::{
CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem, CitationForm, CiteGroup, Destination, FootnoteElem, HeadingElem, LinkElem, ParElem,
Url, Url,
}; };
use crate::routines::{EvalMode, Routines}; use crate::routines::Routines;
use crate::text::{ use crate::text::{
FontStyle, Lang, LocalName, Region, Smallcaps, SubElem, SuperElem, TextElem, FontStyle, Lang, LocalName, Region, Smallcaps, SubElem, SuperElem, TextElem,
WeightDelta, WeightDelta,
@ -90,7 +90,7 @@ use crate::World;
/// ``` /// ```
#[elem(Locatable, Synthesize, Show, ShowSet, LocalName)] #[elem(Locatable, Synthesize, Show, ShowSet, LocalName)]
pub struct BibliographyElem { pub struct BibliographyElem {
/// One or multiple paths to or raw bytes for Hayagriva `.yml` and/or /// One or multiple paths to or raw bytes for Hayagriva `.yaml` and/or
/// BibLaTeX `.bib` files. /// BibLaTeX `.bib` files.
/// ///
/// This can be a: /// This can be a:
@ -321,7 +321,11 @@ impl Bibliography {
for d in data.iter() { for d in data.iter() {
let library = decode_library(d)?; let library = decode_library(d)?;
for entry in library { for entry in library {
match map.entry(Label::new(PicoStr::intern(entry.key()))) { let label = Label::new(PicoStr::intern(entry.key()))
.ok_or("bibliography contains entry with empty key")
.at(d.source.span)?;
match map.entry(label) {
indexmap::map::Entry::Vacant(vacant) => { indexmap::map::Entry::Vacant(vacant) => {
vacant.insert(entry); vacant.insert(entry);
} }
@ -385,7 +389,7 @@ fn decode_library(loaded: &Loaded) -> SourceResult<Library> {
.within(loaded), .within(loaded),
_ => bail!( _ => bail!(
loaded.source.span, loaded.source.span,
"unknown bibliography format (must be .yml/.yaml or .bib)" "unknown bibliography format (must be .yaml/.yml or .bib)"
), ),
} }
} else { } else {
@ -592,7 +596,7 @@ impl Works {
/// Context for generating the bibliography. /// Context for generating the bibliography.
struct Generator<'a> { struct Generator<'a> {
/// The routines that is used to evaluate mathematical material in citations. /// The routines that are used to evaluate mathematical material in citations.
routines: &'a Routines, routines: &'a Routines,
/// The world that is used to evaluate mathematical material in citations. /// The world that is used to evaluate mathematical material in citations.
world: Tracked<'a, dyn World + 'a>, world: Tracked<'a, dyn World + 'a>,
@ -609,7 +613,7 @@ struct Generator<'a> {
/// Details about a group of merged citations. All citations are put into groups /// Details about a group of merged citations. All citations are put into groups
/// of adjacent ones (e.g., `@foo @bar` will merge into a group of length two). /// of adjacent ones (e.g., `@foo @bar` will merge into a group of length two).
/// Even single citations will be put into groups of length ones. /// Even single citations will be put into groups of length one.
struct GroupInfo { struct GroupInfo {
/// The group's location. /// The group's location.
location: Location, location: Location,
@ -1024,7 +1028,7 @@ impl ElemRenderer<'_> {
Sink::new().track_mut(), Sink::new().track_mut(),
math, math,
self.span, self.span,
EvalMode::Math, SyntaxMode::Math,
Scope::new(), Scope::new(),
) )
.map(Value::display) .map(Value::display)

View File

@ -225,25 +225,21 @@ pub struct OutlineElem {
/// to just specifying `{2em}`. /// to just specifying `{2em}`.
/// ///
/// ```example /// ```example
/// #set heading(numbering: "1.a.") /// >>> #show heading: none
/// #set heading(numbering: "I-I.")
/// #set outline(title: none)
/// ///
/// #outline( /// #outline()
/// title: [Contents (Automatic)], /// #line(length: 100%)
/// indent: auto, /// #outline(indent: 3em)
/// )
/// ///
/// #outline( /// = Software engineering technologies
/// title: [Contents (Length)], /// == Requirements
/// indent: 2em, /// == Tools and technologies
/// ) /// === Code editors
/// /// == Analyzing alternatives
/// = About ACME Corp. /// = Designing software components
/// == History /// = Testing and integration
/// === Origins
/// #lorem(10)
///
/// == Products
/// #lorem(10)
/// ``` /// ```
pub indent: Smart<OutlineIndent>, pub indent: Smart<OutlineIndent>,
} }
@ -450,8 +446,9 @@ impl OutlineEntry {
/// at the same level are aligned. /// at the same level are aligned.
/// ///
/// If the outline's indent is a fixed value or a function, the prefixes are /// If the outline's indent is a fixed value or a function, the prefixes are
/// indented, but the inner contents are simply inset from the prefix by the /// indented, but the inner contents are simply offset from the prefix by
/// specified `gap`, rather than aligning outline-wide. /// the specified `gap`, rather than aligning outline-wide. For a visual
/// explanation, see [`outline.indent`]($outline.indent).
#[func(contextual)] #[func(contextual)]
pub fn indented( pub fn indented(
&self, &self,

View File

@ -5,7 +5,7 @@ use crate::diag::{bail, At, Hint, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, Cast, Content, Context, Func, IntoValue, Label, NativeElement, Packed, cast, elem, Cast, Content, Context, Func, IntoValue, Label, NativeElement, Packed,
Show, Smart, StyleChain, Synthesize, Repr, Show, Smart, StyleChain, Synthesize,
}; };
use crate::introspection::{Counter, CounterKey, Locatable}; use crate::introspection::{Counter, CounterKey, Locatable};
use crate::math::EquationElem; use crate::math::EquationElem;
@ -79,6 +79,36 @@ use crate::text::TextElem;
/// reference: `[@intro[Chapter]]`. /// reference: `[@intro[Chapter]]`.
/// ///
/// # Customization /// # Customization
/// When you only ever need to reference pages of a figure/table/heading/etc. in
/// a document, the default `form` field value can be changed to `{"page"}` with
/// a set rule. If you prefer a short "p." supplement over "page", the
/// [`page.supplement`]($page.supplement) field can be used for changing this:
///
/// ```example
/// #set page(
/// numbering: "1",
/// supplement: "p.",
/// >>> margin: (bottom: 3em),
/// >>> footer-descent: 1.25em,
/// )
/// #set ref(form: "page")
///
/// #figure(
/// stack(
/// dir: ltr,
/// spacing: 1em,
/// circle(),
/// square(),
/// ),
/// caption: [Shapes],
/// ) <shapes>
///
/// #pagebreak()
///
/// See @shapes for examples
/// of different shapes.
/// ```
///
/// If you write a show rule for references, you can access the referenced /// If you write a show rule for references, you can access the referenced
/// element through the `element` field of the reference. The `element` may /// element through the `element` field of the reference. The `element` may
/// be `{none}` even if it exists if Typst hasn't discovered it yet, so you /// be `{none}` even if it exists if Typst hasn't discovered it yet, so you
@ -91,16 +121,13 @@ use crate::text::TextElem;
/// #show ref: it => { /// #show ref: it => {
/// let eq = math.equation /// let eq = math.equation
/// let el = it.element /// let el = it.element
/// if el != none and el.func() == eq { /// // Skip all other references.
/// if el == none or el.func() != eq { return it }
/// // Override equation references. /// // Override equation references.
/// link(el.location(),numbering( /// link(el.location(), numbering(
/// el.numbering, /// el.numbering,
/// ..counter(eq).at(el.location()) /// ..counter(eq).at(el.location())
/// )) /// ))
/// } else {
/// // Other references as usual.
/// it
/// }
/// } /// }
/// ///
/// = Beginnings <beginning> /// = Beginnings <beginning>
@ -229,8 +256,15 @@ impl Show for Packed<RefElem> {
// RefForm::Normal // RefForm::Normal
if BibliographyElem::has(engine, self.target) { if BibliographyElem::has(engine, self.target) {
if elem.is_ok() { if let Ok(elem) = elem {
bail!(span, "label occurs in the document and its bibliography"); bail!(
span,
"label `{}` occurs both in the document and its bibliography",
self.target.repr();
hint: "change either the {}'s label or the \
bibliography key to resolve the ambiguity",
elem.func().name(),
);
} }
return Ok(to_citation(self, engine, styles)?.pack().spanned(span)); return Ok(to_citation(self, engine, styles)?.pack().spanned(span));

View File

@ -292,12 +292,35 @@ fn show_cellgrid_html(grid: CellGrid, styles: StyleChain) -> Content {
elem(tag::tr, Content::sequence(row)) elem(tag::tr, Content::sequence(row))
}; };
// TODO(subfooters): similarly to headers, take consecutive footers from // Store all consecutive headers at the start in 'tfoot'. All remaining
// the end for 'tfoot'. // headers are just normal rows across the table body. (There doesn't
let footer = grid.footer.map(|ft| { // appear to be an equivalent of 'th' for footers in HTML.)
let rows = rows.drain(ft.start..); // TODO: test
elem(tag::tfoot, Content::sequence(rows.map(|row| tr(tag::td, row)))) let footer = {
}); let mut consecutive_footer_start = grid.rows.len();
let footers_at_end = grid
.footers
.iter()
.rev()
.take_while(|ft| {
let is_consecutive = ft.range.end == consecutive_footer_start;
consecutive_footer_start = ft.range.start;
is_consecutive
})
.count();
if footers_at_end > 0 {
let last_mid_table_footer = grid.footers.len() - footers_at_end;
let removed_footer_rows =
grid.footers.get(last_mid_table_footer).unwrap().range.start;
let rows = rows.drain(removed_footer_rows..);
Some(elem(tag::tfoot, Content::sequence(rows.map(|row| tr(tag::td, row)))))
} else {
None
}
};
// Store all consecutive headers at the start in 'thead'. All remaining // Store all consecutive headers at the start in 'thead'. All remaining
// headers are just 'th' rows across the table body. // headers are just 'th' rows across the table body.
@ -567,6 +590,16 @@ pub struct TableFooter {
#[default(true)] #[default(true)]
pub repeat: bool, pub repeat: bool,
/// The level of the footer. Must not be zero.
///
/// This allows repeating multiple footers at once. Footers with different
/// levels can repeat together, as long as they have descending levels.
///
/// Notably, when a footer with a lower level stops repeating, all higher
/// or equal level headers start repeating, replacing the previous footer.
#[default(NonZeroU32::ONE)]
pub level: NonZeroU32,
/// The cells and lines within the footer. /// The cells and lines within the footer.
#[variadic] #[variadic]
pub children: Vec<TableItem>, pub children: Vec<TableItem>,

View File

@ -59,7 +59,7 @@ pub struct EmbedElem {
// We can't distinguish between the two at the moment. // We can't distinguish between the two at the moment.
#[required] #[required]
#[parse( #[parse(
match args.find::<Bytes>()? { match args.eat::<Bytes>()? {
Some(data) => data, Some(data) => data,
None => engine.world.file(id).at(span)?, None => engine.world.file(id).at(span)?,
} }

View File

@ -4,7 +4,7 @@ use std::hash::{Hash, Hasher};
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use comemo::{Tracked, TrackedMut}; use comemo::{Tracked, TrackedMut};
use typst_syntax::Span; use typst_syntax::{Span, SyntaxMode};
use typst_utils::LazyHash; use typst_utils::LazyHash;
use crate::diag::SourceResult; use crate::diag::SourceResult;
@ -58,7 +58,7 @@ routines! {
sink: TrackedMut<Sink>, sink: TrackedMut<Sink>,
string: &str, string: &str,
span: Span, span: Span,
mode: EvalMode, mode: SyntaxMode,
scope: Scope, scope: Scope,
) -> SourceResult<Value> ) -> SourceResult<Value>
@ -312,17 +312,6 @@ routines! {
) -> SourceResult<Fragment> ) -> SourceResult<Fragment>
} }
/// In which mode to evaluate a string.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum EvalMode {
/// Evaluate as code, as after a hash.
Code,
/// Evaluate as markup, like in a Typst file.
Markup,
/// Evaluate as math, as in an equation.
Math,
}
/// Defines what kind of realization we are performing. /// Defines what kind of realization we are performing.
pub enum RealizationKind<'a> { pub enum RealizationKind<'a> {
/// This the root realization for layout. Requires a mutable reference /// This the root realization for layout. Requires a mutable reference

View File

@ -2,7 +2,10 @@ use smallvec::smallvec;
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{elem, Content, Packed, Show, Smart, StyleChain}; use crate::foundations::{
elem, Content, NativeElement, Packed, Show, Smart, StyleChain, TargetElem,
};
use crate::html::{attr, tag, HtmlElem};
use crate::layout::{Abs, Corners, Length, Rel, Sides}; use crate::layout::{Abs, Corners, Length, Rel, Sides};
use crate::text::{BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric}; use crate::text::{BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric};
use crate::visualize::{Color, FixedStroke, Paint, Stroke}; use crate::visualize::{Color, FixedStroke, Paint, Stroke};
@ -81,6 +84,16 @@ pub struct UnderlineElem {
impl Show for Packed<UnderlineElem> { impl Show for Packed<UnderlineElem> {
#[typst_macros::time(name = "underline", span = self.span())] #[typst_macros::time(name = "underline", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
if TargetElem::target_in(styles).is_html() {
// Note: In modern HTML, `<u>` is not the underline element, but
// rather an "Unarticulated Annotation" element (see HTML spec
// 4.5.22). Using `text-decoration` instead is recommended by MDN.
return Ok(HtmlElem::new(tag::span)
.with_attr(attr::style, "text-decoration: underline")
.with_body(Some(self.body.clone()))
.pack());
}
Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration {
line: DecoLine::Underline { line: DecoLine::Underline {
stroke: self.stroke(styles).unwrap_or_default(), stroke: self.stroke(styles).unwrap_or_default(),
@ -173,6 +186,13 @@ pub struct OverlineElem {
impl Show for Packed<OverlineElem> { impl Show for Packed<OverlineElem> {
#[typst_macros::time(name = "overline", span = self.span())] #[typst_macros::time(name = "overline", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::span)
.with_attr(attr::style, "text-decoration: overline")
.with_body(Some(self.body.clone()))
.pack());
}
Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration {
line: DecoLine::Overline { line: DecoLine::Overline {
stroke: self.stroke(styles).unwrap_or_default(), stroke: self.stroke(styles).unwrap_or_default(),
@ -250,6 +270,10 @@ pub struct StrikeElem {
impl Show for Packed<StrikeElem> { impl Show for Packed<StrikeElem> {
#[typst_macros::time(name = "strike", span = self.span())] #[typst_macros::time(name = "strike", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::s).with_body(Some(self.body.clone())).pack());
}
Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration {
// Note that we do not support evade option for strikethrough. // Note that we do not support evade option for strikethrough.
line: DecoLine::Strikethrough { line: DecoLine::Strikethrough {
@ -345,6 +369,12 @@ pub struct HighlightElem {
impl Show for Packed<HighlightElem> { impl Show for Packed<HighlightElem> {
#[typst_macros::time(name = "highlight", span = self.span())] #[typst_macros::time(name = "highlight", span = self.span())]
fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> { fn show(&self, _: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
if TargetElem::target_in(styles).is_html() {
return Ok(HtmlElem::new(tag::mark)
.with_body(Some(self.body.clone()))
.pack());
}
Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration { Ok(self.body.clone().styled(TextElem::set_deco(smallvec![Decoration {
line: DecoLine::Highlight { line: DecoLine::Highlight {
fill: self.fill(styles), fill: self.fill(styles),

View File

@ -14,13 +14,14 @@ macro_rules! translation {
}; };
} }
const TRANSLATIONS: [(&str, &str); 40] = [ const TRANSLATIONS: &[(&str, &str)] = &[
translation!("ar"), translation!("ar"),
translation!("bg"), translation!("bg"),
translation!("ca"), translation!("ca"),
translation!("cs"), translation!("cs"),
translation!("da"), translation!("da"),
translation!("de"), translation!("de"),
translation!("el"),
translation!("en"), translation!("en"),
translation!("es"), translation!("es"),
translation!("et"), translation!("et"),
@ -28,8 +29,8 @@ const TRANSLATIONS: [(&str, &str); 40] = [
translation!("fi"), translation!("fi"),
translation!("fr"), translation!("fr"),
translation!("gl"), translation!("gl"),
translation!("el"),
translation!("he"), translation!("he"),
translation!("hr"),
translation!("hu"), translation!("hu"),
translation!("id"), translation!("id"),
translation!("is"), translation!("is"),
@ -41,8 +42,8 @@ const TRANSLATIONS: [(&str, &str); 40] = [
translation!("nl"), translation!("nl"),
translation!("nn"), translation!("nn"),
translation!("pl"), translation!("pl"),
translation!("pt-PT"),
translation!("pt"), translation!("pt"),
translation!("pt-PT"),
translation!("ro"), translation!("ro"),
translation!("ru"), translation!("ru"),
translation!("sl"), translation!("sl"),
@ -53,8 +54,8 @@ const TRANSLATIONS: [(&str, &str); 40] = [
translation!("tr"), translation!("tr"),
translation!("uk"), translation!("uk"),
translation!("vi"), translation!("vi"),
translation!("zh-TW"),
translation!("zh"), translation!("zh"),
translation!("zh-TW"),
]; ];
/// An identifier for a natural language. /// An identifier for a natural language.
@ -312,14 +313,74 @@ fn lang_str(lang: Lang, region: Option<Region>) -> EcoString {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::collections::HashSet;
use std::path::PathBuf;
use typst_utils::option_eq; use typst_utils::option_eq;
use super::*; use super::*;
fn translation_files_iter() -> impl Iterator<Item = PathBuf> {
std::fs::read_dir("translations")
.unwrap()
.map(|e| e.unwrap().path())
.filter(|e| e.is_file() && e.extension().is_some_and(|e| e == "txt"))
}
#[test] #[test]
fn test_region_option_eq() { fn test_region_option_eq() {
let region = Some(Region([b'U', b'S'])); let region = Some(Region([b'U', b'S']));
assert!(option_eq(region, "US")); assert!(option_eq(region, "US"));
assert!(!option_eq(region, "AB")); assert!(!option_eq(region, "AB"));
} }
#[test]
fn test_all_translations_included() {
let defined_keys =
HashSet::<&str>::from_iter(TRANSLATIONS.iter().map(|(lang, _)| *lang));
let mut checked = 0;
for file in translation_files_iter() {
assert!(
defined_keys.contains(
file.file_stem()
.expect("translation file should have basename")
.to_str()
.expect("translation file name should be utf-8 encoded")
),
"translation from {:?} should be registered in TRANSLATIONS in {}",
file.file_name().unwrap(),
file!(),
);
checked += 1;
}
assert_eq!(TRANSLATIONS.len(), checked);
}
#[test]
fn test_all_translation_files_formatted() {
for file in translation_files_iter() {
let content = std::fs::read_to_string(&file)
.expect("translation file should be in utf-8 encoding");
let filename = file.file_name().unwrap();
assert!(
content.ends_with('\n'),
"translation file {filename:?} should end with linebreak",
);
for line in content.lines() {
assert_eq!(
line.trim(),
line,
"line {line:?} in {filename:?} should not have extra whitespaces"
);
}
}
}
#[test]
fn test_translations_sorted() {
assert!(
TRANSLATIONS.is_sorted_by_key(|(lang, _)| lang),
"TRANSLATIONS should be sorted"
);
}
} }

View File

@ -836,7 +836,7 @@ fn to_typst(synt::Color { r, g, b, a }: synt::Color) -> Color {
} }
fn to_syn(color: Color) -> synt::Color { fn to_syn(color: Color) -> synt::Color {
let [r, g, b, a] = color.to_rgb().to_vec4_u8(); let (r, g, b, a) = color.to_rgb().into_format::<u8, u8>().into_components();
synt::Color { r, g, b, a } synt::Color { r, g, b, a }
} }

View File

@ -262,7 +262,7 @@ impl Color {
color: Color, color: Color,
) -> SourceResult<Color> { ) -> SourceResult<Color> {
Ok(if let Some(color) = args.find::<Color>()? { Ok(if let Some(color) = args.find::<Color>()? {
color.to_luma() Color::Luma(color.to_luma())
} else { } else {
let Component(gray) = let Component(gray) =
args.expect("gray component").unwrap_or(Component(Ratio::one())); args.expect("gray component").unwrap_or(Component(Ratio::one()));
@ -318,7 +318,7 @@ impl Color {
color: Color, color: Color,
) -> SourceResult<Color> { ) -> SourceResult<Color> {
Ok(if let Some(color) = args.find::<Color>()? { Ok(if let Some(color) = args.find::<Color>()? {
color.to_oklab() Color::Oklab(color.to_oklab())
} else { } else {
let RatioComponent(l) = args.expect("lightness component")?; let RatioComponent(l) = args.expect("lightness component")?;
let ChromaComponent(a) = args.expect("A component")?; let ChromaComponent(a) = args.expect("A component")?;
@ -374,7 +374,7 @@ impl Color {
color: Color, color: Color,
) -> SourceResult<Color> { ) -> SourceResult<Color> {
Ok(if let Some(color) = args.find::<Color>()? { Ok(if let Some(color) = args.find::<Color>()? {
color.to_oklch() Color::Oklch(color.to_oklch())
} else { } else {
let RatioComponent(l) = args.expect("lightness component")?; let RatioComponent(l) = args.expect("lightness component")?;
let ChromaComponent(c) = args.expect("chroma component")?; let ChromaComponent(c) = args.expect("chroma component")?;
@ -434,7 +434,7 @@ impl Color {
color: Color, color: Color,
) -> SourceResult<Color> { ) -> SourceResult<Color> {
Ok(if let Some(color) = args.find::<Color>()? { Ok(if let Some(color) = args.find::<Color>()? {
color.to_linear_rgb() Color::LinearRgb(color.to_linear_rgb())
} else { } else {
let Component(r) = args.expect("red component")?; let Component(r) = args.expect("red component")?;
let Component(g) = args.expect("green component")?; let Component(g) = args.expect("green component")?;
@ -505,7 +505,7 @@ impl Color {
Ok(if let Some(string) = args.find::<Spanned<Str>>()? { Ok(if let Some(string) = args.find::<Spanned<Str>>()? {
Self::from_str(&string.v).at(string.span)? Self::from_str(&string.v).at(string.span)?
} else if let Some(color) = args.find::<Color>()? { } else if let Some(color) = args.find::<Color>()? {
color.to_rgb() Color::Rgb(color.to_rgb())
} else { } else {
let Component(r) = args.expect("red component")?; let Component(r) = args.expect("red component")?;
let Component(g) = args.expect("green component")?; let Component(g) = args.expect("green component")?;
@ -565,7 +565,7 @@ impl Color {
color: Color, color: Color,
) -> SourceResult<Color> { ) -> SourceResult<Color> {
Ok(if let Some(color) = args.find::<Color>()? { Ok(if let Some(color) = args.find::<Color>()? {
color.to_cmyk() Color::Cmyk(color.to_cmyk())
} else { } else {
let RatioComponent(c) = args.expect("cyan component")?; let RatioComponent(c) = args.expect("cyan component")?;
let RatioComponent(m) = args.expect("magenta component")?; let RatioComponent(m) = args.expect("magenta component")?;
@ -622,7 +622,7 @@ impl Color {
color: Color, color: Color,
) -> SourceResult<Color> { ) -> SourceResult<Color> {
Ok(if let Some(color) = args.find::<Color>()? { Ok(if let Some(color) = args.find::<Color>()? {
color.to_hsl() Color::Hsl(color.to_hsl())
} else { } else {
let h: Angle = args.expect("hue component")?; let h: Angle = args.expect("hue component")?;
let Component(s) = args.expect("saturation component")?; let Component(s) = args.expect("saturation component")?;
@ -679,7 +679,7 @@ impl Color {
color: Color, color: Color,
) -> SourceResult<Color> { ) -> SourceResult<Color> {
Ok(if let Some(color) = args.find::<Color>()? { Ok(if let Some(color) = args.find::<Color>()? {
color.to_hsv() Color::Hsv(color.to_hsv())
} else { } else {
let h: Angle = args.expect("hue component")?; let h: Angle = args.expect("hue component")?;
let Component(s) = args.expect("saturation component")?; let Component(s) = args.expect("saturation component")?;
@ -830,7 +830,7 @@ impl Color {
/// omitted if it is equal to `ff` (255 / 100%). /// omitted if it is equal to `ff` (255 / 100%).
#[func] #[func]
pub fn to_hex(self) -> EcoString { pub fn to_hex(self) -> EcoString {
let [r, g, b, a] = self.to_rgb().to_vec4_u8(); let (r, g, b, a) = self.to_rgb().into_format::<u8, u8>().into_components();
if a != 255 { if a != 255 {
eco_format!("#{:02x}{:02x}{:02x}{:02x}", r, g, b, a) eco_format!("#{:02x}{:02x}{:02x}{:02x}", r, g, b, a)
} else { } else {
@ -886,20 +886,21 @@ impl Color {
/// The factor to saturate the color by. /// The factor to saturate the color by.
factor: Ratio, factor: Ratio,
) -> SourceResult<Color> { ) -> SourceResult<Color> {
let f = factor.get() as f32;
Ok(match self { Ok(match self {
Self::Luma(_) => { Self::Luma(_) => bail!(
bail!(
span, "cannot saturate grayscale color"; span, "cannot saturate grayscale color";
hint: "try converting your color to RGB first" hint: "try converting your color to RGB first"
); ),
Self::Hsl(c) => Self::Hsl(c.saturate(f)),
Self::Hsv(c) => Self::Hsv(c.saturate(f)),
Self::Oklab(_)
| Self::Oklch(_)
| Self::LinearRgb(_)
| Self::Rgb(_)
| Self::Cmyk(_) => {
Color::Hsv(self.to_hsv().saturate(f)).to_space(self.space())
} }
Self::Oklab(_) => self.to_hsv().saturate(span, factor)?.to_oklab(),
Self::Oklch(_) => self.to_hsv().saturate(span, factor)?.to_oklch(),
Self::LinearRgb(_) => self.to_hsv().saturate(span, factor)?.to_linear_rgb(),
Self::Rgb(_) => self.to_hsv().saturate(span, factor)?.to_rgb(),
Self::Cmyk(_) => self.to_hsv().saturate(span, factor)?.to_cmyk(),
Self::Hsl(c) => Self::Hsl(c.saturate(factor.get() as f32)),
Self::Hsv(c) => Self::Hsv(c.saturate(factor.get() as f32)),
}) })
} }
@ -911,20 +912,21 @@ impl Color {
/// The factor to desaturate the color by. /// The factor to desaturate the color by.
factor: Ratio, factor: Ratio,
) -> SourceResult<Color> { ) -> SourceResult<Color> {
let f = factor.get() as f32;
Ok(match self { Ok(match self {
Self::Luma(_) => { Self::Luma(_) => bail!(
bail!(
span, "cannot desaturate grayscale color"; span, "cannot desaturate grayscale color";
hint: "try converting your color to RGB first" hint: "try converting your color to RGB first"
); ),
Self::Hsl(c) => Self::Hsl(c.desaturate(f)),
Self::Hsv(c) => Self::Hsv(c.desaturate(f)),
Self::Oklab(_)
| Self::Oklch(_)
| Self::LinearRgb(_)
| Self::Rgb(_)
| Self::Cmyk(_) => {
Color::Hsv(self.to_hsv().desaturate(f)).to_space(self.space())
} }
Self::Oklab(_) => self.to_hsv().desaturate(span, factor)?.to_oklab(),
Self::Oklch(_) => self.to_hsv().desaturate(span, factor)?.to_oklch(),
Self::LinearRgb(_) => self.to_hsv().desaturate(span, factor)?.to_linear_rgb(),
Self::Rgb(_) => self.to_hsv().desaturate(span, factor)?.to_rgb(),
Self::Cmyk(_) => self.to_hsv().desaturate(span, factor)?.to_cmyk(),
Self::Hsl(c) => Self::Hsl(c.desaturate(factor.get() as f32)),
Self::Hsv(c) => Self::Hsv(c.desaturate(factor.get() as f32)),
}) })
} }
@ -994,23 +996,17 @@ impl Color {
) -> SourceResult<Color> { ) -> SourceResult<Color> {
Ok(match space { Ok(match space {
ColorSpace::Oklch => { ColorSpace::Oklch => {
let Self::Oklch(oklch) = self.to_oklch() else { let oklch = self.to_oklch();
unreachable!();
};
let rotated = oklch.shift_hue(angle.to_deg() as f32); let rotated = oklch.shift_hue(angle.to_deg() as f32);
Self::Oklch(rotated).to_space(self.space()) Self::Oklch(rotated).to_space(self.space())
} }
ColorSpace::Hsl => { ColorSpace::Hsl => {
let Self::Hsl(hsl) = self.to_hsl() else { let hsl = self.to_hsl();
unreachable!();
};
let rotated = hsl.shift_hue(angle.to_deg() as f32); let rotated = hsl.shift_hue(angle.to_deg() as f32);
Self::Hsl(rotated).to_space(self.space()) Self::Hsl(rotated).to_space(self.space())
} }
ColorSpace::Hsv => { ColorSpace::Hsv => {
let Self::Hsv(hsv) = self.to_hsv() else { let hsv = self.to_hsv();
unreachable!();
};
let rotated = hsv.shift_hue(angle.to_deg() as f32); let rotated = hsv.shift_hue(angle.to_deg() as f32);
Self::Hsv(rotated).to_space(self.space()) Self::Hsv(rotated).to_space(self.space())
} }
@ -1281,19 +1277,19 @@ impl Color {
pub fn to_space(self, space: ColorSpace) -> Self { pub fn to_space(self, space: ColorSpace) -> Self {
match space { match space {
ColorSpace::Oklab => self.to_oklab(), ColorSpace::D65Gray => Self::Luma(self.to_luma()),
ColorSpace::Oklch => self.to_oklch(), ColorSpace::Oklab => Self::Oklab(self.to_oklab()),
ColorSpace::Srgb => self.to_rgb(), ColorSpace::Oklch => Self::Oklch(self.to_oklch()),
ColorSpace::LinearRgb => self.to_linear_rgb(), ColorSpace::Srgb => Self::Rgb(self.to_rgb()),
ColorSpace::Hsl => self.to_hsl(), ColorSpace::LinearRgb => Self::LinearRgb(self.to_linear_rgb()),
ColorSpace::Hsv => self.to_hsv(), ColorSpace::Cmyk => Self::Cmyk(self.to_cmyk()),
ColorSpace::Cmyk => self.to_cmyk(), ColorSpace::Hsl => Self::Hsl(self.to_hsl()),
ColorSpace::D65Gray => self.to_luma(), ColorSpace::Hsv => Self::Hsv(self.to_hsv()),
} }
} }
pub fn to_luma(self) -> Self { pub fn to_luma(self) -> Luma {
Self::Luma(match self { match self {
Self::Luma(c) => c, Self::Luma(c) => c,
Self::Oklab(c) => Luma::from_color(c), Self::Oklab(c) => Luma::from_color(c),
Self::Oklch(c) => Luma::from_color(c), Self::Oklch(c) => Luma::from_color(c),
@ -1302,11 +1298,11 @@ impl Color {
Self::Cmyk(c) => Luma::from_color(c.to_rgba()), Self::Cmyk(c) => Luma::from_color(c.to_rgba()),
Self::Hsl(c) => Luma::from_color(c), Self::Hsl(c) => Luma::from_color(c),
Self::Hsv(c) => Luma::from_color(c), Self::Hsv(c) => Luma::from_color(c),
}) }
} }
pub fn to_oklab(self) -> Self { pub fn to_oklab(self) -> Oklab {
Self::Oklab(match self { match self {
Self::Luma(c) => Oklab::from_color(c), Self::Luma(c) => Oklab::from_color(c),
Self::Oklab(c) => c, Self::Oklab(c) => c,
Self::Oklch(c) => Oklab::from_color(c), Self::Oklch(c) => Oklab::from_color(c),
@ -1315,11 +1311,11 @@ impl Color {
Self::Cmyk(c) => Oklab::from_color(c.to_rgba()), Self::Cmyk(c) => Oklab::from_color(c.to_rgba()),
Self::Hsl(c) => Oklab::from_color(c), Self::Hsl(c) => Oklab::from_color(c),
Self::Hsv(c) => Oklab::from_color(c), Self::Hsv(c) => Oklab::from_color(c),
}) }
} }
pub fn to_oklch(self) -> Self { pub fn to_oklch(self) -> Oklch {
Self::Oklch(match self { match self {
Self::Luma(c) => Oklch::from_color(c), Self::Luma(c) => Oklch::from_color(c),
Self::Oklab(c) => Oklch::from_color(c), Self::Oklab(c) => Oklch::from_color(c),
Self::Oklch(c) => c, Self::Oklch(c) => c,
@ -1328,11 +1324,11 @@ impl Color {
Self::Cmyk(c) => Oklch::from_color(c.to_rgba()), Self::Cmyk(c) => Oklch::from_color(c.to_rgba()),
Self::Hsl(c) => Oklch::from_color(c), Self::Hsl(c) => Oklch::from_color(c),
Self::Hsv(c) => Oklch::from_color(c), Self::Hsv(c) => Oklch::from_color(c),
}) }
} }
pub fn to_rgb(self) -> Self { pub fn to_rgb(self) -> Rgb {
Self::Rgb(match self { match self {
Self::Luma(c) => Rgb::from_color(c), Self::Luma(c) => Rgb::from_color(c),
Self::Oklab(c) => Rgb::from_color(c), Self::Oklab(c) => Rgb::from_color(c),
Self::Oklch(c) => Rgb::from_color(c), Self::Oklch(c) => Rgb::from_color(c),
@ -1341,11 +1337,11 @@ impl Color {
Self::Cmyk(c) => Rgb::from_color(c.to_rgba()), Self::Cmyk(c) => Rgb::from_color(c.to_rgba()),
Self::Hsl(c) => Rgb::from_color(c), Self::Hsl(c) => Rgb::from_color(c),
Self::Hsv(c) => Rgb::from_color(c), Self::Hsv(c) => Rgb::from_color(c),
}) }
} }
pub fn to_linear_rgb(self) -> Self { pub fn to_linear_rgb(self) -> LinearRgb {
Self::LinearRgb(match self { match self {
Self::Luma(c) => LinearRgb::from_color(c), Self::Luma(c) => LinearRgb::from_color(c),
Self::Oklab(c) => LinearRgb::from_color(c), Self::Oklab(c) => LinearRgb::from_color(c),
Self::Oklch(c) => LinearRgb::from_color(c), Self::Oklch(c) => LinearRgb::from_color(c),
@ -1354,11 +1350,11 @@ impl Color {
Self::Cmyk(c) => LinearRgb::from_color(c.to_rgba()), Self::Cmyk(c) => LinearRgb::from_color(c.to_rgba()),
Self::Hsl(c) => Rgb::from_color(c).into_linear(), Self::Hsl(c) => Rgb::from_color(c).into_linear(),
Self::Hsv(c) => Rgb::from_color(c).into_linear(), Self::Hsv(c) => Rgb::from_color(c).into_linear(),
}) }
} }
pub fn to_cmyk(self) -> Self { pub fn to_cmyk(self) -> Cmyk {
Self::Cmyk(match self { match self {
Self::Luma(c) => Cmyk::from_luma(c), Self::Luma(c) => Cmyk::from_luma(c),
Self::Oklab(c) => Cmyk::from_rgba(Rgb::from_color(c)), Self::Oklab(c) => Cmyk::from_rgba(Rgb::from_color(c)),
Self::Oklch(c) => Cmyk::from_rgba(Rgb::from_color(c)), Self::Oklch(c) => Cmyk::from_rgba(Rgb::from_color(c)),
@ -1367,11 +1363,11 @@ impl Color {
Self::Cmyk(c) => c, Self::Cmyk(c) => c,
Self::Hsl(c) => Cmyk::from_rgba(Rgb::from_color(c)), Self::Hsl(c) => Cmyk::from_rgba(Rgb::from_color(c)),
Self::Hsv(c) => Cmyk::from_rgba(Rgb::from_color(c)), Self::Hsv(c) => Cmyk::from_rgba(Rgb::from_color(c)),
}) }
} }
pub fn to_hsl(self) -> Self { pub fn to_hsl(self) -> Hsl {
Self::Hsl(match self { match self {
Self::Luma(c) => Hsl::from_color(c), Self::Luma(c) => Hsl::from_color(c),
Self::Oklab(c) => Hsl::from_color(c), Self::Oklab(c) => Hsl::from_color(c),
Self::Oklch(c) => Hsl::from_color(c), Self::Oklch(c) => Hsl::from_color(c),
@ -1380,11 +1376,11 @@ impl Color {
Self::Cmyk(c) => Hsl::from_color(c.to_rgba()), Self::Cmyk(c) => Hsl::from_color(c.to_rgba()),
Self::Hsl(c) => c, Self::Hsl(c) => c,
Self::Hsv(c) => Hsl::from_color(c), Self::Hsv(c) => Hsl::from_color(c),
}) }
} }
pub fn to_hsv(self) -> Self { pub fn to_hsv(self) -> Hsv {
Self::Hsv(match self { match self {
Self::Luma(c) => Hsv::from_color(c), Self::Luma(c) => Hsv::from_color(c),
Self::Oklab(c) => Hsv::from_color(c), Self::Oklab(c) => Hsv::from_color(c),
Self::Oklch(c) => Hsv::from_color(c), Self::Oklch(c) => Hsv::from_color(c),
@ -1393,7 +1389,7 @@ impl Color {
Self::Cmyk(c) => Hsv::from_color(c.to_rgba()), Self::Cmyk(c) => Hsv::from_color(c.to_rgba()),
Self::Hsl(c) => Hsv::from_color(c), Self::Hsl(c) => Hsv::from_color(c),
Self::Hsv(c) => c, Self::Hsv(c) => c,
}) }
} }
} }

View File

@ -1285,24 +1285,17 @@ fn process_stops(stops: &[Spanned<GradientStop>]) -> SourceResult<Vec<(Color, Ra
/// Sample the stops at a given position. /// Sample the stops at a given position.
fn sample_stops(stops: &[(Color, Ratio)], mixing_space: ColorSpace, t: f64) -> Color { fn sample_stops(stops: &[(Color, Ratio)], mixing_space: ColorSpace, t: f64) -> Color {
let t = t.clamp(0.0, 1.0); let t = t.clamp(0.0, 1.0);
let mut low = 0; let mut j = stops.partition_point(|(_, ratio)| ratio.get() < t);
let mut high = stops.len();
while low < high { if j == 0 {
let mid = (low + high) / 2; while stops.get(j + 1).is_some_and(|(_, r)| r.is_zero()) {
if stops[mid].1.get() < t { j += 1;
low = mid + 1;
} else {
high = mid;
} }
return stops[j].0;
} }
if low == 0 { let (col_0, pos_0) = stops[j - 1];
low = 1; let (col_1, pos_1) = stops[j];
}
let (col_0, pos_0) = stops[low - 1];
let (col_1, pos_1) = stops[low];
let t = (t - pos_0.get()) / (pos_1.get() - pos_0.get()); let t = (t - pos_0.get()) / (pos_1.get() - pos_0.get());
Color::mix_iter( Color::mix_iter(

View File

@ -0,0 +1,8 @@
figure = Slika
table = Tablica
equation = Jednadžba
bibliography = Literatura
heading = Odjeljak
outline = Sadržaj
raw = Kôd
page = str.

View File

@ -315,15 +315,15 @@ fn create_func_data(func: &Func) -> TokenStream {
quote! { quote! {
#foundations::NativeFuncData { #foundations::NativeFuncData {
function: #closure, function: #foundations::NativeFuncPtr(&#closure),
name: #name, name: #name,
title: #title, title: #title,
docs: #docs, docs: #docs,
keywords: &[#(#keywords),*], keywords: &[#(#keywords),*],
contextual: #contextual, contextual: #contextual,
scope: ::std::sync::LazyLock::new(|| #scope), scope: ::std::sync::LazyLock::new(&|| #scope),
params: ::std::sync::LazyLock::new(|| ::std::vec![#(#params),*]), params: ::std::sync::LazyLock::new(&|| ::std::vec![#(#params),*]),
returns: ::std::sync::LazyLock::new(|| <#returns as #foundations::Reflect>::output()), returns: ::std::sync::LazyLock::new(&|| <#returns as #foundations::Reflect>::output()),
} }
} }
} }

View File

@ -13,7 +13,7 @@ use krilla::surface::Surface;
use krilla::{Document, SerializeSettings}; use krilla::{Document, SerializeSettings};
use krilla_svg::render_svg_glyph; use krilla_svg::render_svg_glyph;
use typst_library::diag::{bail, error, SourceDiagnostic, SourceResult}; use typst_library::diag::{bail, error, SourceDiagnostic, SourceResult};
use typst_library::foundations::NativeElement; use typst_library::foundations::{NativeElement, Repr};
use typst_library::introspection::Location; use typst_library::introspection::Location;
use typst_library::layout::{ use typst_library::layout::{
Abs, Frame, FrameItem, GroupItem, PagedDocument, Size, Transform, Abs, Frame, FrameItem, GroupItem, PagedDocument, Size, Transform,
@ -429,14 +429,18 @@ fn convert_error(
display_font(gc.fonts_backward.get(f).unwrap()); display_font(gc.fonts_backward.get(f).unwrap());
hint: "try using a different font" hint: "try using a different font"
), ),
ValidationError::InvalidCodepointMapping(_, _, cp, loc) => { ValidationError::InvalidCodepointMapping(_, _, c, loc) => {
if let Some(c) = cp.map(|c| eco_format!("{:#06x}", c as u32)) { if let Some(c) = c {
let msg = if loc.is_some() { let msg = if loc.is_some() {
"the PDF contains text with" "the PDF contains text with"
} else { } else {
"the text contains" "the text contains"
}; };
error!(to_span(*loc), "{prefix} {msg} the disallowed codepoint {c}") error!(
to_span(*loc),
"{prefix} {msg} the disallowed codepoint `{}`",
c.repr()
)
} else { } else {
// I think this code path is in theory unreachable, // I think this code path is in theory unreachable,
// but just to be safe. // but just to be safe.
@ -454,13 +458,12 @@ fn convert_error(
} }
} }
ValidationError::UnicodePrivateArea(_, _, c, loc) => { ValidationError::UnicodePrivateArea(_, _, c, loc) => {
let code_point = eco_format!("{:#06x}", *c as u32);
let msg = if loc.is_some() { "the PDF" } else { "the text" }; let msg = if loc.is_some() { "the PDF" } else { "the text" };
error!( error!(
to_span(*loc), to_span(*loc),
"{prefix} {msg} contains the codepoint {code_point}"; "{prefix} {msg} contains the codepoint `{}`", c.repr();
hint: "codepoints from the Unicode private area are \ hint: "codepoints from the Unicode private area are \
forbidden in this export mode" forbidden in this export mode",
) )
} }
ValidationError::Transparency(loc) => { ValidationError::Transparency(loc) => {

View File

@ -34,8 +34,7 @@ pub(crate) fn embed_files(
}, },
}; };
let data: Arc<dyn AsRef<[u8]> + Send + Sync> = Arc::new(embed.data.clone()); let data: Arc<dyn AsRef<[u8]> + Send + Sync> = Arc::new(embed.data.clone());
// TODO: update when new krilla version lands (https://github.com/LaurenzV/krilla/pull/203) let compress = should_compress(&embed.data);
let compress = should_compress(&embed.data).unwrap_or(true);
let file = EmbeddedFile { let file = EmbeddedFile {
path, path,

View File

@ -18,7 +18,7 @@ use typst_library::foundations::{
SequenceElem, Show, ShowSet, Style, StyleChain, StyledElem, Styles, SymbolElem, SequenceElem, Show, ShowSet, Style, StyleChain, StyledElem, Styles, SymbolElem,
Synthesize, Transformation, Synthesize, Transformation,
}; };
use typst_library::html::{tag, HtmlElem}; use typst_library::html::{tag, FrameElem, HtmlElem};
use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem}; use typst_library::introspection::{Locatable, SplitLocator, Tag, TagElem};
use typst_library::layout::{ use typst_library::layout::{
AlignElem, BoxElem, HElem, InlineElem, PageElem, PagebreakElem, VElem, AlignElem, BoxElem, HElem, InlineElem, PageElem, PagebreakElem, VElem,
@ -237,9 +237,9 @@ fn visit<'a>(
return Ok(()); return Ok(());
} }
// Transformations for math content based on the realization kind. Needs // Transformations for content based on the realization kind. Needs
// to happen before show rules. // to happen before show rules.
if visit_math_rules(s, content, styles)? { if visit_kind_rules(s, content, styles)? {
return Ok(()); return Ok(());
} }
@ -280,9 +280,8 @@ fn visit<'a>(
Ok(()) Ok(())
} }
// Handles special cases for math in normal content and nested equations in // Handles transformations based on the realization kind.
// math. fn visit_kind_rules<'a>(
fn visit_math_rules<'a>(
s: &mut State<'a, '_, '_, '_>, s: &mut State<'a, '_, '_, '_>,
content: &'a Content, content: &'a Content,
styles: StyleChain<'a>, styles: StyleChain<'a>,
@ -335,6 +334,13 @@ fn visit_math_rules<'a>(
} }
} }
if !s.kind.is_html() {
if let Some(elem) = content.to_packed::<FrameElem>() {
visit(s, &elem.body, styles)?;
return Ok(true);
}
}
Ok(false) Ok(false)
} }

View File

@ -255,13 +255,13 @@ pub fn to_sk_paint<'a>(
} }
pub fn to_sk_color(color: Color) -> sk::Color { pub fn to_sk_color(color: Color) -> sk::Color {
let [r, g, b, a] = color.to_rgb().to_vec4(); let (r, g, b, a) = color.to_rgb().into_components();
sk::Color::from_rgba(r, g, b, a) sk::Color::from_rgba(r, g, b, a)
.expect("components must always be in the range [0..=1]") .expect("components must always be in the range [0..=1]")
} }
pub fn to_sk_color_u8(color: Color) -> sk::ColorU8 { pub fn to_sk_color_u8(color: Color) -> sk::ColorU8 {
let [r, g, b, a] = color.to_rgb().to_vec4_u8(); let (r, g, b, a) = color.to_rgb().into_format::<u8, u8>().into_components();
sk::ColorU8::from_rgba(r, g, b, a) sk::ColorU8::from_rgba(r, g, b, a)
} }

View File

@ -724,6 +724,8 @@ node! {
impl<'a> Ref<'a> { impl<'a> Ref<'a> {
/// Get the target. /// Get the target.
///
/// Will not be empty.
pub fn target(self) -> &'a str { pub fn target(self) -> &'a str {
self.0 self.0
.children() .children()

View File

@ -4,7 +4,7 @@ use unicode_script::{Script, UnicodeScript};
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use unscanny::Scanner; use unscanny::Scanner;
use crate::{SyntaxError, SyntaxKind, SyntaxNode}; use crate::{SyntaxError, SyntaxKind, SyntaxMode, SyntaxNode};
/// An iterator over a source code string which returns tokens. /// An iterator over a source code string which returns tokens.
#[derive(Clone)] #[derive(Clone)]
@ -13,28 +13,17 @@ pub(super) struct Lexer<'s> {
s: Scanner<'s>, s: Scanner<'s>,
/// The mode the lexer is in. This determines which kinds of tokens it /// The mode the lexer is in. This determines which kinds of tokens it
/// produces. /// produces.
mode: LexMode, mode: SyntaxMode,
/// Whether the last token contained a newline. /// Whether the last token contained a newline.
newline: bool, newline: bool,
/// An error for the last token. /// An error for the last token.
error: Option<SyntaxError>, error: Option<SyntaxError>,
} }
/// What kind of tokens to emit.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub(super) enum LexMode {
/// Text and markup.
Markup,
/// Math atoms, operators, etc.
Math,
/// Keywords, literals and operators.
Code,
}
impl<'s> Lexer<'s> { impl<'s> Lexer<'s> {
/// Create a new lexer with the given mode and a prefix to offset column /// Create a new lexer with the given mode and a prefix to offset column
/// calculations. /// calculations.
pub fn new(text: &'s str, mode: LexMode) -> Self { pub fn new(text: &'s str, mode: SyntaxMode) -> Self {
Self { Self {
s: Scanner::new(text), s: Scanner::new(text),
mode, mode,
@ -44,12 +33,12 @@ impl<'s> Lexer<'s> {
} }
/// Get the current lexing mode. /// Get the current lexing mode.
pub fn mode(&self) -> LexMode { pub fn mode(&self) -> SyntaxMode {
self.mode self.mode
} }
/// Change the lexing mode. /// Change the lexing mode.
pub fn set_mode(&mut self, mode: LexMode) { pub fn set_mode(&mut self, mode: SyntaxMode) {
self.mode = mode; self.mode = mode;
} }
@ -92,7 +81,7 @@ impl Lexer<'_> {
} }
} }
/// Shared methods with all [`LexMode`]. /// Shared methods with all [`SyntaxMode`].
impl Lexer<'_> { impl Lexer<'_> {
/// Return the next token in our text. Returns both the [`SyntaxNode`] /// Return the next token in our text. Returns both the [`SyntaxNode`]
/// and the raw [`SyntaxKind`] to make it more ergonomic to check the kind /// and the raw [`SyntaxKind`] to make it more ergonomic to check the kind
@ -114,14 +103,14 @@ impl Lexer<'_> {
); );
kind kind
} }
Some('`') if self.mode != LexMode::Math => return self.raw(), Some('`') if self.mode != SyntaxMode::Math => return self.raw(),
Some(c) => match self.mode { Some(c) => match self.mode {
LexMode::Markup => self.markup(start, c), SyntaxMode::Markup => self.markup(start, c),
LexMode::Math => match self.math(start, c) { SyntaxMode::Math => match self.math(start, c) {
(kind, None) => kind, (kind, None) => kind,
(kind, Some(node)) => return (kind, node), (kind, Some(node)) => return (kind, node),
}, },
LexMode::Code => self.code(start, c), SyntaxMode::Code => self.code(start, c),
}, },
None => SyntaxKind::End, None => SyntaxKind::End,
@ -145,7 +134,7 @@ impl Lexer<'_> {
}; };
self.newline = newlines > 0; self.newline = newlines > 0;
if self.mode == LexMode::Markup && newlines >= 2 { if self.mode == SyntaxMode::Markup && newlines >= 2 {
SyntaxKind::Parbreak SyntaxKind::Parbreak
} else { } else {
SyntaxKind::Space SyntaxKind::Space
@ -196,7 +185,7 @@ impl Lexer<'_> {
'h' if self.s.eat_if("ttp://") => self.link(), 'h' if self.s.eat_if("ttp://") => self.link(),
'h' if self.s.eat_if("ttps://") => self.link(), 'h' if self.s.eat_if("ttps://") => self.link(),
'<' if self.s.at(is_id_continue) => self.label(), '<' if self.s.at(is_id_continue) => self.label(),
'@' => self.ref_marker(), '@' if self.s.at(is_id_continue) => self.ref_marker(),
'.' if self.s.eat_if("..") => SyntaxKind::Shorthand, '.' if self.s.eat_if("..") => SyntaxKind::Shorthand,
'-' if self.s.eat_if("--") => SyntaxKind::Shorthand, '-' if self.s.eat_if("--") => SyntaxKind::Shorthand,
@ -965,9 +954,9 @@ impl ScannerExt for Scanner<'_> {
/// Whether a character will become a [`SyntaxKind::Space`] token. /// Whether a character will become a [`SyntaxKind::Space`] token.
#[inline] #[inline]
fn is_space(character: char, mode: LexMode) -> bool { fn is_space(character: char, mode: SyntaxMode) -> bool {
match mode { match mode {
LexMode::Markup => matches!(character, ' ' | '\t') || is_newline(character), SyntaxMode::Markup => matches!(character, ' ' | '\t') || is_newline(character),
_ => character.is_whitespace(), _ => character.is_whitespace(),
} }
} }

View File

@ -30,5 +30,16 @@ pub use self::path::VirtualPath;
pub use self::source::Source; pub use self::source::Source;
pub use self::span::{Span, Spanned}; pub use self::span::{Span, Spanned};
use self::lexer::{LexMode, Lexer}; use self::lexer::Lexer;
use self::parser::{reparse_block, reparse_markup}; use self::parser::{reparse_block, reparse_markup};
/// The syntax mode of a portion of Typst code.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum SyntaxMode {
/// Text and markup, as in the top level.
Markup,
/// Math atoms, operators, etc., as in equations.
Math,
/// Keywords, literals and operators, as after hashes.
Code,
}

View File

@ -7,12 +7,12 @@ use typst_utils::default_math_class;
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use crate::set::{syntax_set, SyntaxSet}; use crate::set::{syntax_set, SyntaxSet};
use crate::{ast, set, LexMode, Lexer, SyntaxError, SyntaxKind, SyntaxNode}; use crate::{ast, set, Lexer, SyntaxError, SyntaxKind, SyntaxMode, SyntaxNode};
/// Parses a source file as top-level markup. /// Parses a source file as top-level markup.
pub fn parse(text: &str) -> SyntaxNode { pub fn parse(text: &str) -> SyntaxNode {
let _scope = typst_timing::TimingScope::new("parse"); let _scope = typst_timing::TimingScope::new("parse");
let mut p = Parser::new(text, 0, LexMode::Markup); let mut p = Parser::new(text, 0, SyntaxMode::Markup);
markup_exprs(&mut p, true, syntax_set!(End)); markup_exprs(&mut p, true, syntax_set!(End));
p.finish_into(SyntaxKind::Markup) p.finish_into(SyntaxKind::Markup)
} }
@ -20,7 +20,7 @@ pub fn parse(text: &str) -> SyntaxNode {
/// Parses top-level code. /// Parses top-level code.
pub fn parse_code(text: &str) -> SyntaxNode { pub fn parse_code(text: &str) -> SyntaxNode {
let _scope = typst_timing::TimingScope::new("parse code"); let _scope = typst_timing::TimingScope::new("parse code");
let mut p = Parser::new(text, 0, LexMode::Code); let mut p = Parser::new(text, 0, SyntaxMode::Code);
code_exprs(&mut p, syntax_set!(End)); code_exprs(&mut p, syntax_set!(End));
p.finish_into(SyntaxKind::Code) p.finish_into(SyntaxKind::Code)
} }
@ -28,7 +28,7 @@ pub fn parse_code(text: &str) -> SyntaxNode {
/// Parses top-level math. /// Parses top-level math.
pub fn parse_math(text: &str) -> SyntaxNode { pub fn parse_math(text: &str) -> SyntaxNode {
let _scope = typst_timing::TimingScope::new("parse math"); let _scope = typst_timing::TimingScope::new("parse math");
let mut p = Parser::new(text, 0, LexMode::Math); let mut p = Parser::new(text, 0, SyntaxMode::Math);
math_exprs(&mut p, syntax_set!(End)); math_exprs(&mut p, syntax_set!(End));
p.finish_into(SyntaxKind::Math) p.finish_into(SyntaxKind::Math)
} }
@ -63,7 +63,7 @@ pub(super) fn reparse_markup(
nesting: &mut usize, nesting: &mut usize,
top_level: bool, top_level: bool,
) -> Option<Vec<SyntaxNode>> { ) -> Option<Vec<SyntaxNode>> {
let mut p = Parser::new(text, range.start, LexMode::Markup); let mut p = Parser::new(text, range.start, SyntaxMode::Markup);
*at_start |= p.had_newline(); *at_start |= p.had_newline();
while !p.end() && p.current_start() < range.end { while !p.end() && p.current_start() < range.end {
// If not top-level and at a new RightBracket, stop the reparse. // If not top-level and at a new RightBracket, stop the reparse.
@ -205,7 +205,7 @@ fn reference(p: &mut Parser) {
/// Parses a mathematical equation: `$x$`, `$ x^2 $`. /// Parses a mathematical equation: `$x$`, `$ x^2 $`.
fn equation(p: &mut Parser) { fn equation(p: &mut Parser) {
let m = p.marker(); let m = p.marker();
p.enter_modes(LexMode::Math, AtNewline::Continue, |p| { p.enter_modes(SyntaxMode::Math, AtNewline::Continue, |p| {
p.assert(SyntaxKind::Dollar); p.assert(SyntaxKind::Dollar);
math(p, syntax_set!(Dollar, End)); math(p, syntax_set!(Dollar, End));
p.expect_closing_delimiter(m, SyntaxKind::Dollar); p.expect_closing_delimiter(m, SyntaxKind::Dollar);
@ -615,7 +615,7 @@ fn code_exprs(p: &mut Parser, stop_set: SyntaxSet) {
/// Parses an atomic code expression embedded in markup or math. /// Parses an atomic code expression embedded in markup or math.
fn embedded_code_expr(p: &mut Parser) { fn embedded_code_expr(p: &mut Parser) {
p.enter_modes(LexMode::Code, AtNewline::Stop, |p| { p.enter_modes(SyntaxMode::Code, AtNewline::Stop, |p| {
p.assert(SyntaxKind::Hash); p.assert(SyntaxKind::Hash);
if p.had_trivia() || p.end() { if p.had_trivia() || p.end() {
p.expected("expression"); p.expected("expression");
@ -777,7 +777,7 @@ fn code_primary(p: &mut Parser, atomic: bool) {
/// Reparses a full content or code block. /// Reparses a full content or code block.
pub(super) fn reparse_block(text: &str, range: Range<usize>) -> Option<SyntaxNode> { pub(super) fn reparse_block(text: &str, range: Range<usize>) -> Option<SyntaxNode> {
let mut p = Parser::new(text, range.start, LexMode::Code); let mut p = Parser::new(text, range.start, SyntaxMode::Code);
assert!(p.at(SyntaxKind::LeftBracket) || p.at(SyntaxKind::LeftBrace)); assert!(p.at(SyntaxKind::LeftBracket) || p.at(SyntaxKind::LeftBrace));
block(&mut p); block(&mut p);
(p.balanced && p.prev_end() == range.end) (p.balanced && p.prev_end() == range.end)
@ -796,7 +796,7 @@ fn block(p: &mut Parser) {
/// Parses a code block: `{ let x = 1; x + 2 }`. /// Parses a code block: `{ let x = 1; x + 2 }`.
fn code_block(p: &mut Parser) { fn code_block(p: &mut Parser) {
let m = p.marker(); let m = p.marker();
p.enter_modes(LexMode::Code, AtNewline::Continue, |p| { p.enter_modes(SyntaxMode::Code, AtNewline::Continue, |p| {
p.assert(SyntaxKind::LeftBrace); p.assert(SyntaxKind::LeftBrace);
code(p, syntax_set!(RightBrace, RightBracket, RightParen, End)); code(p, syntax_set!(RightBrace, RightBracket, RightParen, End));
p.expect_closing_delimiter(m, SyntaxKind::RightBrace); p.expect_closing_delimiter(m, SyntaxKind::RightBrace);
@ -807,7 +807,7 @@ fn code_block(p: &mut Parser) {
/// Parses a content block: `[*Hi* there!]`. /// Parses a content block: `[*Hi* there!]`.
fn content_block(p: &mut Parser) { fn content_block(p: &mut Parser) {
let m = p.marker(); let m = p.marker();
p.enter_modes(LexMode::Markup, AtNewline::Continue, |p| { p.enter_modes(SyntaxMode::Markup, AtNewline::Continue, |p| {
p.assert(SyntaxKind::LeftBracket); p.assert(SyntaxKind::LeftBracket);
markup(p, true, true, syntax_set!(RightBracket, End)); markup(p, true, true, syntax_set!(RightBracket, End));
p.expect_closing_delimiter(m, SyntaxKind::RightBracket); p.expect_closing_delimiter(m, SyntaxKind::RightBracket);
@ -1516,10 +1516,10 @@ fn pattern_leaf<'s>(
/// ### Modes /// ### Modes
/// ///
/// The parser manages the transitions between the three modes of Typst through /// The parser manages the transitions between the three modes of Typst through
/// [lexer modes](`LexMode`) and [newline modes](`AtNewline`). /// [syntax modes](`SyntaxMode`) and [newline modes](`AtNewline`).
/// ///
/// The lexer modes map to the three Typst modes and are stored in the lexer, /// The syntax modes map to the three Typst modes and are stored in the lexer,
/// changing which`SyntaxKind`s it will generate. /// changing which `SyntaxKind`s it will generate.
/// ///
/// The newline mode is used to determine whether a newline should end the /// The newline mode is used to determine whether a newline should end the
/// current expression. If so, the parser temporarily changes `token`'s kind to /// current expression. If so, the parser temporarily changes `token`'s kind to
@ -1529,7 +1529,7 @@ struct Parser<'s> {
/// The source text shared with the lexer. /// The source text shared with the lexer.
text: &'s str, text: &'s str,
/// A lexer over the source text with multiple modes. Defines the boundaries /// A lexer over the source text with multiple modes. Defines the boundaries
/// of tokens and determines their [`SyntaxKind`]. Contains the [`LexMode`] /// of tokens and determines their [`SyntaxKind`]. Contains the [`SyntaxMode`]
/// defining our current Typst mode. /// defining our current Typst mode.
lexer: Lexer<'s>, lexer: Lexer<'s>,
/// The newline mode: whether to insert a temporary end at newlines. /// The newline mode: whether to insert a temporary end at newlines.
@ -1612,7 +1612,7 @@ impl AtNewline {
AtNewline::RequireColumn(min_col) => { AtNewline::RequireColumn(min_col) => {
// When the column is `None`, the newline doesn't start a // When the column is `None`, the newline doesn't start a
// column, and we continue parsing. This may happen on the // column, and we continue parsing. This may happen on the
// boundary of lexer modes, since we only report a column in // boundary of syntax modes, since we only report a column in
// Markup. // Markup.
column.is_some_and(|column| column <= min_col) column.is_some_and(|column| column <= min_col)
} }
@ -1643,8 +1643,8 @@ impl IndexMut<Marker> for Parser<'_> {
/// Creating/Consuming the parser and getting info about the current token. /// Creating/Consuming the parser and getting info about the current token.
impl<'s> Parser<'s> { impl<'s> Parser<'s> {
/// Create a new parser starting from the given text offset and lexer mode. /// Create a new parser starting from the given text offset and syntax mode.
fn new(text: &'s str, offset: usize, mode: LexMode) -> Self { fn new(text: &'s str, offset: usize, mode: SyntaxMode) -> Self {
let mut lexer = Lexer::new(text, mode); let mut lexer = Lexer::new(text, mode);
lexer.jump(offset); lexer.jump(offset);
let nl_mode = AtNewline::Continue; let nl_mode = AtNewline::Continue;
@ -1825,13 +1825,13 @@ impl<'s> Parser<'s> {
self.nodes.insert(from, SyntaxNode::inner(kind, children)); self.nodes.insert(from, SyntaxNode::inner(kind, children));
} }
/// Parse within the [`LexMode`] for subsequent tokens (does not change the /// Parse within the [`SyntaxMode`] for subsequent tokens (does not change the
/// current token). This may re-lex the final token on exit. /// current token). This may re-lex the final token on exit.
/// ///
/// This function effectively repurposes the call stack as a stack of modes. /// This function effectively repurposes the call stack as a stack of modes.
fn enter_modes( fn enter_modes(
&mut self, &mut self,
mode: LexMode, mode: SyntaxMode,
stop: AtNewline, stop: AtNewline,
func: impl FnOnce(&mut Parser<'s>), func: impl FnOnce(&mut Parser<'s>),
) { ) {
@ -1891,7 +1891,8 @@ impl<'s> Parser<'s> {
} }
let newline = if had_newline { let newline = if had_newline {
let column = (lexer.mode() == LexMode::Markup).then(|| lexer.column(start)); let column =
(lexer.mode() == SyntaxMode::Markup).then(|| lexer.column(start));
let newline = Newline { column, parbreak }; let newline = Newline { column, parbreak };
if nl_mode.stop_at(newline, kind) { if nl_mode.stop_at(newline, kind) {
// Insert a temporary `SyntaxKind::End` to halt the parser. // Insert a temporary `SyntaxKind::End` to halt the parser.
@ -1938,7 +1939,7 @@ struct Checkpoint {
#[derive(Clone)] #[derive(Clone)]
struct PartialState { struct PartialState {
cursor: usize, cursor: usize,
lex_mode: LexMode, lex_mode: SyntaxMode,
token: Token, token: Token,
} }

View File

@ -23,7 +23,7 @@ pub use self::scalar::Scalar;
#[doc(hidden)] #[doc(hidden)]
pub use once_cell; pub use once_cell;
use std::fmt::{Debug, Formatter}; use std::fmt::{Debug, Display, Formatter};
use std::hash::Hash; use std::hash::Hash;
use std::iter::{Chain, Flatten, Rev}; use std::iter::{Chain, Flatten, Rev};
use std::num::{NonZeroU32, NonZeroUsize}; use std::num::{NonZeroU32, NonZeroUsize};
@ -52,6 +52,25 @@ where
Wrapper(f) Wrapper(f)
} }
/// Turn a closure into a struct implementing [`Display`].
pub fn display<F>(f: F) -> impl Display
where
F: Fn(&mut Formatter) -> std::fmt::Result,
{
struct Wrapper<F>(F);
impl<F> Display for Wrapper<F>
where
F: Fn(&mut Formatter) -> std::fmt::Result,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.0(f)
}
}
Wrapper(f)
}
/// Calculate a 128-bit siphash of a value. /// Calculate a 128-bit siphash of a value.
pub fn hash128<T: Hash + ?Sized>(value: &T) -> u128 { pub fn hash128<T: Hash + ?Sized>(value: &T) -> u128 {
let mut state = SipHasher13::new(); let mut state = SipHasher13::new();

View File

@ -72,7 +72,7 @@ impl PicoStr {
pub const fn constant(string: &'static str) -> PicoStr { pub const fn constant(string: &'static str) -> PicoStr {
match PicoStr::try_constant(string) { match PicoStr::try_constant(string) {
Ok(value) => value, Ok(value) => value,
Err(err) => panic!("{}", err.message()), Err(err) => failed_to_compile_time_intern(err, string),
} }
} }
@ -190,15 +190,9 @@ mod bitcode {
impl EncodingError { impl EncodingError {
pub const fn message(&self) -> &'static str { pub const fn message(&self) -> &'static str {
match self { match self {
Self::TooLong => { Self::TooLong => "the maximum auto-internible string length is 12",
"the maximum auto-internible string length is 12. \
you can add an exception to typst-utils/src/pico.rs \
to intern longer strings."
}
Self::BadChar => { Self::BadChar => {
"can only auto-intern the chars 'a'-'z', '1'-'4', and '-'. \ "can only auto-intern the chars 'a'-'z', '1'-'4', and '-'"
you can add an exception to typst-utils/src/pico.rs \
to intern other strings."
} }
} }
} }
@ -210,18 +204,70 @@ mod exceptions {
use std::cmp::Ordering; use std::cmp::Ordering;
/// A global list of non-bitcode-encodable compile-time internible strings. /// A global list of non-bitcode-encodable compile-time internible strings.
///
/// Must be sorted.
pub const LIST: &[&str] = &[ pub const LIST: &[&str] = &[
"accept-charset",
"allowfullscreen",
"aria-activedescendant",
"aria-autocomplete",
"aria-colcount",
"aria-colindex",
"aria-controls",
"aria-describedby",
"aria-disabled",
"aria-dropeffect",
"aria-errormessage",
"aria-expanded",
"aria-haspopup",
"aria-keyshortcuts",
"aria-labelledby",
"aria-multiline",
"aria-multiselectable",
"aria-orientation",
"aria-placeholder",
"aria-posinset",
"aria-readonly",
"aria-relevant",
"aria-required",
"aria-roledescription",
"aria-rowcount",
"aria-rowindex",
"aria-selected",
"aria-valuemax",
"aria-valuemin",
"aria-valuenow",
"aria-valuetext",
"autocapitalize",
"cjk-latin-spacing", "cjk-latin-spacing",
"contenteditable",
"discretionary-ligatures", "discretionary-ligatures",
"fetchpriority",
"formnovalidate",
"h5", "h5",
"h6", "h6",
"historical-ligatures", "historical-ligatures",
"number-clearance", "number-clearance",
"number-margin", "number-margin",
"numbering-scope", "numbering-scope",
"onbeforeprint",
"onbeforeunload",
"onlanguagechange",
"onmessageerror",
"onrejectionhandled",
"onunhandledrejection",
"page-numbering", "page-numbering",
"par-line-marker", "par-line-marker",
"popovertarget",
"popovertargetaction",
"referrerpolicy",
"shadowrootclonable",
"shadowrootcustomelementregistry",
"shadowrootdelegatesfocus",
"shadowrootmode",
"shadowrootserializable",
"transparentize", "transparentize",
"writingsuggestions",
]; ];
/// Try to find the index of an exception if it exists. /// Try to find the index of an exception if it exists.
@ -356,6 +402,39 @@ impl Hash for ResolvedPicoStr {
} }
} }
/// The error when a string could not be interned at compile time. Because the
/// normal formatting machinery is not available at compile time, just producing
/// the message is a bit involved ...
#[track_caller]
const fn failed_to_compile_time_intern(
error: bitcode::EncodingError,
string: &'static str,
) -> ! {
const CAPACITY: usize = 512;
const fn push((buf, i): &mut ([u8; CAPACITY], usize), s: &str) {
let mut k = 0;
while k < s.len() && *i < buf.len() {
buf[*i] = s.as_bytes()[k];
k += 1;
*i += 1;
}
}
let mut dest = ([0; CAPACITY], 0);
push(&mut dest, "failed to compile-time intern string \"");
push(&mut dest, string);
push(&mut dest, "\". ");
push(&mut dest, error.message());
push(&mut dest, ". you can add an exception to ");
push(&mut dest, file!());
push(&mut dest, " to intern longer strings.");
let (slice, _) = dest.0.split_at(dest.1);
let Ok(message) = std::str::from_utf8(slice) else { panic!() };
panic!("{}", message);
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -21,7 +21,7 @@ description: Changes in Typst 0.9.0
- Added [`full`]($bibliography.full) argument to bibliography function to print - Added [`full`]($bibliography.full) argument to bibliography function to print
the full bibliography even if not all works were cited the full bibliography even if not all works were cited
- Bibliography entries can now contain Typst equations (wrapped in `[$..$]` just - Bibliography entries can now contain Typst equations (wrapped in `[$..$]` just
like in markup), this works both for `.yml` and `.bib` bibliographies like in markup), this works both for `.yaml` and `.bib` bibliographies
- The hayagriva YAML format was improved. See its - The hayagriva YAML format was improved. See its
[changelog](https://github.com/typst/hayagriva/blob/main/CHANGELOG.md) for [changelog](https://github.com/typst/hayagriva/blob/main/CHANGELOG.md) for
more details. **(Breaking change)** more details. **(Breaking change)**

View File

@ -256,8 +256,8 @@ In Typst, the same function can be used both to affect the appearance for the
remainder of the document, a block (or scope), or just its arguments. For remainder of the document, a block (or scope), or just its arguments. For
example, `[#text(weight: "bold")[bold text]]` will only embolden its argument, example, `[#text(weight: "bold")[bold text]]` will only embolden its argument,
while `[#set text(weight: "bold")]` will embolden any text until the end of the while `[#set text(weight: "bold")]` will embolden any text until the end of the
current block, or, if there is none, document. The effects of a function are current block, or the end of the document, if there is none. The effects of a
immediately obvious based on whether it is used in a call or a function are immediately obvious based on whether it is used in a call or a
[set rule.]($styling/#set-rules) [set rule.]($styling/#set-rules)
```example ```example

View File

@ -206,7 +206,6 @@ label exists on the current page:
```typ ```typ
>>> #set page("a5", margin: (x: 2.5cm, y: 3cm)) >>> #set page("a5", margin: (x: 2.5cm, y: 3cm))
#set page(header: context { #set page(header: context {
let page-counter =
let matches = query(<big-table>) let matches = query(<big-table>)
let current = counter(page).get() let current = counter(page).get()
let has-table = matches.any(m => let has-table = matches.any(m =>
@ -218,7 +217,7 @@ label exists on the current page:
#h(1fr) #h(1fr)
National Academy of Sciences National Academy of Sciences
] ]
})) })
#lorem(100) #lorem(100)
#pagebreak() #pagebreak()

View File

@ -137,6 +137,55 @@
In addition to the functions listed below, the `calc` module also defines In addition to the functions listed below, the `calc` module also defines
the constants `pi`, `tau`, `e`, and `inf`. the constants `pi`, `tau`, `e`, and `inf`.
- name: std
title: Standard library
category: foundations
path: ["std"]
details: |
A module that contains all globally accessible items.
# Using "shadowed" definitions
The `std` module is useful whenever you overrode a name from the global
scope (this is called _shadowing_). For instance, you might have used the
name `text` for a parameter. To still access the `text` element, write
`std.text`.
```example
>>> #set page(margin: (left: 3em))
#let par = [My special paragraph.]
#let special(text) = {
set std.text(style: "italic")
set std.par.line(numbering: "1")
text
}
#special(par)
#lorem(10)
```
# Conditional access
You can also use this in combination with the [dictionary
constructor]($dictionary) to conditionally access global definitions. This
can, for instance, be useful to use new or experimental functionality when
it is available, while falling back to an alternative implementation if
used on an older Typst version. In particular, this allows us to create
[polyfills](https://en.wikipedia.org/wiki/Polyfill_(programming)).
This can be as simple as creating an alias to prevent warning messages, for
example, conditionally using `pattern` in Typst version 0.12, but using
[`tiling`] in newer versions. Since the parameters accepted by the `tiling`
function match those of the older `pattern` function, using the `tiling`
function when available and falling back to `pattern` otherwise will unify
the usage across all versions. Note that, when creating a polyfill,
[`sys.version`]($category/foundations/sys) can also be very useful.
```typ
#let tiling = if "tiling" in std { tiling } else { pattern }
...
```
- name: sys - name: sys
title: System title: System
category: foundations category: foundations

View File

@ -37,7 +37,7 @@ static GROUPS: LazyLock<Vec<GroupData>> = LazyLock::new(|| {
let mut groups: Vec<GroupData> = let mut groups: Vec<GroupData> =
yaml::from_str(load!("reference/groups.yml")).unwrap(); yaml::from_str(load!("reference/groups.yml")).unwrap();
for group in &mut groups { for group in &mut groups {
if group.filter.is_empty() { if group.filter.is_empty() && group.name != "std" {
group.filter = group group.filter = group
.module() .module()
.scope() .scope()
@ -720,18 +720,12 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel {
} }
}; };
for (variant, c) in symbol.variants() { for (variant, c, deprecation) in symbol.variants() {
let shorthand = |list: &[(&'static str, char)]| { let shorthand = |list: &[(&'static str, char)]| {
list.iter().copied().find(|&(_, x)| x == c).map(|(s, _)| s) list.iter().copied().find(|&(_, x)| x == c).map(|(s, _)| s)
}; };
let name = complete(variant); let name = complete(variant);
let deprecation = match name.as_str() {
"integral.sect" => {
Some("`integral.sect` is deprecated, use `integral.inter` instead")
}
_ => binding.deprecation(),
};
list.push(SymbolModel { list.push(SymbolModel {
name, name,
@ -742,10 +736,10 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel {
accent: typst::math::Accent::combine(c).is_some(), accent: typst::math::Accent::combine(c).is_some(),
alternates: symbol alternates: symbol
.variants() .variants()
.filter(|(other, _)| other != &variant) .filter(|(other, _, _)| other != &variant)
.map(|(other, _)| complete(other)) .map(|(other, _, _)| complete(other))
.collect(), .collect(),
deprecation, deprecation: deprecation.or_else(|| binding.deprecation()),
}); });
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 910 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 970 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<p><s>Struck</s> <mark>Highlighted</mark> <span style="text-decoration: underline">Underlined</span> <span style="text-decoration: overline">Overlined</span></p>
<p><span style="text-decoration: overline"><span style="text-decoration: underline"><mark><s>Mixed</s></mark></span></span></p>
</body>
</html>

View File

@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body><textarea>hello &lt;/textarea></textarea></body>
</html>

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<pre>hello</pre>
<pre>
hello</pre>
<pre>
hello</pre>
</body>
</html>

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<script>
const x = 1
const y = 2
console.log(x < y, Math.max(1, 2))
</script>
<script>
console.log(`Hello
World`)
</script>
<script type="text/python">x = 1
y = 2
print(x < y, max(x, y))</script>
</body>
</html>

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<style>
body {
text: red;
}
</style>
</body>
</html>

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body><textarea>
enter</textarea></body>
</html>

View File

@ -0,0 +1,63 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id="hi"></div>
<div aria-autocomplete="none"></div>
<div aria-expanded="undefined"></div>
<link referrerpolicy>
<div></div>
<div autofocus></div>
<div></div>
<div hidden></div>
<div aria-atomic="false"></div>
<div aria-atomic="true"></div>
<div translate="no"></div>
<div translate="yes"></div>
<form autocomplete="off"></form>
<form autocomplete="on"></form>
<div accesskey="K"></div>
<div aria-colcount="2"></div>
<object width="120" height="10"></object>
<td rowspan="2"></td>
<meter low="3.4" high="7.9"></meter>
<div class="alpha"></div>
<div class="alpha beta"></div>
<div class="alpha beta"></div>
<div><input accept="image/jpeg"></div>
<div><input accept="image/jpeg, image/png"></div>
<div><input accept="image/jpeg, image/png"></div>
<area coords="2.3, 4, 5.6">
<link color="#ff4136">
<link color="rgb(100% 32.94% 29.06%)">
<link color="rgb(50% 50% 50%)">
<link color="#958677">
<link color="oklab(27% 0.08 -0.012 / 50%)">
<link color="color(srgb-linear 20% 30% 40% / 50%)">
<link color="hsl(20deg 10% 20%)">
<link color="hsl(30deg 11.11% 27%)">
<div><time datetime="3w 4s"></time></div>
<div><time datetime="1d 4m"></time></div>
<div><time datetime="0s"></time></div>
<div><time datetime="2005-07-10"></time></div>
<div><time datetime="0000-02-01"></time></div>
<div><time datetime="06:30"></time></div>
<div><time datetime="0000-02-01T11:11"></time></div>
<div><time datetime="0000-02-01T06:00:09"></time></div>
<div dir="ltr">RTL</div>
<img src="image.png" alt="My wonderful image" srcset="/image-120px.png 120w, /image-60px.png 60w" sizes="(min-width: 800px) 400pt, (min-width: 400px) 250pt">
<form enctype="text/plain"></form>
<form role="complementary"></form>
<div hidden="until-found"></div>
<div aria-checked="false"></div>
<div aria-checked="true"></div>
<div aria-checked="mixed"></div>
<div><input value="5.6"></div>
<div><input value="#ff4136"></div>
<div><input min="3" max="9"></div>
<link rel="icon" sizes="32x24 64x48">
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

View File

@ -4,7 +4,7 @@ use std::path::PathBuf;
use ecow::eco_vec; use ecow::eco_vec;
use tiny_skia as sk; use tiny_skia as sk;
use typst::diag::{SourceDiagnostic, Warned}; use typst::diag::{SourceDiagnostic, SourceResult, Warned};
use typst::html::HtmlDocument; use typst::html::HtmlDocument;
use typst::layout::{Abs, Frame, FrameItem, PagedDocument, Transform}; use typst::layout::{Abs, Frame, FrameItem, PagedDocument, Transform};
use typst::visualize::Color; use typst::visualize::Color;
@ -82,17 +82,26 @@ impl<'a> Runner<'a> {
/// Run test specific to document format. /// Run test specific to document format.
fn run_test<D: OutputType>(&mut self) { fn run_test<D: OutputType>(&mut self) {
let Warned { output, warnings } = typst::compile(&self.world); let Warned { output, warnings } = typst::compile(&self.world);
let (doc, errors) = match output { let (doc, mut errors) = match output {
Ok(doc) => (Some(doc), eco_vec![]), Ok(doc) => (Some(doc), eco_vec![]),
Err(errors) => (None, errors), Err(errors) => (None, errors),
}; };
if doc.is_none() && errors.is_empty() { D::check_custom(self, doc.as_ref());
let output = doc.and_then(|doc: D| match doc.make_live() {
Ok(live) => Some((doc, live)),
Err(list) => {
errors.extend(list);
None
}
});
if output.is_none() && errors.is_empty() {
log!(self, "no document, but also no errors"); log!(self, "no document, but also no errors");
} }
D::check_custom(self, doc.as_ref()); self.check_output(output);
self.check_output(doc.as_ref());
for error in &errors { for error in &errors {
self.check_diagnostic(NoteKind::Error, error); self.check_diagnostic(NoteKind::Error, error);
@ -128,12 +137,12 @@ impl<'a> Runner<'a> {
} }
/// Check that the document output is correct. /// Check that the document output is correct.
fn check_output<D: OutputType>(&mut self, document: Option<&D>) { fn check_output<D: OutputType>(&mut self, output: Option<(D, D::Live)>) {
let live_path = D::live_path(&self.test.name); let live_path = D::live_path(&self.test.name);
let ref_path = D::ref_path(&self.test.name); let ref_path = D::ref_path(&self.test.name);
let ref_data = std::fs::read(&ref_path); let ref_data = std::fs::read(&ref_path);
let Some(document) = document else { let Some((document, live)) = output else {
if ref_data.is_ok() { if ref_data.is_ok() {
log!(self, "missing document"); log!(self, "missing document");
log!(self, " ref | {}", ref_path.display()); log!(self, " ref | {}", ref_path.display());
@ -141,7 +150,7 @@ impl<'a> Runner<'a> {
return; return;
}; };
let skippable = match D::is_skippable(document) { let skippable = match D::is_skippable(&document) {
Ok(skippable) => skippable, Ok(skippable) => skippable,
Err(()) => { Err(()) => {
log!(self, "document has zero pages"); log!(self, "document has zero pages");
@ -157,7 +166,6 @@ impl<'a> Runner<'a> {
} }
// Render and save live version. // Render and save live version.
let live = document.make_live();
document.save_live(&self.test.name, &live); document.save_live(&self.test.name, &live);
// Compare against reference output if available. // Compare against reference output if available.
@ -214,9 +222,13 @@ impl<'a> Runner<'a> {
return; return;
} }
let message = diag.message.replace("\\", "/"); let message = if diag.message.contains("\\u{") {
&diag.message
} else {
&diag.message.replace("\\", "/")
};
let range = self.world.range(diag.span); let range = self.world.range(diag.span);
self.validate_note(kind, diag.span.id(), range.clone(), &message); self.validate_note(kind, diag.span.id(), range.clone(), message);
// Check hints. // Check hints.
for hint in &diag.hints { for hint in &diag.hints {
@ -359,7 +371,7 @@ trait OutputType: Document {
} }
/// Produces the live output. /// Produces the live output.
fn make_live(&self) -> Self::Live; fn make_live(&self) -> SourceResult<Self::Live>;
/// Saves the live output. /// Saves the live output.
fn save_live(&self, name: &str, live: &Self::Live); fn save_live(&self, name: &str, live: &Self::Live);
@ -406,8 +418,8 @@ impl OutputType for PagedDocument {
} }
} }
fn make_live(&self) -> Self::Live { fn make_live(&self) -> SourceResult<Self::Live> {
render(self, 1.0) Ok(render(self, 1.0))
} }
fn save_live(&self, name: &str, live: &Self::Live) { fn save_live(&self, name: &str, live: &Self::Live) {
@ -471,9 +483,8 @@ impl OutputType for HtmlDocument {
format!("{}/html/{}.html", crate::REF_PATH, name).into() format!("{}/html/{}.html", crate::REF_PATH, name).into()
} }
fn make_live(&self) -> Self::Live { fn make_live(&self) -> SourceResult<Self::Live> {
// TODO: Do this earlier to be able to process export errors. typst_html::html(self)
typst_html::html(self).unwrap()
} }
fn save_live(&self, name: &str, live: &Self::Live) { fn save_live(&self, name: &str, live: &Self::Live) {

View File

@ -92,3 +92,7 @@ _Visible_
--- label-non-existent-error --- --- label-non-existent-error ---
// Error: 5-10 sequence does not have field "label" // Error: 5-10 sequence does not have field "label"
#[].label #[].label
--- label-empty ---
// Error: 23-32 label name must not be empty
= Something to label #label("")

View File

@ -0,0 +1,8 @@
// No proper HTML tests here yet because we don't want to test SVG export just
// yet. We'll definitely add tests at some point.
--- html-frame-in-layout ---
// Ensure that HTML frames are transparent in layout. This is less important for
// actual paged export than for _nested_ HTML frames, which take the same code
// path.
#html.frame[A]

Some files were not shown because too many files have changed in this diff Show More