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
132 changed files with 2904 additions and 698 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,54 +70,37 @@ 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());
w.buf.push('=');
w.buf.push('"'); // If the string is empty, we can use shorthand syntax.
for c in value.chars() { // `<elem attr="">..</div` is equivalent to `<elem attr>..</div>`
if charsets::is_valid_in_attribute_value(c) { if !value.is_empty() {
w.buf.push(c); w.buf.push('=');
} else { w.buf.push('"');
write_escape(w, c).at(element.span)?; for c in value.chars() {
if charsets::is_valid_in_attribute_value(c) {
w.buf.push(c);
} else {
write_escape(w, c).at(element.span)?;
}
} }
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(());
} }
let pretty = w.pretty; if tag::is_raw(element.tag) {
if !element.children.is_empty() { write_raw(w, element)?;
let pretty_inside = allows_pretty_inside(element.tag) } else if !element.children.is_empty() {
&& element.children.iter().any(|node| match node { write_children(w, element)?;
HtmlNode::Element(child) => wants_pretty_around(child.tag),
_ => false,
});
w.pretty &= pretty_inside;
let mut indent = w.pretty;
w.level += 1;
for c in &element.children {
let pretty_around = match c {
HtmlNode::Tag(_) => continue,
HtmlNode::Element(child) => w.pretty && wants_pretty_around(child.tag),
HtmlNode::Text(..) | HtmlNode::Frame(_) => false,
};
if core::mem::take(&mut indent) || pretty_around {
write_indent(w);
}
write_node(w, c)?;
indent = pretty_around;
}
w.level -= 1;
write_indent(w);
} }
w.pretty = pretty;
w.buf.push_str("</"); w.buf.push_str("</");
w.buf.push_str(&element.tag.resolve()); w.buf.push_str(&element.tag.resolve());
@ -125,6 +109,159 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
Ok(()) 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_inside = allows_pretty_inside(element.tag)
&& element.children.iter().any(|node| match node {
HtmlNode::Element(child) => wants_pretty_around(child.tag),
_ => false,
});
w.pretty &= pretty_inside;
let mut indent = w.pretty;
w.level += 1;
for c in &element.children {
let pretty_around = match c {
HtmlNode::Tag(_) => continue,
HtmlNode::Element(child) => w.pretty && wants_pretty_around(child.tag),
HtmlNode::Text(..) | HtmlNode::Frame(_) => false,
};
if core::mem::take(&mut indent) || pretty_around {
write_indent(w);
}
write_node(w, c)?;
indent = pretty_around;
}
w.level -= 1;
write_indent(w);
w.pretty = pretty;
Ok(())
}
/// 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(())
}
/// 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

@ -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

@ -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.
/// // Override equation references. /// if el == none or el.func() != eq { return it }
/// link(el.location(),numbering( /// // Override equation references.
/// el.numbering, /// link(el.location(), numbering(
/// ..counter(eq).at(el.location()) /// el.numbering,
/// )) /// ..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

@ -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

@ -5,4 +5,4 @@ bibliography = المراجع
heading = الفصل heading = الفصل
outline = المحتويات outline = المحتويات
raw = قائمة raw = قائمة
page = صفحة page = صفحة

View File

@ -5,4 +5,4 @@ bibliography = Библиография
heading = Раздел heading = Раздел
outline = Съдържание outline = Съдържание
raw = Приложение raw = Приложение
page = стр. page = стр.

View File

@ -5,4 +5,4 @@ bibliography = Bibliografia
heading = Secció heading = Secció
outline = Índex outline = Índex
raw = Llistat raw = Llistat
page = pàgina page = pàgina

View File

@ -5,4 +5,4 @@ bibliography = Bibliografie
heading = Kapitola heading = Kapitola
outline = Obsah outline = Obsah
raw = Výpis raw = Výpis
page = strana page = strana

View File

@ -5,4 +5,4 @@ bibliography = Bibliografi
heading = Afsnit heading = Afsnit
outline = Indhold outline = Indhold
raw = Liste raw = Liste
page = side page = side

View File

@ -5,4 +5,4 @@ bibliography = Bibliographie
heading = Abschnitt heading = Abschnitt
outline = Inhaltsverzeichnis outline = Inhaltsverzeichnis
raw = Listing raw = Listing
page = Seite page = Seite

View File

@ -4,4 +4,4 @@ equation = Εξίσωση
bibliography = Βιβλιογραφία bibliography = Βιβλιογραφία
heading = Κεφάλαιο heading = Κεφάλαιο
outline = Περιεχόμενα outline = Περιεχόμενα
raw = Παράθεση raw = Παράθεση

View File

@ -5,4 +5,4 @@ bibliography = Bibliography
heading = Section heading = Section
outline = Contents outline = Contents
raw = Listing raw = Listing
page = page page = page

View File

@ -5,4 +5,4 @@ bibliography = Bibliografía
heading = Sección heading = Sección
outline = Índice outline = Índice
raw = Listado raw = Listado
page = página page = página

View File

@ -5,4 +5,4 @@ bibliography = Viited
heading = Peatükk heading = Peatükk
outline = Sisukord outline = Sisukord
raw = List raw = List
page = lk. page = lk.

View File

@ -5,4 +5,4 @@ bibliography = Viitteet
heading = Osio heading = Osio
outline = Sisällys outline = Sisällys
raw = Esimerkki raw = Esimerkki
page = sivu page = sivu

View File

@ -5,4 +5,4 @@ bibliography = Bibliographie
heading = Chapitre heading = Chapitre
outline = Table des matières outline = Table des matières
raw = Liste raw = Liste
page = page page = page

View File

@ -5,4 +5,4 @@ bibliography = Bibliografía
heading = Sección heading = Sección
outline = Índice outline = Índice
raw = Listado raw = Listado
page = páxina page = páxina

View File

@ -5,4 +5,4 @@ bibliography = רשימת מקורות
heading = חלק heading = חלק
outline = תוכן עניינים outline = תוכן עניינים
raw = קטע מקור raw = קטע מקור
page = עמוד page = עמוד

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

@ -4,5 +4,5 @@ equation = Egyenlet
bibliography = Irodalomjegyzék bibliography = Irodalomjegyzék
heading = Fejezet heading = Fejezet
outline = Tartalomjegyzék outline = Tartalomjegyzék
# raw = # raw =
page = oldal page = oldal

View File

@ -5,4 +5,4 @@ bibliography = Heimildaskrá
heading = Kafli heading = Kafli
outline = Efnisyfirlit outline = Efnisyfirlit
raw = Sýnishorn raw = Sýnishorn
page = blaðsíða page = blaðsíða

View File

@ -5,4 +5,4 @@ bibliography = Bibliografia
heading = Sezione heading = Sezione
outline = Indice outline = Indice
raw = Codice raw = Codice
page = pag. page = pag.

View File

@ -5,4 +5,4 @@ bibliography = 参考文献
heading = 節 heading = 節
outline = 目次 outline = 目次
raw = リスト raw = リスト
page = ページ page = ページ

View File

@ -5,4 +5,4 @@ bibliography = Conspectus librorum
heading = Caput heading = Caput
outline = Index capitum outline = Index capitum
raw = Exemplum raw = Exemplum
page = charta page = charta

View File

@ -5,4 +5,4 @@ bibliography = Bibliografi
heading = Kapittel heading = Kapittel
outline = Innhold outline = Innhold
raw = Utskrift raw = Utskrift
page = side page = side

View File

@ -5,4 +5,4 @@ bibliography = Bibliografie
heading = Hoofdstuk heading = Hoofdstuk
outline = Inhoudsopgave outline = Inhoudsopgave
raw = Listing raw = Listing
page = pagina page = pagina

View File

@ -5,4 +5,4 @@ bibliography = Bibliografi
heading = Kapittel heading = Kapittel
outline = Innhald outline = Innhald
raw = Utskrift raw = Utskrift
page = side page = side

View File

@ -5,4 +5,4 @@ bibliography = Bibliografia
heading = Sekcja heading = Sekcja
outline = Spis treści outline = Spis treści
raw = Program raw = Program
page = strona page = strona

View File

@ -1,8 +1,8 @@
# figure = # figure =
# table = # table =
# equation = # equation =
# bibliography = # bibliography =
heading = Secção heading = Secção
outline = Índice outline = Índice
# raw = # raw =
page = página page = página

View File

@ -5,4 +5,4 @@ bibliography = Bibliografia
heading = Seção heading = Seção
outline = Sumário outline = Sumário
raw = Listagem raw = Listagem
page = página page = página

View File

@ -6,4 +6,4 @@ heading = Secțiunea
outline = Cuprins outline = Cuprins
# may be wrong # may be wrong
raw = Listă raw = Listă
page = pagina page = pagina

View File

@ -5,4 +5,4 @@ bibliography = Библиография
heading = Раздел heading = Раздел
outline = Содержание outline = Содержание
raw = Листинг raw = Листинг
page = с. page = с.

View File

@ -5,4 +5,4 @@ bibliography = Literatura
heading = Poglavje heading = Poglavje
outline = Kazalo outline = Kazalo
raw = Program raw = Program
page = stran page = stran

View File

@ -5,4 +5,4 @@ bibliography = Bibliografi
heading = Kapitull heading = Kapitull
outline = Përmbajtja outline = Përmbajtja
raw = List raw = List
page = faqe page = faqe

View File

@ -5,4 +5,4 @@ bibliography = Литература
heading = Поглавље heading = Поглавље
outline = Садржај outline = Садржај
raw = Програм raw = Програм
page = страна page = страна

View File

@ -5,4 +5,4 @@ bibliography = Bibliografi
heading = Kapitel heading = Kapitel
outline = Innehåll outline = Innehåll
raw = Listing raw = Listing
page = sida page = sida

View File

@ -5,4 +5,4 @@ bibliography = Bibliograpiya
heading = Seksyon heading = Seksyon
outline = Talaan ng mga Nilalaman outline = Talaan ng mga Nilalaman
raw = Listahan raw = Listahan
# page = # page =

View File

@ -5,4 +5,4 @@ bibliography = Kaynakça
heading = Bölüm heading = Bölüm
outline = İçindekiler outline = İçindekiler
raw = Liste raw = Liste
page = sayfa page = sayfa

View File

@ -5,4 +5,4 @@ bibliography = Бібліографія
heading = Розділ heading = Розділ
outline = Зміст outline = Зміст
raw = Лістинг raw = Лістинг
page = c. page = c.

View File

@ -6,4 +6,4 @@ heading = Phần
outline = Mục lục outline = Mục lục
# may be wrong # may be wrong
raw = Chương trình raw = Chương trình
page = trang page = trang

View File

@ -1,8 +1,8 @@
figure = 圖 figure = 圖
# table = # table =
equation = 式 equation = 式
bibliography = 書目 bibliography = 書目
heading = 小節 heading = 小節
outline = 目錄 outline = 目錄
raw = 程式 raw = 程式
# page = # page =

View File

@ -5,4 +5,4 @@ bibliography = 参考文献
heading = 小节 heading = 小节
outline = 目录 outline = 目录
raw = 代码 raw = 代码
# page = # page =

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: 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>

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