Compare commits

...

44 Commits

Author SHA1 Message Date
+merlan #flirora
df3586ed00 Revert another stray change 2025-06-24 16:53:45 -04:00
+merlan #flirora
b25cdcdcb3 Remove another stray change 2025-06-24 16:46:10 -04:00
+merlan #flirora
5ae74fe2dd Remove IndexedItemEntry struct 2025-06-24 16:44:36 -04:00
+merlan #flirora
4d971f124e Revert unrelated changes 2025-06-24 16:30:52 -04:00
+merlan #flirora
fad03a474b Use unstable sort for sorting frames 2025-06-24 16:27:22 -04:00
+merlan #flirora
b5f0bc914a Update test 2025-06-24 16:27:12 -04:00
+merlan #flirora
46d4be8d6a Update reference image 2025-06-24 16:18:38 -04:00
+merlan #flirora
2db7ee0292 Fix Clippy errors 2025-06-24 16:18:38 -04:00
+merlan #flirora
327872fdc2 Remove debug statements 2025-06-24 16:18:38 -04:00
+merlan #flirora
6bf5240d10 Remove idx field from Run 2025-06-24 16:18:38 -04:00
+merlan #flirora
92ef06089f Sort frame items by logical index 2025-06-24 16:18:38 -04:00
+merlan #flirora
46b0d81f80 Associate each item in Items with a logical index 2025-06-24 16:18:38 -04:00
+merlan #flirora
b4d43e21cd Add idx field to Run 2025-06-24 16:18:38 -04:00
+merlan #flirora
e2d99a684b Extract Run struct 2025-06-24 16:18:38 -04:00
+merlan #flirora
0c5a7e7a9e Add minor fixes to doc-comments 2025-06-24 16:18:38 -04:00
+merlan #flirora
65b11a78f1 Add failing test for #5775 2025-06-24 16:18:38 -04: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
56 changed files with 2431 additions and 490 deletions

View File

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

20
Cargo.lock generated
View File

@ -786,9 +786,9 @@ checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
[[package]]
name = "font-types"
version = "0.8.4"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa6a5e5a77b5f3f7f9e32879f484aa5b3632ddfbe568a16266c904a6f32cdaf"
checksum = "02a596f5713680923a2080d86de50fe472fb290693cf0f701187a1c8b36996b7"
dependencies = [
"bytemuck",
]
@ -1367,8 +1367,7 @@ dependencies = [
[[package]]
name = "krilla"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69ee6128ebf52d7ce684613b6431ead2959f2be9ff8cf776eeaaad0427c953e9"
source = "git+https://github.com/LaurenzV/krilla?rev=20c14fe#20c14fefee5002566b3d6668b338bbe2168784e7"
dependencies = [
"base64",
"bumpalo",
@ -1396,8 +1395,7 @@ dependencies = [
[[package]]
name = "krilla-svg"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3462989578155cf620ef8035f8921533cc95c28e2a0c75de172f7219e6aba84e"
source = "git+https://github.com/LaurenzV/krilla?rev=20c14fe#20c14fefee5002566b3d6668b338bbe2168784e7"
dependencies = [
"flate2",
"fontdb",
@ -2106,9 +2104,9 @@ dependencies = [
[[package]]
name = "read-fonts"
version = "0.28.0"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "600e807b48ac55bad68a8cb75cc3c7739f139b9248f7e003e01e080f589b5288"
checksum = "192735ef611aac958468e670cb98432c925426f3cb71521fda202130f7388d91"
dependencies = [
"bytemuck",
"font-types",
@ -2434,9 +2432,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "skrifa"
version = "0.30.0"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fa1e5622e4f7b98877e8a19890efddcac1230cec6198bd9de91ec0e00010dc8"
checksum = "e6d632b5a73f566303dbeabd344dc3e716fd4ddc9a70d6fc8ea8e6f06617da97"
dependencies = [
"bytemuck",
"read-fonts",
@ -2863,7 +2861,7 @@ dependencies = [
[[package]]
name = "typst-assets"
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]]
name = "typst-cli"

View File

@ -32,7 +32,7 @@ typst-svg = { path = "crates/typst-svg", 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-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" }
arrayvec = "0.7.4"
az = "1.2"
@ -73,8 +73,8 @@ image = { version = "0.25.5", default-features = false, features = ["png", "jpeg
indexmap = { version = "2", features = ["serde"] }
infer = { version = "0.19.0", default-features = false }
kamadak-exif = "0.6"
krilla = { version = "0.4.0", default-features = false, features = ["raster-images", "comemo", "rayon"] }
krilla-svg = "0.1.0"
krilla = { git = "https://github.com/LaurenzV/krilla", rev = "20c14fe", default-features = false, features = ["raster-images", "comemo", "rayon"] }
krilla-svg = { git = "https://github.com/LaurenzV/krilla", rev = "20c14fe" }
kurbo = "0.11"
libfuzzer-sys = "0.4"
lipsum = "0.9"

View File

@ -2,7 +2,9 @@ use std::fmt::Write;
use typst_library::diag::{bail, At, SourceResult, StrResult};
use typst_library::foundations::Repr;
use typst_library::html::{charsets, tag, HtmlDocument, HtmlElement, HtmlNode, HtmlTag};
use typst_library::html::{
attr, charsets, tag, HtmlDocument, HtmlElement, HtmlNode, HtmlTag,
};
use typst_library::layout::Frame;
use typst_syntax::Span;
@ -28,7 +30,7 @@ struct Writer {
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) {
if w.pretty {
w.buf.push('\n');
@ -38,7 +40,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<()> {
match node {
HtmlNode::Tag(_) => {}
@ -49,7 +51,7 @@ fn write_node(w: &mut Writer, node: &HtmlNode) -> SourceResult<()> {
Ok(())
}
/// Encode plain text into the writer.
/// Encodes plain text into the writer.
fn write_text(w: &mut Writer, text: &str, span: Span) -> SourceResult<()> {
for c in text.chars() {
if charsets::is_valid_in_normal_element_text(c) {
@ -61,7 +63,7 @@ fn write_text(w: &mut Writer, text: &str, span: Span) -> SourceResult<()> {
Ok(())
}
/// Encode one element into the write.
/// Encodes one element into the writer.
fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
w.buf.push('<');
w.buf.push_str(&element.tag.resolve());
@ -69,6 +71,10 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
for (attr, value) in &element.attrs.0 {
w.buf.push(' ');
w.buf.push_str(&attr.resolve());
// If the string is empty, we can use shorthand syntax.
// `<elem attr="">..</div` is equivalent to `<elem attr>..</div>`
if !value.is_empty() {
w.buf.push('=');
w.buf.push('"');
for c in value.chars() {
@ -80,15 +86,38 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
}
w.buf.push('"');
}
}
w.buf.push('>');
if tag::is_void(element.tag) {
if !element.children.is_empty() {
bail!(element.span, "HTML void elements must not have children");
}
return Ok(());
}
if tag::is_raw(element.tag) {
write_raw(w, element)?;
} else if !element.children.is_empty() {
write_children(w, element)?;
}
w.buf.push_str("</");
w.buf.push_str(&element.tag.resolve());
w.buf.push('>');
Ok(())
}
/// Encodes the children of an element.
fn write_children(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
// See HTML spec § 13.1.2.5.
if matches!(element.tag, tag::pre | tag::textarea) && starts_with_newline(element) {
w.buf.push('\n');
}
let pretty = w.pretty;
if !element.children.is_empty() {
let pretty_inside = allows_pretty_inside(element.tag)
&& element.children.iter().any(|node| match node {
HtmlNode::Element(child) => wants_pretty_around(child.tag),
@ -115,16 +144,125 @@ fn write_element(w: &mut Writer, element: &HtmlElement) -> SourceResult<()> {
w.level -= 1;
write_indent(w);
}
w.pretty = pretty;
w.buf.push_str("</");
w.buf.push_str(&element.tag.resolve());
w.buf.push('>');
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
/// element's contents.
///
@ -160,7 +298,7 @@ fn write_escape(w: &mut Writer, c: char) -> StrResult<()> {
c if charsets::is_w3c_text_char(c) && c != '\r' => {
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(())
}

View File

@ -180,9 +180,6 @@ fn handle(
if let Some(body) = elem.body(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 {
tag: elem.tag,
attrs: elem.attrs(styles).clone(),

View File

@ -701,7 +701,10 @@ fn complete_params(ctx: &mut CompletionContext) -> bool {
let mut deciding = ctx.leaf.clone();
while !matches!(
deciding.kind(),
SyntaxKind::LeftParen | SyntaxKind::Comma | SyntaxKind::Colon
SyntaxKind::LeftParen
| SyntaxKind::RightParen
| SyntaxKind::Comma
| SyntaxKind::Colon
) {
let Some(prev) = deciding.prev_leaf() else { break };
deciding = prev;
@ -1734,6 +1737,8 @@ mod tests {
test("#numbering(\"foo\", 1, )", -2)
.must_include(["integer"])
.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
@ -1829,18 +1834,27 @@ mod tests {
#[test]
fn test_autocomplete_fonts() {
test("#text(font:)", -1)
test("#text(font:)", -2)
.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\""]);
test("#show math.equation: set text(font: )", -1)
test("#show math.equation: set text(font: )", -2)
.must_include(["\"New Computer Modern Math\""])
.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_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

@ -10,7 +10,7 @@ use typst::syntax::package::{PackageSpec, PackageVersion};
use typst::syntax::{FileId, Source, VirtualPath};
use typst::text::{Font, FontBook, TextElem, TextSize};
use typst::utils::{singleton, LazyHash};
use typst::{Library, World};
use typst::{Feature, Library, World};
use crate::IdeWorld;
@ -168,7 +168,9 @@ fn library() -> Library {
// 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
// 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
.set(PageElem::set_width(Smart::Custom(Abs::pt(120.0).into())));
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 {
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.
if !items.iter().any(|item| matches!(item, Item::Text(_))) {
if let Some(fallback) = fallback {
items.push(fallback);
items.push(fallback, usize::MAX);
}
}
@ -270,12 +270,13 @@ fn collect_range<'a>(
items: &mut Items<'a>,
fallback: &mut Option<ItemEntry<'a>>,
) {
for (subrange, item) in p.slice(range.clone()) {
for (idx, run) in p.slice(range.clone()).enumerate() {
// All non-text items are just kept, they can't be split.
let Item::Text(shaped) = item else {
items.push(item);
let Item::Text(shaped) = &run.item else {
items.push(&run.item, idx);
continue;
};
let subrange = &run.range;
// The intersection range of the item, the subrange, and the line's
// trimming.
@ -293,10 +294,10 @@ fn collect_range<'a>(
} else if split {
// When the item is split in half, reshape it.
let reshaped = shaped.reshape(engine, sliced);
items.push(Item::Text(reshaped));
items.push(Item::Text(reshaped), idx);
} else {
// When the item is fully contained, just keep it.
items.push(item);
items.push(&run.item, idx);
}
}
}
@ -499,16 +500,16 @@ pub fn commit(
// Build the frames and determine the height and baseline.
let mut frames = vec![];
for item in line.items.iter() {
let mut push = |offset: &mut Abs, frame: Frame| {
for &(idx, ref item) in line.items.indexed_iter() {
let mut push = |offset: &mut Abs, frame: Frame, idx: usize| {
let width = frame.width();
top.set_max(frame.baseline());
bottom.set_max(frame.size().y - frame.baseline());
frames.push((*offset, frame));
frames.push((*offset, frame, idx));
*offset += width;
};
match item {
match &**item {
Item::Absolute(v, _) => {
offset += *v;
}
@ -520,7 +521,7 @@ pub fn commit(
layout_box(elem, engine, loc.relayout(), styles, region)
})?;
apply_baseline_shift(&mut frame, *styles);
push(&mut offset, frame);
push(&mut offset, frame, idx);
} else {
offset += amount;
}
@ -532,15 +533,15 @@ pub fn commit(
justification_ratio,
extra_justification,
);
push(&mut offset, frame);
push(&mut offset, frame, idx);
}
Item::Frame(frame) => {
push(&mut offset, frame.clone());
push(&mut offset, frame.clone(), idx);
}
Item::Tag(tag) => {
let mut frame = Frame::soft(Size::zero());
frame.push(Point::zero(), FrameItem::Tag((*tag).clone()));
frames.push((offset, frame));
frames.push((offset, frame, idx));
}
Item::Skip(_) => {}
}
@ -559,8 +560,9 @@ pub fn commit(
add_par_line_marker(&mut output, marker, engine, locator, top);
}
frames.sort_unstable_by_key(|(_, _, idx)| *idx);
// Construct the line's frame.
for (offset, frame) in frames {
for (offset, frame, _) in frames {
let x = offset + p.config.align.position(remaining);
let y = top - frame.baseline();
output.push_frame(Point::new(x, y), frame);
@ -627,7 +629,7 @@ fn overhang(c: char) -> f64 {
}
/// 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> {
/// Create empty items.
@ -636,33 +638,38 @@ impl<'a> Items<'a> {
}
/// Push a new item.
pub fn push(&mut self, entry: impl Into<ItemEntry<'a>>) {
self.0.push(entry.into());
pub fn push(&mut self, entry: impl Into<ItemEntry<'a>>, idx: usize) {
self.0.push((idx, entry.into()));
}
/// Iterate over the items
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.
pub fn first(&self) -> Option<&Item<'a>> {
self.0.first().map(|item| &**item)
self.0.first().map(|(_, item)| &**item)
}
/// Access the last item.
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.
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.
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.
@ -673,12 +680,12 @@ impl<'a> Items<'a> {
impl<'a> FromIterator<ItemEntry<'a>> for Items<'a> {
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> {
type Target = Vec<ItemEntry<'a>>;
type Target = Vec<(usize, ItemEntry<'a>)>;
fn deref(&self) -> &Self::Target {
&self.0

View File

@ -844,8 +844,8 @@ fn linebreak_link(link: &str, mut f: impl FnMut(usize)) {
/// Whether hyphenation is enabled at the given offset.
fn hyphenate_at(p: &Preparation, offset: usize) -> bool {
p.config.hyphenate.unwrap_or_else(|| {
let (_, item) = p.get(offset);
match item.text() {
let run = p.get(offset);
match run.item.text() {
Some(text) => TextElem::hyphenate_in(text.styles).unwrap_or(p.config.justify),
None => false,
}
@ -855,8 +855,8 @@ fn hyphenate_at(p: &Preparation, offset: usize) -> bool {
/// The text language at the given offset.
fn lang_at(p: &Preparation, offset: usize) -> Option<hypher::Lang> {
let lang = p.config.lang.or_else(|| {
let (_, item) = p.get(offset);
let styles = item.text()?.styles;
let run = p.get(offset);
let styles = run.item.text()?.styles;
Some(TextElem::lang_in(styles))
})?;
@ -921,8 +921,8 @@ impl Estimates {
let mut shrinkability = CumulativeVec::with_capacity(cap);
let mut justifiables = CumulativeVec::with_capacity(cap);
for (range, item) in p.items.iter() {
if let Item::Text(shaped) = item {
for run in p.items.iter() {
if let Item::Text(shaped) = &run.item {
for g in shaped.glyphs.iter() {
let byte_len = g.range.len();
let stretch = g.stretchability().0 + g.stretchability().1;
@ -933,13 +933,13 @@ impl Estimates {
justifiables.push(byte_len, g.is_justifiable() as usize);
}
} else {
widths.push(range.len(), item.natural_width());
widths.push(run.range.len(), run.item.natural_width());
}
widths.adjust(range.end);
stretchability.adjust(range.end);
shrinkability.adjust(range.end);
justifiables.adjust(range.end);
widths.adjust(run.range.end);
stretchability.adjust(run.range.end);
shrinkability.adjust(run.range.end);
justifiables.adjust(run.range.end);
}
Self {

View File

@ -3,6 +3,11 @@ use unicode_bidi::{BidiInfo, Level as BidiLevel};
use super::*;
pub struct Run<'a> {
pub item: Item<'a>,
pub range: Range,
}
/// A representation in which children are already layouted and text is already
/// preshaped.
///
@ -20,22 +25,22 @@ pub struct Preparation<'a> {
/// direction).
pub bidi: Option<BidiInfo<'a>>,
/// Text runs, spacing and layouted elements.
pub items: Vec<(Range, Item<'a>)>,
pub items: Vec<Run<'a>>,
/// Maps from byte indices to item indices.
pub indices: Vec<usize>,
/// The span mapper.
pub spans: SpanMapper,
}
impl<'a> Preparation<'a> {
impl Preparation<'_> {
/// Get the item that contains the given `text_offset`.
pub fn get(&self, offset: usize) -> &(Range, Item<'a>) {
pub fn get(&self, offset: usize) -> &Run {
let idx = self.indices.get(offset).copied().unwrap_or(0);
&self.items[idx]
}
/// Iterate over the items that intersect the given `sliced` range.
pub fn slice(&self, sliced: Range) -> impl Iterator<Item = &(Range, Item<'a>)> {
pub fn slice(&self, sliced: Range) -> impl Iterator<Item = &Run> {
// Usually, we don't want empty-range items at the start of the line
// (because they will be part of the previous line), but for the first
// line, we need to keep them.
@ -43,8 +48,8 @@ impl<'a> Preparation<'a> {
0 => 0,
n => self.indices.get(n).copied().unwrap_or(0),
};
self.items[start..].iter().take_while(move |(range, _)| {
range.start < sliced.end || range.end <= sliced.end
self.items[start..].iter().take_while(move |run| {
run.range.start < sliced.end || run.range.end <= sliced.end
})
}
}
@ -84,7 +89,9 @@ pub fn prepare<'a>(
Segment::Text(_, styles) => {
shape_range(&mut items, engine, text, &bidi, range, styles);
}
Segment::Item(item) => items.push((range, item)),
Segment::Item(item) => {
items.push(Run { range, item });
}
}
cursor = end;
@ -92,8 +99,8 @@ pub fn prepare<'a>(
// Build the mapping from byte to item indices.
let mut indices = Vec::with_capacity(text.len());
for (i, (range, _)) in items.iter().enumerate() {
indices.extend(range.clone().map(|_| i));
for (i, run) in items.iter().enumerate() {
indices.extend(run.range.clone().map(|_| i));
}
if config.cjk_latin_spacing {
@ -113,15 +120,15 @@ pub fn prepare<'a>(
/// Add some spacing between Han characters and western characters. See
/// Requirements for Chinese Text Layout, Section 3.2.2 Mixed Text Composition
/// in Horizontal Written Mode
fn add_cjk_latin_spacing(items: &mut [(Range, Item)]) {
fn add_cjk_latin_spacing(items: &mut [Run]) {
let mut items = items
.iter_mut()
.filter(|(_, x)| !matches!(x, Item::Tag(_)))
.filter(|run| !matches!(run.item, Item::Tag(_)))
.peekable();
let mut prev: Option<&ShapedGlyph> = None;
while let Some((_, item)) = items.next() {
let Some(text) = item.text_mut() else {
while let Some(run) = items.next() {
let Some(text) = run.item.text_mut() else {
prev = None;
continue;
};
@ -135,7 +142,7 @@ fn add_cjk_latin_spacing(items: &mut [(Range, Item)]) {
let next = glyphs.peek().map(|n| n as _).or_else(|| {
items
.peek()
.and_then(|(_, i)| i.text())
.and_then(|run| run.item.text())
.and_then(|shaped| shaped.glyphs.first())
});

View File

@ -17,6 +17,7 @@ use typst_utils::SliceExt;
use unicode_bidi::{BidiInfo, Level as BidiLevel};
use unicode_script::{Script, UnicodeScript};
use super::prepare::Run;
use super::{decorate, Item, Range, SpanMapper};
use crate::modifiers::FrameModifyText;
@ -592,7 +593,7 @@ impl Debug for ShapedText<'_> {
/// Group a range of text by BiDi level and script, shape the runs and generate
/// items for them.
pub fn shape_range<'a>(
items: &mut Vec<(Range, Item<'a>)>,
items: &mut Vec<Run<'a>>,
engine: &Engine,
text: &'a str,
bidi: &BidiInfo<'a>,
@ -606,7 +607,7 @@ pub fn shape_range<'a>(
let dir = if level.is_ltr() { Dir::LTR } else { Dir::RTL };
let shaped =
shape(engine, range.start, &text[range.clone()], styles, dir, lang, region);
items.push((range, Item::Text(shaped)));
items.push(Run { range, item: Item::Text(shaped) });
};
let mut prev_level = BidiLevel::ltr();

View File

@ -11,8 +11,8 @@ use typst_library::layout::{
};
use typst_library::visualize::{
CircleElem, CloseMode, Curve, CurveComponent, CurveElem, EllipseElem, FillRule,
FixedStroke, Geometry, LineElem, Paint, PathElem, PathVertex, PolygonElem, RectElem,
Shape, SquareElem, Stroke,
FixedStroke, Geometry, LineCap, LineElem, Paint, PathElem, PathVertex, PolygonElem,
RectElem, Shape, SquareElem, Stroke,
};
use typst_syntax::Span;
use typst_utils::{Get, Numeric};
@ -889,7 +889,13 @@ fn segmented_rect(
let end = current;
last = current;
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 {
res.push(shape);
} else {
@ -899,7 +905,14 @@ fn segmented_rect(
}
} else if let Some(stroke) = &strokes.top {
// 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
@ -946,6 +959,8 @@ fn curve_segment(
fn segment(
start: Corner,
end: Corner,
start_cap: LineCap,
end_cap: LineCap,
corners: &Corners<ControlPoints>,
stroke: &FixedStroke,
) -> (Shape, bool) {
@ -979,7 +994,7 @@ fn segment(
let use_fill = solid && fill_corners(start, end, corners);
let shape = if use_fill {
fill_segment(start, end, corners, stroke)
fill_segment(start, end, start_cap, end_cap, corners, stroke)
} else {
stroke_segment(start, end, corners, stroke.clone())
};
@ -1010,6 +1025,8 @@ fn stroke_segment(
fn fill_segment(
start: Corner,
end: Corner,
start_cap: LineCap,
end_cap: LineCap,
corners: &Corners<ControlPoints>,
stroke: &FixedStroke,
) -> Shape {
@ -1035,8 +1052,7 @@ fn fill_segment(
if c.arc_outer() {
curve.arc_line(c.mid_outer(), c.center_outer(), c.end_outer());
} else {
curve.line(c.outer());
curve.line(c.end_outer());
c.start_cap(&mut curve, start_cap);
}
}
@ -1079,7 +1095,7 @@ fn fill_segment(
if c.arc_inner() {
curve.arc_line(c.mid_inner(), c.center_inner(), c.start_inner());
} else {
curve.line(c.center_inner());
c.end_cap(&mut curve, end_cap);
}
}
@ -1134,6 +1150,16 @@ struct 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.
fn rotate(&self, point: Point) -> Point {
match self.corner {
@ -1280,6 +1306,77 @@ impl ControlPoints {
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.

View File

@ -16,6 +16,21 @@ impl Duration {
pub fn is_zero(&self) -> bool {
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]
@ -118,34 +133,25 @@ impl Debug for Duration {
impl Repr for Duration {
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 weeks = tmp.whole_seconds() / 604_800.0 as i64;
if weeks != 0 {
vec.push(eco_format!("weeks: {}", weeks.repr()));
}
tmp -= weeks.weeks();
let days = tmp.whole_days();
if days != 0 {
vec.push(eco_format!("days: {}", days.repr()));
}
tmp -= days.days();
let hours = tmp.whole_hours();
if hours != 0 {
vec.push(eco_format!("hours: {}", hours.repr()));
}
tmp -= hours.hours();
let minutes = tmp.whole_minutes();
if minutes != 0 {
vec.push(eco_format!("minutes: {}", minutes.repr()));
}
tmp -= minutes.minutes();
let seconds = tmp.whole_seconds();
if seconds != 0 {
vec.push(eco_format!("seconds: {}", seconds.repr()));
}

View File

@ -210,3 +210,25 @@ cast! {
fn parse_float(s: EcoString) -> Result<f64, ParseFloatError> {
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> {
match &self.repr {
Repr::Native(native) => {
let value = (native.function)(engine, context, &mut args)?;
let value = (native.function.0)(engine, context, &mut args)?;
args.finish()?;
Ok(value)
}
@ -491,8 +491,8 @@ pub trait NativeFunc {
/// Defines a native function.
#[derive(Debug)]
pub struct NativeFuncData {
/// Invokes the function from Typst.
pub function: fn(&mut Engine, Tracked<Context>, &mut Args) -> SourceResult<Value>,
/// The implementation of the function.
pub function: NativeFuncPtr,
/// The function's normal name (e.g. `align`), as exposed to Typst.
pub name: &'static str,
/// The function's title case name (e.g. `Align`).
@ -504,11 +504,11 @@ pub struct NativeFuncData {
/// Whether this function makes use of context.
pub contextual: bool,
/// Definitions in the scope of the function.
pub scope: LazyLock<Scope>,
pub scope: DynLazyLock<Scope>,
/// 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.
pub returns: LazyLock<CastInfo>,
pub returns: DynLazyLock<CastInfo>,
}
cast! {
@ -516,6 +516,28 @@ cast! {
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.
#[derive(Debug, Clone)]
pub struct ParamInfo {

View File

@ -79,7 +79,12 @@ impl Label {
impl Repr for Label {
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

@ -188,7 +188,7 @@ cast! {
.collect::<HintedStrResult<_>>()?),
}
/// An attribute of an HTML.
/// An attribute of an HTML element.
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub struct HtmlAttr(PicoStr);
@ -347,135 +347,124 @@ pub mod charsets {
}
/// Predefined constants for HTML tags.
#[allow(non_upper_case_globals)]
pub mod tag {
use super::HtmlTag;
macro_rules! tags {
($($tag:ident)*) => {
$(#[allow(non_upper_case_globals)]
pub const $tag: HtmlTag = HtmlTag::constant(
stringify!($tag)
);)*
}
}
pub const a: HtmlTag = HtmlTag::constant("a");
pub const abbr: HtmlTag = HtmlTag::constant("abbr");
pub const address: HtmlTag = HtmlTag::constant("address");
pub const area: HtmlTag = HtmlTag::constant("area");
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! {
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
/// Whether this is a void tag whose associated element may not have
/// children.
pub fn is_void(tag: HtmlTag) -> bool {
matches!(
@ -490,7 +479,6 @@ pub mod tag {
| self::input
| self::link
| self::meta
| self::param
| self::source
| self::track
| self::wbr
@ -629,36 +617,196 @@ pub mod tag {
}
}
/// Predefined constants for HTML attributes.
///
/// Note: These are very incomplete.
#[allow(non_upper_case_globals)]
#[rustfmt::skip]
pub mod attr {
use super::HtmlAttr;
macro_rules! attrs {
($($attr:ident)*) => {
$(#[allow(non_upper_case_globals)]
pub const $attr: HtmlAttr = HtmlAttr::constant(
stringify!($attr)
);)*
}
}
attrs! {
charset
cite
colspan
content
href
name
reversed
role
rowspan
start
style
value
}
use crate::html::HtmlAttr;
pub const abbr: HtmlAttr = HtmlAttr::constant("abbr");
pub const accept: HtmlAttr = HtmlAttr::constant("accept");
pub const accept_charset: HtmlAttr = HtmlAttr::constant("accept-charset");
pub const accesskey: HtmlAttr = HtmlAttr::constant("accesskey");
pub const action: HtmlAttr = HtmlAttr::constant("action");
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");
pub const aria_atomic: HtmlAttr = HtmlAttr::constant("aria-atomic");
pub const aria_autocomplete: HtmlAttr = HtmlAttr::constant("aria-autocomplete");
pub const aria_busy: HtmlAttr = HtmlAttr::constant("aria-busy");
pub const aria_checked: HtmlAttr = HtmlAttr::constant("aria-checked");
pub const aria_colcount: HtmlAttr = HtmlAttr::constant("aria-colcount");
pub const aria_colindex: HtmlAttr = HtmlAttr::constant("aria-colindex");
pub const aria_colspan: HtmlAttr = HtmlAttr::constant("aria-colspan");
pub const aria_controls: HtmlAttr = HtmlAttr::constant("aria-controls");
pub const aria_current: HtmlAttr = HtmlAttr::constant("aria-current");
pub const aria_describedby: HtmlAttr = HtmlAttr::constant("aria-describedby");
pub const aria_details: HtmlAttr = HtmlAttr::constant("aria-details");
pub const aria_disabled: HtmlAttr = HtmlAttr::constant("aria-disabled");
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_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.
mod dom;
mod typed;
pub use self::dom::*;
@ -14,6 +15,7 @@ pub fn module() -> Module {
html.start_category(crate::Category::Html);
html.define_elem::<HtmlElem>();
html.define_elem::<FrameElem>();
self::typed::define(&mut 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

@ -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:
/// - `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 crate::diag::bail;
use crate::foundations::{array, cast, Array, Resolve, Smart, StyleChain};
use crate::layout::{Abs, Dir, Length, Ratio, Rel, Size};
use crate::diag::{bail, HintedStrResult};
use crate::foundations::{
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.
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
@ -275,40 +278,39 @@ impl BitAndAssign for Axes<bool> {
}
}
cast! {
Axes<Rel<Length>>,
self => array![self.x, self.y].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!("point array must contain exactly two entries"),
impl<T: Reflect> Reflect for Axes<T> {
fn input() -> CastInfo {
Array::input()
}
fn output() -> CastInfo {
Array::output()
}
fn castable(value: &Value) -> bool {
Array::castable(value)
}
},
}
cast! {
Axes<Ratio>,
self => array![self.x, self.y].into_value(),
array: Array => {
impl<T: FromValue> FromValue for Axes<T> {
fn from_value(value: Value) -> HintedStrResult<Self> {
let array = value.cast::<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!("ratio array must contain exactly two entries"),
(Some(a), Some(b), None) => Ok(Axes::new(a.cast()?, b.cast()?)),
_ => 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! {
Axes<Length>,
self => array![self.x, self.y].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: IntoValue> IntoValue for Axes<T> {
fn into_value(self) -> Value {
array![self.x.into_value(), self.y.into_value()].into_value()
}
},
}
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
/// everything else configurable via set and show rules).
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,
/// In-development features that were enabled.
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::foundations::{cast, elem, func, Content, NativeElement, SymbolElem};
use crate::layout::{Length, Rel};
@ -81,17 +88,22 @@ impl Accent {
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.
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

@ -90,7 +90,7 @@ use crate::World;
/// ```
#[elem(Locatable, Synthesize, Show, ShowSet, LocalName)]
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.
///
/// This can be a:
@ -385,7 +385,7 @@ fn decode_library(loaded: &Loaded) -> SourceResult<Library> {
.within(loaded),
_ => bail!(
loaded.source.span,
"unknown bibliography format (must be .yml/.yaml or .bib)"
"unknown bibliography format (must be .yaml/.yml or .bib)"
),
}
} else {

View File

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

View File

@ -5,7 +5,7 @@ use crate::diag::{bail, At, Hint, SourceResult};
use crate::engine::Engine;
use crate::foundations::{
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::math::EquationElem;
@ -79,6 +79,36 @@ use crate::text::TextElem;
/// reference: `[@intro[Chapter]]`.
///
/// # 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
/// 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
@ -91,16 +121,13 @@ use crate::text::TextElem;
/// #show ref: it => {
/// let eq = math.equation
/// let el = it.element
/// if el != none and el.func() == eq {
/// // Skip all other references.
/// if el == none or el.func() != eq { return it }
/// // Override equation references.
/// link(el.location(),numbering(
/// link(el.location(), numbering(
/// el.numbering,
/// ..counter(eq).at(el.location())
/// ))
/// } else {
/// // Other references as usual.
/// it
/// }
/// }
///
/// = Beginnings <beginning>
@ -229,8 +256,15 @@ impl Show for Packed<RefElem> {
// RefForm::Normal
if BibliographyElem::has(engine, self.target) {
if elem.is_ok() {
bail!(span, "label occurs in the document and its bibliography");
if let Ok(elem) = elem {
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));

View File

@ -30,6 +30,7 @@ const TRANSLATIONS: &[(&str, &str)] = &[
translation!("fr"),
translation!("gl"),
translation!("he"),
translation!("hr"),
translation!("hu"),
translation!("id"),
translation!("is"),

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 {
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 }
}

View File

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

View File

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

View File

@ -315,15 +315,15 @@ fn create_func_data(func: &Func) -> TokenStream {
quote! {
#foundations::NativeFuncData {
function: #closure,
function: #foundations::NativeFuncPtr(&#closure),
name: #name,
title: #title,
docs: #docs,
keywords: &[#(#keywords),*],
contextual: #contextual,
scope: ::std::sync::LazyLock::new(|| #scope),
params: ::std::sync::LazyLock::new(|| ::std::vec![#(#params),*]),
returns: ::std::sync::LazyLock::new(|| <#returns as #foundations::Reflect>::output()),
scope: ::std::sync::LazyLock::new(&|| #scope),
params: ::std::sync::LazyLock::new(&|| ::std::vec![#(#params),*]),
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_svg::render_svg_glyph;
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::layout::{
Abs, Frame, FrameItem, GroupItem, PagedDocument, Size, Transform,
@ -429,14 +429,18 @@ fn convert_error(
display_font(gc.fonts_backward.get(f).unwrap());
hint: "try using a different font"
),
ValidationError::InvalidCodepointMapping(_, _, cp, loc) => {
if let Some(c) = cp.map(|c| eco_format!("{:#06x}", c as u32)) {
ValidationError::InvalidCodepointMapping(_, _, c, loc) => {
if let Some(c) = c {
let msg = if loc.is_some() {
"the PDF contains text with"
} else {
"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 {
// I think this code path is in theory unreachable,
// but just to be safe.
@ -454,13 +458,12 @@ fn convert_error(
}
}
ValidationError::UnicodePrivateArea(_, _, c, loc) => {
let code_point = eco_format!("{:#06x}", *c as u32);
let msg = if loc.is_some() { "the PDF" } else { "the text" };
error!(
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 \
forbidden in this export mode"
forbidden in this export mode",
)
}
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());
// TODO: update when new krilla version lands (https://github.com/LaurenzV/krilla/pull/203)
let compress = should_compress(&embed.data).unwrap_or(true);
let compress = should_compress(&embed.data);
let file = EmbeddedFile {
path,

View File

@ -255,13 +255,13 @@ pub fn to_sk_paint<'a>(
}
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)
.expect("components must always be in the range [0..=1]")
}
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)
}

View File

@ -23,7 +23,7 @@ pub use self::scalar::Scalar;
#[doc(hidden)]
pub use once_cell;
use std::fmt::{Debug, Formatter};
use std::fmt::{Debug, Display, Formatter};
use std::hash::Hash;
use std::iter::{Chain, Flatten, Rev};
use std::num::{NonZeroU32, NonZeroUsize};
@ -52,6 +52,25 @@ where
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.
pub fn hash128<T: Hash + ?Sized>(value: &T) -> u128 {
let mut state = SipHasher13::new();

View File

@ -204,18 +204,70 @@ mod exceptions {
use std::cmp::Ordering;
/// A global list of non-bitcode-encodable compile-time internible strings.
///
/// Must be sorted.
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",
"contenteditable",
"discretionary-ligatures",
"fetchpriority",
"formnovalidate",
"h5",
"h6",
"historical-ligatures",
"number-clearance",
"number-margin",
"numbering-scope",
"onbeforeprint",
"onbeforeunload",
"onlanguagechange",
"onmessageerror",
"onrejectionhandled",
"onunhandledrejection",
"page-numbering",
"par-line-marker",
"popovertarget",
"popovertargetaction",
"referrerpolicy",
"shadowrootclonable",
"shadowrootcustomelementregistry",
"shadowrootdelegatesfocus",
"shadowrootmode",
"shadowrootserializable",
"transparentize",
"writingsuggestions",
];
/// Try to find the index of an exception if it exists.

View File

@ -21,7 +21,7 @@ description: Changes in Typst 0.9.0
- Added [`full`]($bibliography.full) argument to bibliography function to print
the full bibliography even if not all works were cited
- 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
[changelog](https://github.com/typst/hayagriva/blob/main/CHANGELOG.md) for
more details. **(Breaking change)**

View File

@ -137,6 +137,59 @@
In addition to the functions listed below, the `calc` module also defines
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 dictionary(std) {
tiling
} else {
pattern
}
...
```
- name: sys
title: System
category: foundations

View File

@ -37,7 +37,7 @@ static GROUPS: LazyLock<Vec<GroupData>> = LazyLock::new(|| {
let mut groups: Vec<GroupData> =
yaml::from_str(load!("reference/groups.yml")).unwrap();
for group in &mut groups {
if group.filter.is_empty() {
if group.filter.is_empty() && group.name != "std" {
group.filter = group
.module()
.scope()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 B

View File

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

View File

@ -0,0 +1,66 @@
--- html-non-char html ---
// Error: 1-9 the character `"\u{fdd0}"` cannot be encoded in HTML
\u{fdd0}
--- html-void-element-with-children html ---
// Error: 2-27 HTML void elements must not have children
#html.elem("img", [Hello])
--- html-pre-starting-with-newline html ---
#html.pre("hello")
#html.pre("\nhello")
#html.pre("\n\nhello")
--- html-textarea-starting-with-newline html ---
#html.textarea("\nenter")
--- html-script html ---
// This should be pretty and indented.
#html.script(
```js
const x = 1
const y = 2
console.log(x < y, Math.max(1, 2))
```.text,
)
// This should have extra newlines, but no indent because of the multiline
// string literal.
#html.script("console.log(`Hello\nWorld`)")
// This should be untouched.
#html.script(
type: "text/python",
```py
x = 1
y = 2
print(x < y, max(x, y))
```.text,
)
--- html-style html ---
// This should be pretty and indented.
#html.style(
```css
body {
text: red;
}
```.text,
)
--- html-raw-text-contains-elem html ---
// Error: 14-32 HTML raw text element cannot have non-text children
#html.script(html.strong[Hello])
--- html-raw-text-contains-frame html ---
// Error: 2-29 HTML raw text element cannot have non-text children
#html.script(html.frame[Ok])
--- html-raw-text-contains-closing-tag html ---
// Error: 2-32 HTML raw text element cannot contain its own closing tag
// Hint: 2-32 the sequence `</SCRiPT` appears in the raw text
#html.script("hello </SCRiPT ")
--- html-escapable-raw-text-contains-closing-tag html ---
// This is okay because we escape it.
#html.textarea("hello </textarea>")

187
tests/suite/html/typed.typ Normal file
View File

@ -0,0 +1,187 @@
--- html-typed html ---
// String
#html.div(id: "hi")
// Different kinds of options.
#html.div(aria-autocomplete: none) // "none"
#html.div(aria-expanded: none) // "undefined"
#html.link(referrerpolicy: none) // present
// Different kinds of bools.
#html.div(autofocus: false) // absent
#html.div(autofocus: true) // present
#html.div(hidden: false) // absent
#html.div(hidden: true) // present
#html.div(aria-atomic: false) // "false"
#html.div(aria-atomic: true) // "true"
#html.div(translate: false) // "no"
#html.div(translate: true) // "yes"
#html.form(autocomplete: false) // "on"
#html.form(autocomplete: true) // "off"
// Char
#html.div(accesskey: "K")
// Int
#html.div(aria-colcount: 2)
#html.object(width: 120, height: 10)
#html.td(rowspan: 2)
// Float
#html.meter(low: 3.4, high: 7.9)
// Space-separated strings.
#html.div(class: "alpha")
#html.div(class: "alpha beta")
#html.div(class: ("alpha", "beta"))
// Comma-separated strings.
#html.div(html.input(accept: "image/jpeg"))
#html.div(html.input(accept: "image/jpeg, image/png"))
#html.div(html.input(accept: ("image/jpeg", "image/png")))
// Comma-separated floats.
#html.area(coords: (2.3, 4, 5.6))
// Colors.
#for c in (
red,
red.lighten(10%),
luma(50%),
cmyk(10%, 20%, 30%, 40%),
oklab(27%, 20%, -3%, 50%),
color.linear-rgb(20%, 30%, 40%, 50%),
color.hsl(20deg, 10%, 20%),
color.hsv(30deg, 20%, 30%),
) {
html.link(color: c)
}
// Durations & datetimes.
#for d in (
duration(weeks: 3, seconds: 4),
duration(days: 1, minutes: 4),
duration(),
datetime(day: 10, month: 7, year: 2005),
datetime(day: 1, month: 2, year: 0),
datetime(hour: 6, minute: 30, second: 0),
datetime(day: 1, month: 2, year: 0, hour: 11, minute: 11, second: 0),
datetime(day: 1, month: 2, year: 0, hour: 6, minute: 0, second: 9),
) {
html.div(html.time(datetime: d))
}
// Direction
#html.div(dir: ltr)[RTL]
// Image candidate and source size.
#html.img(
src: "image.png",
alt: "My wonderful image",
srcset: (
(src: "/image-120px.png", width: 120),
(src: "/image-60px.png", width: 60),
),
sizes: (
(condition: "min-width: 800px", size: 400pt),
(condition: "min-width: 400px", size: 250pt),
)
)
// String enum.
#html.form(enctype: "text/plain")
#html.form(role: "complementary")
#html.div(hidden: "until-found")
// Or.
#html.div(aria-checked: false)
#html.div(aria-checked: true)
#html.div(aria-checked: "mixed")
// Input value.
#html.div(html.input(value: 5.6))
#html.div(html.input(value: red))
#html.div(html.input(min: 3, max: 9))
// Icon size.
#html.link(rel: "icon", sizes: ((32, 24), (64, 48)))
--- html-typed-dir-str html ---
// Error: 16-21 expected direction or auto, found string
#html.div(dir: "ltr")
--- html-typed-char-too-long html ---
// Error: 22-35 expected exactly one character
#html.div(accesskey: ("Ctrl", "K"))
--- html-typed-int-negative html ---
// Error: 18-21 number must be at least zero
#html.img(width: -10)
--- html-typed-int-zero html ---
// Error: 22-23 number must be positive
#html.textarea(rows: 0)
--- html-typed-float-negative html ---
// Error: 19-23 number must be positive
#html.input(step: -3.4)
--- html-typed-string-array-with-space html ---
// Error: 18-41 array item may not contain a space
// Hint: 18-41 the array attribute will be encoded as a space-separated string
#html.div(class: ("alpha beta", "gamma"))
--- html-typed-float-array-invalid-shorthand html ---
// Error: 20-23 expected array, found float
#html.area(coords: 4.5)
--- html-typed-dir-vertical html ---
// Error: 16-19 direction must be horizontal
#html.div(dir: ttb)
--- html-typed-string-enum-invalid html ---
// Error: 21-28 expected "application/x-www-form-urlencoded", "multipart/form-data", or "text/plain"
#html.form(enctype: "utf-8")
--- html-typed-or-invalid ---
// Error: 25-31 expected boolean or "mixed"
#html.div(aria-checked: "nope")
--- html-typed-string-enum-or-array-invalid ---
// Error: 27-33 expected array, "additions", "additions text", "all", "removals", or "text"
// Error: 49-54 expected boolean or "mixed"
#html.link(aria-relevant: "nope", aria-checked: "yes")
--- html-typed-srcset-both-width-and-density html ---
// Error: 19-64 cannot specify both `width` and `density`
#html.img(srcset: ((src: "img.png", width: 120, density: 0.5),))
--- html-typed-srcset-src-comma html ---
// Error: 19-50 `src` must not start or end with a comma
#html.img(srcset: ((src: "img.png,", width: 50),))
--- html-typed-sizes-string-size html ---
// Error: 18-66 expected length, found string
// Hint: 18-66 CSS lengths that are not expressible as Typst lengths are not yet supported
// Hint: 18-66 you can use `html.elem` to create a raw attribute
#html.img(sizes: ((condition: "min-width: 100px", size: "10px"),))
--- html-typed-input-value-invalid html ---
// Error: 20-25 expected string, float, datetime, color, or array, found boolean
#html.input(value: false)
--- html-typed-input-bound-invalid html ---
// Error: 18-21 expected string, float, or datetime, found color
#html.input(min: red)
--- html-typed-icon-size-invalid html ---
// Error: 32-45 expected array, found string
#html.link(rel: "icon", sizes: "10x20 20x30")
--- html-typed-hidden-none html ---
// Error: 19-23 expected boolean or "until-found", found none
#html.div(hidden: none)
--- html-typed-invalid-body html ---
// Error: 10-14 unexpected argument
#html.img[hi]

View File

@ -147,3 +147,15 @@ B #cite(<netwok>) #cite(<arrgh>).
// Error: 7-17 expected label, found string
// Hint: 7-17 use `label("%@&#*!\\")` to create a label
#cite("%@&#*!\\")
--- issue-5775-cite-order-rtl ---
// Test citation order in RTL text.
#set page(width: 300pt)
@netwok
aaa
این است
@tolkien54
و این یکی هست
@arrgh
#bibliography("/assets/bib/works.bib")

View File

@ -51,7 +51,8 @@ $ A = 1 $ <eq2>
// Test ambiguous reference.
= Introduction <arrgh>
// Error: 1-7 label occurs in the document and its bibliography
// Error: 1-7 label `<arrgh>` occurs both in the document and its bibliography
// Hint: 1-7 change either the heading's label or the bibliography key to resolve the ambiguity
@arrgh
#bibliography("/assets/bib/works.bib")

View File

@ -84,7 +84,8 @@
--- line-bad-point-array ---
// Test errors.
// Error: 12-19 point array must contain exactly two entries
// Error: 12-19 array must contain exactly two items
// Hint: 12-19 the first item determines the value for the X axis and the second item the value for the Y axis
#line(end: (50pt,))
--- line-bad-point-component-type ---

View File

@ -76,7 +76,8 @@
#path(((0%, 0%), (0%, 0%), (0%, 0%), (0%, 0%)))
--- path-bad-point-array ---
// Error: 7-31 point array must contain exactly two entries
// Error: 7-31 array must contain exactly two items
// Hint: 7-31 the first item determines the value for the X axis and the second item the value for the Y axis
// Warning: 2-6 the `path` function is deprecated, use `curve` instead
#path(((0%, 0%), (0%, 0%, 0%)))

View File

@ -49,7 +49,8 @@
)
--- polygon-bad-point-array ---
// Error: 10-17 point array must contain exactly two entries
// Error: 10-17 array must contain exactly two items
// Hint: 10-17 the first item determines the value for the X axis and the second item the value for the Y axis
#polygon((50pt,))
--- polygon-infinite-size ---

View File

@ -54,6 +54,22 @@
#v(3pt)
#rect(width: 20pt, height: 20pt, stroke: (thickness: 5pt, join: "round"))
--- rect-stroke-caps ---
// Separated segments
#rect(width: 20pt, height: 20pt, stroke: (
left: (cap: "round", thickness: 5pt),
right: (cap: "square", thickness: 7pt),
))
// Joined segment with different caps.
#rect(width: 20pt, height: 20pt, stroke: (
left: (cap: "round", thickness: 5pt),
top: (cap: "square", thickness: 7pt),
))
// No caps when there is a radius for that corner.
#rect(width: 20pt, height: 20pt, radius: (top: 3pt), stroke: (
left: (cap: "round", thickness: 5pt),
top: (cap: "square", thickness: 7pt),
))
--- red-stroke-bad-type ---
// Error: 15-21 expected length, color, gradient, tiling, dictionary, stroke, none, or auto, found array
#rect(stroke: (1, 2))