Compare commits

...

39 Commits

Author SHA1 Message Date
Tobias Schmitz
bffd208691
WIP [no ci] 2025-06-28 21:57:58 +02:00
Tobias Schmitz
671ca337d4
feat: [no ci] tag table headers and footers 2025-06-28 21:57:58 +02:00
Tobias Schmitz
8055ecc769
feat: [no ci] support headings with level >= 7 2025-06-27 14:21:41 +02:00
Tobias Schmitz
d6fea42984
refactor: [no ci] move link tagging code 2025-06-27 14:17:39 +02:00
Tobias Schmitz
ad9fc0cafb
fix: [no ci] don't include outline title in TOC hierarchy 2025-06-27 14:17:39 +02:00
Tobias Schmitz
fa6d5f2c51
fix: only use link annotation quadpoints when exporting a PDF/UA-1 document 2025-06-27 14:17:39 +02:00
Tobias Schmitz
40f3b7756c
feat: [no ci] hierarchical outline tags 2025-06-27 14:17:39 +02:00
Tobias Schmitz
9f0b320729
docs: [no ci] fixup some comments 2025-06-27 14:17:39 +02:00
Tobias Schmitz
40b438e1ec
feat: [no ci] mark RepeatElem as artifact 2025-06-27 14:17:39 +02:00
Tobias Schmitz
0de83dbb05
fix: [no ci] mark table gutter and fill as artifacts 2025-06-27 14:17:39 +02:00
Tobias Schmitz
1d90860357
feat: always write alt text in marked content sequence for images 2025-06-27 14:17:39 +02:00
Tobias Schmitz
298835e6fe
feat: [no ci] add cli args for PDF/UA-1 standard and to disable tagging 2025-06-27 14:17:39 +02:00
Tobias Schmitz
5c44d2222a
refactor: revert some changes to FrameItem::Link 2025-06-27 14:17:39 +02:00
Tobias Schmitz
45759253d3
refactor: [no ci] derive(Cast) for ArtifactKind 2025-06-27 14:17:39 +02:00
Tobias Schmitz
67f9b04617
fix: [no ci] avoid empty marked-content sequences 2025-06-27 14:17:39 +02:00
Tobias Schmitz
1bf8c37a19
feat: [no ci] generate tags for tables 2025-06-27 14:17:39 +02:00
Tobias Schmitz
bcf40aa755
feat: [no ci] use local krilla version 2025-06-27 14:17:39 +02:00
Tobias Schmitz
53e3d50611
feat: [no ci] pdf.tag function to manually create pdf tags 2025-06-27 14:17:39 +02:00
Tobias Schmitz
d982bdba49
feat: [no ci] write tags for more elements 2025-06-27 14:17:39 +02:00
Tobias Schmitz
3d36f75e86
feat: [no ci] write tags for links and use quadpoints in link annotations 2025-06-27 14:17:39 +02:00
Tobias Schmitz
761ae38fcd
feat: pdf.artifact element 2025-06-27 14:17:39 +02:00
Tobias Schmitz
8ee990c453
feat: [no ci] mark artifacts 2025-06-27 14:17:39 +02:00
Tobias Schmitz
a80502a110
feat: [WIP] allow specifying alt text for links
skip-checks:true

# Please enter the commit message for your changes. Lines starting
# with '#' will be kept; you may remove them yourself if you want to.
# An empty message aborts the commit.
#
# Date:      Wed May 28 17:47:35 2025 +0200
#
# On branch pdf-accessibility
# Your branch and 'origin/pdf-accessibility' have diverged,
# and have 11 and 5 different commits each, respectively.
#
# Changes to be committed:
#	modified:   crates/typst-ide/src/jump.rs
#	modified:   crates/typst-layout/src/flow/distribute.rs
#	modified:   crates/typst-layout/src/modifiers.rs
#	modified:   crates/typst-library/src/foundations/content.rs
#	modified:   crates/typst-library/src/layout/frame.rs
#	modified:   crates/typst-library/src/model/bibliography.rs
#	modified:   crates/typst-library/src/model/footnote.rs
#	modified:   crates/typst-library/src/model/link.rs
#	modified:   crates/typst-library/src/model/outline.rs
#	modified:   crates/typst-library/src/model/reference.rs
#	modified:   crates/typst-pdf/src/convert.rs
#	modified:   crates/typst-pdf/src/link.rs
#	modified:   crates/typst-render/src/lib.rs
#	modified:   crates/typst-svg/src/lib.rs
#	modified:   tests/src/run.rs
#
2025-06-27 14:17:39 +02:00
Tobias Schmitz
6f18f28a21
feat: [WIP] include links in tag tree
skip-checks:true
2025-06-27 14:17:39 +02:00
Tobias Schmitz
f3810dd36a
feat: [WIP] write tags
skip-checks:true
2025-06-27 14:17:39 +02:00
Tobias Schmitz
6eaa12fab8
feat: [WIP] make more things locatable
skip-checks:true
2025-06-27 14:17:37 +02:00
Tobias Schmitz
41b57697d9
feat: [draft] generate accessibility tag tree for headings
skip-checks:true
2025-06-27 14:13:02 +02:00
Max
74b1b10986
Bump typst-dev-assets (#6514) 2025-06-27 10:35:05 +00:00
+merlan #flirora
584dd5fec6
Fix panic when sampling across two coincident gradient stops (#6166) 2025-06-27 09:26:15 +00:00
+merlan #flirora
b9f3a95e03
Sort line items by logical order when constructing frame (#5887)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-27 08:36:46 +00:00
Florian Bohlken
e8ce894ee7
Improve sentence in guide for LaTeX users (#6511) 2025-06-26 15:24:55 +00:00
Laurenz
9311f6f08e
Basic support for text decoration functions in HTML (#6510) 2025-06-26 13:44:45 +00:00
Laurenz
7420ec972f
Fix nested HTML frames (#6509) 2025-06-26 13:20:22 +00:00
Said A.
5dd5771df0
Disallow empty labels and references (#5776) (#6332)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
2025-06-26 09:18:51 +00:00
Malo
04fd0acaca
Allow deprecating symbol variants (#6441) 2025-06-26 08:24:21 +00:00
Laurenz
6a1d6c08e2
Consistent sizing for html.frame (#6505) 2025-06-26 08:07:41 +00:00
Laurenz
35809387f8
Support in operator on strings and modules (#6498) 2025-06-26 08:06:22 +00:00
Connor K
d3caedd813
Fix typos in page-setup.md (#6499) 2025-06-25 16:59:19 +00:00
+merlan #flirora
d54544297b
Minor fixes to doc comments (#6500) 2025-06-25 16:58:40 +00:00
86 changed files with 1569 additions and 282 deletions

6
Cargo.lock generated
View File

@ -413,7 +413,7 @@ dependencies = [
[[package]] [[package]]
name = "codex" name = "codex"
version = "0.1.1" version = "0.1.1"
source = "git+https://github.com/typst/codex?rev=56eb217#56eb2172fc0670f4c1c8b79a63d11f9354e5babe" source = "git+https://github.com/typst/codex?rev=a5428cb#a5428cb9c81a41354d44b44dbd5a16a710bbd928"
[[package]] [[package]]
name = "color-print" name = "color-print"
@ -1367,7 +1367,6 @@ dependencies = [
[[package]] [[package]]
name = "krilla" name = "krilla"
version = "0.4.0" version = "0.4.0"
source = "git+https://github.com/LaurenzV/krilla?rev=20c14fe#20c14fefee5002566b3d6668b338bbe2168784e7"
dependencies = [ dependencies = [
"base64", "base64",
"bumpalo", "bumpalo",
@ -1395,7 +1394,6 @@ dependencies = [
[[package]] [[package]]
name = "krilla-svg" name = "krilla-svg"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/LaurenzV/krilla?rev=20c14fe#20c14fefee5002566b3d6668b338bbe2168784e7"
dependencies = [ dependencies = [
"flate2", "flate2",
"fontdb", "fontdb",
@ -2911,7 +2909,7 @@ dependencies = [
[[package]] [[package]]
name = "typst-dev-assets" name = "typst-dev-assets"
version = "0.13.1" version = "0.13.1"
source = "git+https://github.com/typst/typst-dev-assets?rev=fddbf8b#fddbf8b99506bc370ac0edcd4959add603a7fc92" source = "git+https://github.com/typst/typst-dev-assets?rev=bfa947f#bfa947f3433d7d13a995168c40ae788a2ebfe648"
[[package]] [[package]]
name = "typst-docs" name = "typst-docs"

View File

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

View File

@ -246,6 +246,13 @@ pub struct CompileArgs {
#[arg(long = "pdf-standard", value_delimiter = ',')] #[arg(long = "pdf-standard", value_delimiter = ',')]
pub pdf_standard: Vec<PdfStandard>, pub pdf_standard: Vec<PdfStandard>,
/// By default, even when not producing a `PDF/UA-1` document, a tagged PDF
/// document is written to provide a baseline of accessibility. In some
/// circumstances (for example when trying to reduce the size of a document)
/// it can be desirable to disable tagged PDF.
#[arg(long = "disable-pdf-tags")]
pub disable_pdf_tags: bool,
/// The PPI (pixels per inch) to use for PNG export. /// The PPI (pixels per inch) to use for PNG export.
#[arg(long = "ppi", default_value_t = 144.0)] #[arg(long = "ppi", default_value_t = 144.0)]
pub ppi: f32, pub ppi: f32,
@ -506,6 +513,9 @@ pub enum PdfStandard {
/// PDF/A-4e. /// PDF/A-4e.
#[value(name = "a-4e")] #[value(name = "a-4e")]
A_4e, A_4e,
/// PDF/UA-1.
#[value(name = "ua-1")]
Ua_1,
} }
display_possible_values!(PdfStandard); display_possible_values!(PdfStandard);

View File

@ -65,6 +65,8 @@ pub struct CompileConfig {
pub open: Option<Option<String>>, pub open: Option<Option<String>>,
/// A list of standards the PDF should conform to. /// A list of standards the PDF should conform to.
pub pdf_standards: PdfStandards, pub pdf_standards: PdfStandards,
/// Whether to write PDF (accessibility) tags.
pub disable_pdf_tags: bool,
/// A path to write a Makefile rule describing the current compilation. /// A path to write a Makefile rule describing the current compilation.
pub make_deps: Option<PathBuf>, pub make_deps: Option<PathBuf>,
/// The PPI (pixels per inch) to use for PNG export. /// The PPI (pixels per inch) to use for PNG export.
@ -150,6 +152,7 @@ impl CompileConfig {
output_format, output_format,
pages, pages,
pdf_standards, pdf_standards,
disable_pdf_tags: args.disable_pdf_tags,
creation_timestamp: args.world.creation_timestamp, creation_timestamp: args.world.creation_timestamp,
make_deps: args.make_deps.clone(), make_deps: args.make_deps.clone(),
ppi: args.ppi, ppi: args.ppi,
@ -291,6 +294,7 @@ fn export_pdf(document: &PagedDocument, config: &CompileConfig) -> SourceResult<
timestamp, timestamp,
page_ranges: config.pages.clone(), page_ranges: config.pages.clone(),
standards: config.pdf_standards.clone(), standards: config.pdf_standards.clone(),
disable_tags: config.disable_pdf_tags,
}; };
let buffer = typst_pdf::pdf(document, &options)?; let buffer = typst_pdf::pdf(document, &options)?;
config config
@ -773,6 +777,7 @@ impl From<PdfStandard> for typst_pdf::PdfStandard {
PdfStandard::A_4 => typst_pdf::PdfStandard::A_4, PdfStandard::A_4 => typst_pdf::PdfStandard::A_4,
PdfStandard::A_4f => typst_pdf::PdfStandard::A_4f, PdfStandard::A_4f => typst_pdf::PdfStandard::A_4f,
PdfStandard::A_4e => typst_pdf::PdfStandard::A_4e, PdfStandard::A_4e => typst_pdf::PdfStandard::A_4e,
PdfStandard::Ua_1 => typst_pdf::PdfStandard::Ua_1,
} }
} }
} }

View File

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

View File

@ -3,9 +3,8 @@ use std::fmt::Write;
use typst_library::diag::{bail, At, SourceResult, StrResult}; use typst_library::diag::{bail, At, SourceResult, StrResult};
use typst_library::foundations::Repr; use typst_library::foundations::Repr;
use typst_library::html::{ use typst_library::html::{
attr, charsets, tag, HtmlDocument, HtmlElement, HtmlNode, HtmlTag, attr, charsets, tag, HtmlDocument, HtmlElement, HtmlFrame, HtmlNode, HtmlTag,
}; };
use typst_library::layout::Frame;
use typst_syntax::Span; use typst_syntax::Span;
/// Encodes an HTML document into a string. /// Encodes an HTML document into a string.
@ -304,9 +303,15 @@ fn write_escape(w: &mut Writer, c: char) -> StrResult<()> {
} }
/// Encode a laid out frame into the writer. /// Encode a laid out frame into the writer.
fn write_frame(w: &mut Writer, frame: &Frame) { fn write_frame(w: &mut Writer, frame: &HtmlFrame) {
// FIXME: This string replacement is obviously a hack. // FIXME: This string replacement is obviously a hack.
let svg = typst_svg::svg_frame(frame) let svg = typst_svg::svg_frame(&frame.inner).replace(
.replace("<svg class", "<svg style=\"overflow: visible;\" class"); "<svg class",
&format!(
"<svg style=\"overflow: visible; width: {}em; height: {}em;\" class",
frame.inner.width() / frame.text_size,
frame.inner.height() / frame.text_size,
),
);
w.buf.push_str(&svg); w.buf.push_str(&svg);
} }

View File

@ -9,7 +9,7 @@ use typst_library::diag::{bail, warning, At, SourceResult};
use typst_library::engine::{Engine, Route, Sink, Traced}; use typst_library::engine::{Engine, Route, Sink, Traced};
use typst_library::foundations::{Content, StyleChain, Target, TargetElem}; use typst_library::foundations::{Content, StyleChain, Target, TargetElem};
use typst_library::html::{ use typst_library::html::{
attr, tag, FrameElem, HtmlDocument, HtmlElem, HtmlElement, HtmlNode, attr, tag, FrameElem, HtmlDocument, HtmlElem, HtmlElement, HtmlFrame, HtmlNode,
}; };
use typst_library::introspection::{ use typst_library::introspection::{
Introspector, Locator, LocatorLink, SplitLocator, TagElem, Introspector, Locator, LocatorLink, SplitLocator, TagElem,
@ -246,7 +246,10 @@ fn handle(
styles.chain(&style), styles.chain(&style),
Region::new(Size::splat(Abs::inf()), Axes::splat(false)), Region::new(Size::splat(Abs::inf()), Axes::splat(false)),
)?; )?;
output.push(HtmlNode::Frame(frame)); output.push(HtmlNode::Frame(HtmlFrame {
inner: frame,
text_size: TextElem::size_in(styles),
}));
} else { } else {
engine.sink.warn(warning!( engine.sink.warn(warning!(
child.span(), child.span(),

View File

@ -448,7 +448,7 @@ fn field_access_completions(
match value { match value {
Value::Symbol(symbol) => { Value::Symbol(symbol) => {
for modifier in symbol.modifiers() { for modifier in symbol.modifiers() {
if let Ok(modified) = symbol.clone().modified(modifier) { if let Ok(modified) = symbol.clone().modified((), modifier) {
ctx.completions.push(Completion { ctx.completions.push(Completion {
kind: CompletionKind::Symbol(modified.get()), kind: CompletionKind::Symbol(modified.get()),
label: modifier.into(), label: modifier.into(),

View File

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

View File

@ -93,7 +93,7 @@ impl Item<'_, '_> {
Self::Frame(frame, _) => { Self::Frame(frame, _) => {
frame.size().is_zero() frame.size().is_zero()
&& frame.items().all(|(_, item)| { && frame.items().all(|(_, item)| {
matches!(item, FrameItem::Link(_, _) | FrameItem::Tag(_)) matches!(item, FrameItem::Link(..) | FrameItem::Tag(_))
}) })
} }
Self::Placed(_, placed) => !placed.float, Self::Placed(_, placed) => !placed.float,

View File

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

View File

@ -13,6 +13,7 @@ use typst_library::layout::{
VAlignment, VAlignment,
}; };
use typst_library::model::Numbering; use typst_library::model::Numbering;
use typst_library::pdf::ArtifactKind;
use typst_library::routines::{Pair, Routines}; use typst_library::routines::{Pair, Routines};
use typst_library::text::{LocalName, TextElem}; use typst_library::text::{LocalName, TextElem};
use typst_library::visualize::Paint; use typst_library::visualize::Paint;
@ -200,6 +201,11 @@ fn layout_page_run_impl(
// Layout marginals. // Layout marginals.
let mut layouted = Vec::with_capacity(fragment.len()); let mut layouted = Vec::with_capacity(fragment.len());
let header = header.as_ref().map(|h| h.clone().artifact(ArtifactKind::Header));
let footer = footer.as_ref().map(|f| f.clone().artifact(ArtifactKind::Footer));
let background = background.as_ref().map(|b| b.clone().artifact(ArtifactKind::Page));
for inner in fragment { for inner in fragment {
let header_size = Size::new(inner.width(), margin.top - header_ascent); let header_size = Size::new(inner.width(), margin.top - header_ascent);
let footer_size = Size::new(inner.width(), margin.bottom - footer_descent); let footer_size = Size::new(inner.width(), margin.bottom - footer_descent);
@ -210,9 +216,9 @@ fn layout_page_run_impl(
fill: fill.clone(), fill: fill.clone(),
numbering: numbering.clone(), numbering: numbering.clone(),
supplement: supplement.clone(), supplement: supplement.clone(),
header: layout_marginal(header, header_size, Alignment::BOTTOM)?, header: layout_marginal(&header, header_size, Alignment::BOTTOM)?,
footer: layout_marginal(footer, footer_size, Alignment::TOP)?, footer: layout_marginal(&footer, footer_size, Alignment::TOP)?,
background: layout_marginal(background, full_size, mid)?, background: layout_marginal(&background, full_size, mid)?,
foreground: layout_marginal(foreground, full_size, mid)?, foreground: layout_marginal(foreground, full_size, mid)?,
margin, margin,
binding, binding,

View File

@ -16,12 +16,13 @@ use crate::diag::{SourceResult, StrResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
elem, func, scope, ty, Context, Dict, Element, Fields, IntoValue, Label, elem, func, scope, ty, Context, Dict, Element, Fields, IntoValue, Label,
NativeElement, Recipe, RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles, NativeElement, Recipe, RecipeIndex, Repr, Selector, Show, Str, Style, StyleChain,
Value, Styles, Value,
}; };
use crate::introspection::Location; use crate::introspection::{Locatable, Location};
use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides}; use crate::layout::{AlignElem, Alignment, Axes, Length, MoveElem, PadElem, Rel, Sides};
use crate::model::{Destination, EmphElem, LinkElem, StrongElem}; use crate::model::{Destination, EmphElem, LinkElem, StrongElem};
use crate::pdf::{ArtifactElem, ArtifactKind};
use crate::text::UnderlineElem; use crate::text::UnderlineElem;
/// A piece of document content. /// A piece of document content.
@ -503,8 +504,12 @@ impl Content {
} }
/// Link the content somewhere. /// Link the content somewhere.
pub fn linked(self, dest: Destination) -> Self { pub fn linked(self, dest: Destination, alt: Option<EcoString>) -> Self {
self.styled(LinkElem::set_current(Some(dest))) let span = self.span();
LinkMarker::new(self, dest.clone(), alt)
.pack()
.spanned(span)
.styled(LinkElem::set_current(Some(dest)))
} }
/// Set alignments for this content. /// Set alignments for this content.
@ -533,6 +538,12 @@ impl Content {
.pack() .pack()
.spanned(span) .spanned(span)
} }
/// Link the content somewhere.
pub fn artifact(self, kind: ArtifactKind) -> Self {
let span = self.span();
ArtifactElem::new(self).with_kind(kind).pack().spanned(span)
}
} }
#[scope] #[scope]
@ -980,6 +991,24 @@ pub trait PlainText {
fn plain_text(&self, text: &mut EcoString); fn plain_text(&self, text: &mut EcoString);
} }
/// An element that associates the body of a link with the destination.
#[elem(Show, Locatable)]
pub struct LinkMarker {
/// The content.
#[required]
pub body: Content,
#[required]
pub dest: Destination,
#[required]
pub alt: Option<EcoString>,
}
impl Show for Packed<LinkMarker> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(self.body.clone())
}
}
/// An error arising when trying to access a field of content. /// An error arising when trying to access a field of content.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum FieldAccessError { pub enum FieldAccessError {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ use typst_utils::{PicoStr, ResolvedPicoStr};
use crate::diag::{bail, HintedStrResult, StrResult}; use crate::diag::{bail, HintedStrResult, StrResult};
use crate::foundations::{cast, Dict, Repr, Str}; use crate::foundations::{cast, Dict, Repr, Str};
use crate::introspection::{Introspector, Tag}; use crate::introspection::{Introspector, Tag};
use crate::layout::Frame; use crate::layout::{Abs, Frame};
use crate::model::DocumentInfo; use crate::model::DocumentInfo;
/// An HTML document. /// An HTML document.
@ -30,8 +30,8 @@ pub enum HtmlNode {
Text(EcoString, Span), Text(EcoString, Span),
/// Another element. /// Another element.
Element(HtmlElement), Element(HtmlElement),
/// A frame that will be displayed as an embedded SVG. /// Layouted content that will be embedded into HTML as an SVG.
Frame(Frame), Frame(HtmlFrame),
} }
impl HtmlNode { impl HtmlNode {
@ -263,6 +263,17 @@ cast! {
v: Str => Self::intern(&v)?, v: Str => Self::intern(&v)?,
} }
/// Layouted content that will be embedded into HTML as an SVG.
#[derive(Debug, Clone, Hash)]
pub struct HtmlFrame {
/// The frame that will be displayed as an SVG.
pub inner: Frame,
/// The text size where the frame was defined. This is used to size the
/// frame with em units to make text in and outside of the frame sized
/// consistently.
pub text_size: Abs,
}
/// Defines syntactical properties of HTML tags, attributes, and text. /// Defines syntactical properties of HTML tags, attributes, and text.
pub mod charsets { pub mod charsets {
/// Check whether a character is in a tag name. /// Check whether a character is in a tag name.

View File

@ -8,6 +8,7 @@ pub use self::dom::*;
use ecow::EcoString; use ecow::EcoString;
use crate::foundations::{elem, Content, Module, Scope}; use crate::foundations::{elem, Content, Module, Scope};
use crate::introspection::Locatable;
/// Create a module with all HTML definitions. /// Create a module with all HTML definitions.
pub fn module() -> Module { pub fn module() -> Module {
@ -40,7 +41,7 @@ pub fn module() -> Module {
/// A div with _Typst content_ inside! /// A div with _Typst content_ inside!
/// ] /// ]
/// ``` /// ```
#[elem(name = "elem")] #[elem(name = "elem", Locatable)]
pub struct HtmlElem { pub struct HtmlElem {
/// The element's tag. /// The element's tag.
#[required] #[required]

View File

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

View File

@ -13,6 +13,7 @@ use crate::foundations::{
cast, elem, scope, Array, CastInfo, Content, Context, Fold, FromValue, Func, cast, elem, scope, Array, CastInfo, Content, Context, Fold, FromValue, Func,
IntoValue, NativeElement, Packed, Reflect, Resolve, Show, Smart, StyleChain, Value, IntoValue, NativeElement, Packed, Reflect, Resolve, Show, Smart, StyleChain, Value,
}; };
use crate::introspection::Locatable;
use crate::layout::{ use crate::layout::{
Alignment, BlockElem, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, Sizing, Alignment, BlockElem, Length, OuterHAlignment, OuterVAlignment, Rel, Sides, Sizing,
}; };
@ -136,7 +137,7 @@ use crate::visualize::{Paint, Stroke};
/// ///
/// Furthermore, strokes of a repeated grid header or footer will take /// Furthermore, strokes of a repeated grid header or footer will take
/// precedence over regular cell strokes. /// precedence over regular cell strokes.
#[elem(scope, Show)] #[elem(scope, Locatable, Show)]
pub struct GridElem { pub struct GridElem {
/// The column sizes. /// The column sizes.
/// ///
@ -462,7 +463,7 @@ impl TryFrom<Content> for GridItem {
/// If `repeat` is set to `true`, the header will be repeated across pages. For /// If `repeat` is set to `true`, the header will be repeated across pages. For
/// an example, refer to the [`table.header`]($table.header) element and the /// an example, refer to the [`table.header`]($table.header) element and the
/// [`grid.stroke`]($grid.stroke) parameter. /// [`grid.stroke`]($grid.stroke) parameter.
#[elem(name = "header", title = "Grid Header")] #[elem(name = "header", title = "Grid Header", Locatable)]
pub struct GridHeader { pub struct GridHeader {
/// Whether this header should be repeated across pages. /// Whether this header should be repeated across pages.
#[default(true)] #[default(true)]
@ -490,7 +491,7 @@ pub struct GridHeader {
/// itself on every page of the table. /// itself on every page of the table.
/// ///
/// No other grid cells may be placed after the footer. /// No other grid cells may be placed after the footer.
#[elem(name = "footer", title = "Grid Footer")] #[elem(name = "footer", title = "Grid Footer", Locatable)]
pub struct GridFooter { pub struct GridFooter {
/// Whether this footer should be repeated across pages. /// Whether this footer should be repeated across pages.
#[default(true)] #[default(true)]
@ -657,7 +658,7 @@ pub struct GridVLine {
/// which allows you, for example, to apply styles based on a cell's position. /// which allows you, for example, to apply styles based on a cell's position.
/// Refer to the examples of the [`table.cell`]($table.cell) element to learn /// Refer to the examples of the [`table.cell`]($table.cell) element to learn
/// more about this. /// more about this.
#[elem(name = "cell", title = "Grid Cell", Show)] #[elem(name = "cell", title = "Grid Cell", Locatable, Show)]
pub struct GridCell { pub struct GridCell {
/// The cell's body. /// The cell's body.
#[required] #[required]

View File

@ -22,6 +22,7 @@ use typst_syntax::Span;
use typst_utils::NonZeroExt; use typst_utils::NonZeroExt;
use crate::introspection::SplitLocator; use crate::introspection::SplitLocator;
use crate::model::TableCellKind;
/// Convert a grid to a cell grid. /// Convert a grid to a cell grid.
#[typst_macros::time(span = elem.span())] #[typst_macros::time(span = elem.span())]
@ -217,6 +218,7 @@ impl ResolvableCell for Packed<TableCell> {
breakable: bool, breakable: bool,
locator: Locator<'a>, locator: Locator<'a>,
styles: StyleChain, styles: StyleChain,
kind: Smart<TableCellKind>,
) -> Cell<'a> { ) -> Cell<'a> {
let cell = &mut *self; let cell = &mut *self;
let colspan = cell.colspan(styles); let colspan = cell.colspan(styles);
@ -224,6 +226,8 @@ impl ResolvableCell for Packed<TableCell> {
let breakable = cell.breakable(styles).unwrap_or(breakable); let breakable = cell.breakable(styles).unwrap_or(breakable);
let fill = cell.fill(styles).unwrap_or_else(|| fill.clone()); let fill = cell.fill(styles).unwrap_or_else(|| fill.clone());
let kind = cell.kind(styles).or(kind);
let cell_stroke = cell.stroke(styles); let cell_stroke = cell.stroke(styles);
let stroke_overridden = let stroke_overridden =
cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_)))); cell_stroke.as_ref().map(|side| matches!(side, Some(Some(_))));
@ -267,6 +271,7 @@ impl ResolvableCell for Packed<TableCell> {
}), }),
); );
cell.push_breakable(Smart::Custom(breakable)); cell.push_breakable(Smart::Custom(breakable));
cell.push_kind(kind);
Cell { Cell {
body: self.pack(), body: self.pack(),
locator, locator,
@ -312,6 +317,7 @@ impl ResolvableCell for Packed<GridCell> {
breakable: bool, breakable: bool,
locator: Locator<'a>, locator: Locator<'a>,
styles: StyleChain, styles: StyleChain,
_: Smart<TableCellKind>,
) -> Cell<'a> { ) -> Cell<'a> {
let cell = &mut *self; let cell = &mut *self;
let colspan = cell.colspan(styles); let colspan = cell.colspan(styles);
@ -522,6 +528,7 @@ pub trait ResolvableCell {
breakable: bool, breakable: bool,
locator: Locator<'a>, locator: Locator<'a>,
styles: StyleChain, styles: StyleChain,
kind: Smart<TableCellKind>,
) -> Cell<'a>; ) -> Cell<'a>;
/// Returns this cell's column override. /// Returns this cell's column override.
@ -1206,8 +1213,12 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
// a non-empty row. // a non-empty row.
let mut first_available_row = 0; let mut first_available_row = 0;
let mut cell_kind: Smart<TableCellKind> = Smart::Auto;
let (header_footer_items, simple_item) = match child { let (header_footer_items, simple_item) = match child {
ResolvableGridChild::Header { repeat, level, span, items, .. } => { ResolvableGridChild::Header { repeat, level, span, items, .. } => {
cell_kind = Smart::Custom(TableCellKind::Header);
row_group_data = Some(RowGroupData { row_group_data = Some(RowGroupData {
range: None, range: None,
span, span,
@ -1239,6 +1250,8 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
bail!(span, "cannot have more than one footer"); bail!(span, "cannot have more than one footer");
} }
cell_kind = Smart::Custom(TableCellKind::Footer);
row_group_data = Some(RowGroupData { row_group_data = Some(RowGroupData {
range: None, range: None,
span, span,
@ -1447,7 +1460,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
// Let's resolve the cell so it can determine its own fields // Let's resolve the cell so it can determine its own fields
// based on its final position. // based on its final position.
let cell = self.resolve_cell(cell, x, y, rowspan, cell_span)?; let cell = self.resolve_cell(cell, x, y, rowspan, cell_span, cell_kind)?;
if largest_index >= resolved_cells.len() { if largest_index >= resolved_cells.len() {
// Ensure the length of the vector of resolved cells is // Ensure the length of the vector of resolved cells is
@ -1542,6 +1555,10 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
// and footers without having to loop through them each time. // and footers without having to loop through them each time.
// Cells themselves, unfortunately, still have to. // Cells themselves, unfortunately, still have to.
assert!(resolved_cells[*local_auto_index].is_none()); assert!(resolved_cells[*local_auto_index].is_none());
let kind = match row_group.kind {
RowGroupKind::Header => TableCellKind::Header,
RowGroupKind::Footer => TableCellKind::Header,
};
resolved_cells[*local_auto_index] = resolved_cells[*local_auto_index] =
Some(Entry::Cell(self.resolve_cell( Some(Entry::Cell(self.resolve_cell(
T::default(), T::default(),
@ -1549,6 +1566,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
first_available_row, first_available_row,
1, 1,
Span::detached(), Span::detached(),
Smart::Custom(kind),
)?)); )?));
group_start..group_end group_start..group_end
@ -1673,6 +1691,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
y, y,
1, 1,
Span::detached(), Span::detached(),
Smart::Auto,
)?)) )?))
} }
}) })
@ -1918,6 +1937,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
y: usize, y: usize,
rowspan: usize, rowspan: usize,
cell_span: Span, cell_span: Span,
kind: Smart<TableCellKind>,
) -> SourceResult<Cell<'x>> ) -> SourceResult<Cell<'x>>
where where
T: ResolvableCell + Default, T: ResolvableCell + Default,
@ -1954,6 +1974,7 @@ impl<'x> CellGridResolver<'_, '_, 'x> {
breakable, breakable,
self.locator.next(&cell_span), self.locator.next(&cell_span),
self.styles, self.styles,
kind,
)) ))
} }
} }

View File

@ -1,6 +1,7 @@
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{elem, Content, Packed, Show, StyleChain}; use crate::foundations::{elem, Content, Packed, Show, StyleChain};
use crate::introspection::Locatable;
/// Hides content without affecting layout. /// Hides content without affecting layout.
/// ///
@ -14,7 +15,7 @@ use crate::foundations::{elem, Content, Packed, Show, StyleChain};
/// Hello Jane \ /// Hello Jane \
/// #hide[Hello] Joe /// #hide[Hello] Joe
/// ``` /// ```
#[elem(Show)] #[elem(Locatable, Show)]
pub struct HideElem { pub struct HideElem {
/// The content to hide. /// The content to hide.
#[required] #[required]

View File

@ -1,6 +1,7 @@
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain}; use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain};
use crate::introspection::Locatable;
use crate::layout::{BlockElem, Length}; use crate::layout::{BlockElem, Length};
/// Repeats content to the available space. /// Repeats content to the available space.
@ -24,7 +25,7 @@ use crate::layout::{BlockElem, Length};
/// Berlin, the 22nd of December, 2022 /// Berlin, the 22nd of December, 2022
/// ] /// ]
/// ``` /// ```
#[elem(Show)] #[elem(Locatable, Show)]
pub struct RepeatElem { pub struct RepeatElem {
/// The content to repeat. /// The content to repeat.
#[required] #[required]

View File

@ -28,6 +28,7 @@ use typst_utils::singleton;
use unicode_math_class::MathClass; use unicode_math_class::MathClass;
use crate::foundations::{elem, Content, Module, NativeElement, Scope}; use crate::foundations::{elem, Content, Module, NativeElement, Scope};
use crate::introspection::Locatable;
use crate::layout::{Em, HElem}; use crate::layout::{Em, HElem};
use crate::text::TextElem; use crate::text::TextElem;
@ -109,7 +110,7 @@ pub fn module() -> Module {
pub trait Mathy {} pub trait Mathy {}
/// A math alignment point: `&`, `&&`. /// A math alignment point: `&`, `&&`.
#[elem(title = "Alignment Point", Mathy)] #[elem(title = "Alignment Point", Mathy, Locatable)]
pub struct AlignPointElem {} pub struct AlignPointElem {}
impl AlignPointElem { impl AlignPointElem {
@ -136,7 +137,7 @@ impl AlignPointElem {
/// ///
/// $x loves y and y loves 5$ /// $x loves y and y loves 5$
/// ``` /// ```
#[elem(Mathy)] #[elem(Mathy, Locatable)]
pub struct ClassElem { pub struct ClassElem {
/// The class to apply to the content. /// The class to apply to the content.
#[required] #[required]

View File

@ -1,6 +1,7 @@
use typst_syntax::Span; use typst_syntax::Span;
use crate::foundations::{elem, func, Content, NativeElement}; use crate::foundations::{elem, func, Content, NativeElement};
use crate::introspection::Locatable;
use crate::math::Mathy; use crate::math::Mathy;
/// A square root. /// A square root.
@ -22,7 +23,7 @@ pub fn sqrt(
/// ```example /// ```example
/// $ root(3, x) $ /// $ root(3, x) $
/// ``` /// ```
#[elem(Mathy)] #[elem(Mathy, Locatable)]
pub struct RootElem { pub struct RootElem {
/// Which root of the radicand to take. /// Which root of the radicand to take.
#[positional] #[positional]

View File

@ -321,7 +321,11 @@ impl Bibliography {
for d in data.iter() { for d in data.iter() {
let library = decode_library(d)?; let library = decode_library(d)?;
for entry in library { for entry in library {
match map.entry(Label::new(PicoStr::intern(entry.key()))) { let label = Label::new(PicoStr::intern(entry.key()))
.ok_or("bibliography contains entry with empty key")
.at(d.source.span)?;
match map.entry(label) {
indexmap::map::Entry::Vacant(vacant) => { indexmap::map::Entry::Vacant(vacant) => {
vacant.insert(entry); vacant.insert(entry);
} }
@ -592,7 +596,7 @@ impl Works {
/// Context for generating the bibliography. /// Context for generating the bibliography.
struct Generator<'a> { struct Generator<'a> {
/// The routines that is used to evaluate mathematical material in citations. /// The routines that are used to evaluate mathematical material in citations.
routines: &'a Routines, routines: &'a Routines,
/// The world that is used to evaluate mathematical material in citations. /// The world that is used to evaluate mathematical material in citations.
world: Tracked<'a, dyn World + 'a>, world: Tracked<'a, dyn World + 'a>,
@ -609,7 +613,7 @@ struct Generator<'a> {
/// Details about a group of merged citations. All citations are put into groups /// Details about a group of merged citations. All citations are put into groups
/// of adjacent ones (e.g., `@foo @bar` will merge into a group of length two). /// of adjacent ones (e.g., `@foo @bar` will merge into a group of length two).
/// Even single citations will be put into groups of length ones. /// Even single citations will be put into groups of length one.
struct GroupInfo { struct GroupInfo {
/// The group's location. /// The group's location.
location: Location, location: Location,
@ -873,7 +877,8 @@ impl<'a> Generator<'a> {
renderer.display_elem_child(elem, &mut None, false)?; renderer.display_elem_child(elem, &mut None, false)?;
if let Some(location) = first_occurrences.get(item.key.as_str()) { if let Some(location) = first_occurrences.get(item.key.as_str()) {
let dest = Destination::Location(*location); let dest = Destination::Location(*location);
content = content.linked(dest); // TODO: accept user supplied alt text
content = content.linked(dest, None);
} }
StrResult::Ok(content) StrResult::Ok(content)
}) })
@ -1008,7 +1013,8 @@ impl ElemRenderer<'_> {
if let Some(hayagriva::ElemMeta::Entry(i)) = elem.meta { if let Some(hayagriva::ElemMeta::Entry(i)) = elem.meta {
if let Some(location) = (self.link)(i) { if let Some(location) = (self.link)(i) {
let dest = Destination::Location(location); let dest = Destination::Location(location);
content = content.linked(dest); // TODO: accept user supplied alt text
content = content.linked(dest, None);
} }
} }

View File

@ -43,7 +43,7 @@ use crate::text::{Lang, Region, TextElem};
/// This function indirectly has dedicated syntax. [References]($ref) can be /// This function indirectly has dedicated syntax. [References]($ref) can be
/// used to cite works from the bibliography. The label then corresponds to the /// used to cite works from the bibliography. The label then corresponds to the
/// citation key. /// citation key.
#[elem(Synthesize)] #[elem(Locatable, Synthesize)]
pub struct CiteElem { pub struct CiteElem {
/// The citation key that identifies the entry in the bibliography that /// The citation key that identifies the entry in the bibliography that
/// shall be cited, as a label. /// shall be cited, as a label.

View File

@ -4,6 +4,7 @@ use crate::foundations::{
elem, Content, NativeElement, Packed, Show, StyleChain, TargetElem, elem, Content, NativeElement, Packed, Show, StyleChain, TargetElem,
}; };
use crate::html::{tag, HtmlElem}; use crate::html::{tag, HtmlElem};
use crate::introspection::Locatable;
use crate::text::{ItalicToggle, TextElem}; use crate::text::{ItalicToggle, TextElem};
/// Emphasizes content by toggling italics. /// Emphasizes content by toggling italics.
@ -29,7 +30,7 @@ use crate::text::{ItalicToggle, TextElem};
/// This function also has dedicated syntax: To emphasize content, simply /// This function also has dedicated syntax: To emphasize content, simply
/// enclose it in underscores (`_`). Note that this only works at word /// enclose it in underscores (`_`). Note that this only works at word
/// boundaries. To emphasize part of a word, you have to use the function. /// boundaries. To emphasize part of a word, you have to use the function.
#[elem(title = "Emphasis", keywords = ["italic"], Show)] #[elem(title = "Emphasis", keywords = ["italic"], Locatable, Show)]
pub struct EmphElem { pub struct EmphElem {
/// The content to emphasize. /// The content to emphasize.
#[required] #[required]

View File

@ -10,6 +10,7 @@ use crate::foundations::{
Styles, TargetElem, Styles, TargetElem,
}; };
use crate::html::{attr, tag, HtmlElem}; use crate::html::{attr, tag, HtmlElem};
use crate::introspection::Locatable;
use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem}; use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem};
use crate::model::{ use crate::model::{
ListItemLike, ListLike, Numbering, NumberingPattern, ParElem, ParbreakElem, ListItemLike, ListLike, Numbering, NumberingPattern, ParElem, ParbreakElem,
@ -71,7 +72,7 @@ use crate::model::{
/// Enumeration items can contain multiple paragraphs and other block-level /// Enumeration items can contain multiple paragraphs and other block-level
/// content. All content that is indented more than an item's marker becomes /// content. All content that is indented more than an item's marker becomes
/// part of that item. /// part of that item.
#[elem(scope, title = "Numbered List", Show)] #[elem(scope, title = "Numbered List", Locatable, Show)]
pub struct EnumElem { pub struct EnumElem {
/// Defines the default [spacing]($enum.spacing) of the enumeration. If it /// Defines the default [spacing]($enum.spacing) of the enumeration. If it
/// is `{false}`, the items are spaced apart with /// is `{false}`, the items are spaced apart with
@ -271,7 +272,7 @@ impl Show for Packed<EnumElem> {
} }
/// An enumeration item. /// An enumeration item.
#[elem(name = "item", title = "Numbered List Item")] #[elem(name = "item", title = "Numbered List Item", Locatable)]
pub struct EnumItem { pub struct EnumItem {
/// The item's number. /// The item's number.
#[positional] #[positional]

View File

@ -473,7 +473,7 @@ impl Outlinable for Packed<FigureElem> {
/// caption: [A rectangle], /// caption: [A rectangle],
/// ) /// )
/// ``` /// ```
#[elem(name = "caption", Synthesize, Show)] #[elem(name = "caption", Locatable, Synthesize, Show)]
pub struct FigureCaption { pub struct FigureCaption {
/// The caption's position in the figure. Either `{top}` or `{bottom}`. /// The caption's position in the figure. Either `{top}` or `{bottom}`.
/// ///

View File

@ -147,7 +147,8 @@ impl Show for Packed<FootnoteElem> {
let sup = SuperElem::new(num).pack().spanned(span); let sup = SuperElem::new(num).pack().spanned(span);
let loc = loc.variant(1); let loc = loc.variant(1);
// Add zero-width weak spacing to make the footnote "sticky". // Add zero-width weak spacing to make the footnote "sticky".
Ok(HElem::hole().pack() + sup.linked(Destination::Location(loc))) // TODO: accept user supplied alt text
Ok(HElem::hole().pack() + sup.linked(Destination::Location(loc), None))
} }
} }
@ -192,7 +193,7 @@ cast! {
/// page run is a sequence of pages without an explicit pagebreak in between). /// page run is a sequence of pages without an explicit pagebreak in between).
/// For this reason, set and show rules for footnote entries should be defined /// For this reason, set and show rules for footnote entries should be defined
/// before any page content, typically at the very start of the document. /// before any page content, typically at the very start of the document.
#[elem(name = "entry", title = "Footnote Entry", Show, ShowSet)] #[elem(name = "entry", title = "Footnote Entry", Locatable, Show, ShowSet)]
pub struct FootnoteEntry { pub struct FootnoteEntry {
/// The footnote for this entry. Its location can be used to determine /// The footnote for this entry. Its location can be used to determine
/// the footnote counter state. /// the footnote counter state.
@ -296,7 +297,8 @@ impl Show for Packed<FootnoteEntry> {
let sup = SuperElem::new(num) let sup = SuperElem::new(num)
.pack() .pack()
.spanned(span) .spanned(span)
.linked(Destination::Location(loc)) // TODO: accept user supplied alt text
.linked(Destination::Location(loc), None)
.located(loc.variant(1)); .located(loc.variant(1));
Ok(Content::sequence([ Ok(Content::sequence([

View File

@ -9,7 +9,7 @@ use crate::foundations::{
StyleChain, Styles, TargetElem, StyleChain, Styles, TargetElem,
}; };
use crate::html::{attr, tag, HtmlElem}; use crate::html::{attr, tag, HtmlElem};
use crate::introspection::Location; use crate::introspection::{Locatable, Location};
use crate::layout::Position; use crate::layout::Position;
use crate::text::TextElem; use crate::text::TextElem;
@ -38,8 +38,11 @@ use crate::text::TextElem;
/// # Syntax /// # Syntax
/// This function also has dedicated syntax: Text that starts with `http://` or /// This function also has dedicated syntax: Text that starts with `http://` or
/// `https://` is automatically turned into a link. /// `https://` is automatically turned into a link.
#[elem(Show)] #[elem(Locatable, Show)]
pub struct LinkElem { pub struct LinkElem {
/// A text describing the link.
pub alt: Option<EcoString>,
/// The destination the link points to. /// The destination the link points to.
/// ///
/// - To link to web pages, `dest` should be a valid URL string. If the URL /// - To link to web pages, `dest` should be a valid URL string. If the URL
@ -123,12 +126,13 @@ impl Show for Packed<LinkElem> {
body body
} }
} else { } else {
let alt = self.alt(styles);
match &self.dest { match &self.dest {
LinkTarget::Dest(dest) => body.linked(dest.clone()), LinkTarget::Dest(dest) => body.linked(dest.clone(), alt),
LinkTarget::Label(label) => { LinkTarget::Label(label) => {
let elem = engine.introspector.query_label(*label).at(self.span())?; let elem = engine.introspector.query_label(*label).at(self.span())?;
let dest = Destination::Location(elem.location().unwrap()); let dest = Destination::Location(elem.location().unwrap());
body.clone().linked(dest) body.linked(dest, alt)
} }
} }
}) })

View File

@ -7,6 +7,7 @@ use crate::foundations::{
Smart, StyleChain, Styles, TargetElem, Value, Smart, StyleChain, Styles, TargetElem, Value,
}; };
use crate::html::{tag, HtmlElem}; use crate::html::{tag, HtmlElem};
use crate::introspection::Locatable;
use crate::layout::{BlockElem, Em, Length, VElem}; use crate::layout::{BlockElem, Em, Length, VElem};
use crate::model::{ParElem, ParbreakElem}; use crate::model::{ParElem, ParbreakElem};
use crate::text::TextElem; use crate::text::TextElem;
@ -42,7 +43,7 @@ use crate::text::TextElem;
/// followed by a space to create a list item. A list item can contain multiple /// followed by a space to create a list item. A list item can contain multiple
/// paragraphs and other block-level content. All content that is indented /// paragraphs and other block-level content. All content that is indented
/// more than an item's marker becomes part of that item. /// more than an item's marker becomes part of that item.
#[elem(scope, title = "Bullet List", Show)] #[elem(scope, title = "Bullet List", Locatable, Show)]
pub struct ListElem { pub struct ListElem {
/// Defines the default [spacing]($list.spacing) of the list. If it is /// Defines the default [spacing]($list.spacing) of the list. If it is
/// `{false}`, the items are spaced apart with /// `{false}`, the items are spaced apart with
@ -178,7 +179,7 @@ impl Show for Packed<ListElem> {
} }
/// A bullet list item. /// A bullet list item.
#[elem(name = "item", title = "Bullet List Item")] #[elem(name = "item", title = "Bullet List Item", Locatable)]
pub struct ListItem { pub struct ListItem {
/// The item's body. /// The item's body.
#[required] #[required]

View File

@ -2,6 +2,7 @@ use std::num::NonZeroUsize;
use std::str::FromStr; use std::str::FromStr;
use comemo::{Track, Tracked}; use comemo::{Track, Tracked};
use ecow::eco_format;
use smallvec::SmallVec; use smallvec::SmallVec;
use typst_syntax::Span; use typst_syntax::Span;
use typst_utils::{Get, NonZeroExt}; use typst_utils::{Get, NonZeroExt};
@ -17,8 +18,8 @@ use crate::introspection::{
Counter, CounterKey, Introspector, Locatable, Location, Locator, LocatorLink, Counter, CounterKey, Introspector, Locatable, Location, Locator, LocatorLink,
}; };
use crate::layout::{ use crate::layout::{
Abs, Axes, BlockBody, BlockElem, BoxElem, Dir, Em, Fr, HElem, Length, Region, Rel, Abs, Axes, BlockBody, BlockElem, BoxElem, Dir, Em, Fr, HElem, Length, PageElem,
RepeatElem, Sides, Region, Rel, RepeatElem, Sides,
}; };
use crate::math::EquationElem; use crate::math::EquationElem;
use crate::model::{Destination, HeadingElem, NumberingPattern, ParElem, Refable}; use crate::model::{Destination, HeadingElem, NumberingPattern, ParElem, Refable};
@ -272,6 +273,7 @@ impl Show for Packed<OutlineElem> {
let depth = self.depth(styles).unwrap_or(NonZeroUsize::MAX); let depth = self.depth(styles).unwrap_or(NonZeroUsize::MAX);
// Build the outline entries. // Build the outline entries.
let mut entries = vec![];
for elem in elems { for elem in elems {
let Some(outlinable) = elem.with::<dyn Outlinable>() else { let Some(outlinable) = elem.with::<dyn Outlinable>() else {
bail!(span, "cannot outline {}", elem.func().name()); bail!(span, "cannot outline {}", elem.func().name());
@ -280,10 +282,13 @@ impl Show for Packed<OutlineElem> {
let level = outlinable.level(); let level = outlinable.level();
if outlinable.outlined() && level <= depth { if outlinable.outlined() && level <= depth {
let entry = OutlineEntry::new(level, elem); let entry = OutlineEntry::new(level, elem);
seq.push(entry.pack().spanned(span)); entries.push(entry.pack().spanned(span));
} }
} }
// Wrap the entries into a marker for pdf tagging.
seq.push(OutlineBody::new(Content::sequence(entries)).pack());
Ok(Content::sequence(seq)) Ok(Content::sequence(seq))
} }
} }
@ -306,6 +311,19 @@ impl LocalName for Packed<OutlineElem> {
const KEY: &'static str = "outline"; const KEY: &'static str = "outline";
} }
/// Only used to mark
#[elem(Locatable, Show)]
pub struct OutlineBody {
#[required]
body: Content,
}
impl Show for Packed<OutlineBody> {
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(self.body.clone())
}
}
/// Defines how an outline is indented. /// Defines how an outline is indented.
#[derive(Debug, Clone, PartialEq, Hash)] #[derive(Debug, Clone, PartialEq, Hash)]
pub enum OutlineIndent { pub enum OutlineIndent {
@ -364,7 +382,7 @@ pub trait Outlinable: Refable {
/// With show-set and show rules on outline entries, you can richly customize /// With show-set and show rules on outline entries, you can richly customize
/// the outline's appearance. See the /// the outline's appearance. See the
/// [section on styling the outline]($outline/#styling-the-outline) for details. /// [section on styling the outline]($outline/#styling-the-outline) for details.
#[elem(scope, name = "entry", title = "Outline Entry", Show)] #[elem(scope, name = "entry", title = "Outline Entry", Locatable, Show)]
pub struct OutlineEntry { pub struct OutlineEntry {
/// The nesting level of this outline entry. Starts at `{1}` for top-level /// The nesting level of this outline entry. Starts at `{1}` for top-level
/// entries. /// entries.
@ -417,8 +435,19 @@ impl Show for Packed<OutlineEntry> {
let context = Context::new(None, Some(styles)); let context = Context::new(None, Some(styles));
let context = context.track(); let context = context.track();
// TODO: prefix should be wrapped in a `Lbl` structure element
let prefix = self.prefix(engine, context, span)?; let prefix = self.prefix(engine, context, span)?;
let inner = self.inner(engine, context, span)?; let body = self.body().at(span)?;
let page = self.page(engine, context, span)?;
let alt = {
// TODO: accept user supplied alt text
let prefix = prefix.as_ref().map(|p| p.plain_text()).unwrap_or_default();
let body = body.plain_text();
let page_str = PageElem::local_name_in(styles);
let page_nr = page.plain_text();
eco_format!("{prefix} \"{body}\", {page_str} {page_nr}")
};
let inner = self.inner(context, span, body, page)?;
let block = if self.element.is::<EquationElem>() { let block = if self.element.is::<EquationElem>() {
let body = prefix.unwrap_or_default() + inner; let body = prefix.unwrap_or_default() + inner;
BlockElem::new() BlockElem::new()
@ -430,7 +459,7 @@ impl Show for Packed<OutlineEntry> {
}; };
let loc = self.element_location().at(span)?; let loc = self.element_location().at(span)?;
Ok(block.linked(Destination::Location(loc))) Ok(block.linked(Destination::Location(loc), Some(alt)))
} }
} }
@ -565,9 +594,10 @@ impl OutlineEntry {
#[func(contextual)] #[func(contextual)]
pub fn inner( pub fn inner(
&self, &self,
engine: &mut Engine,
context: Tracked<Context>, context: Tracked<Context>,
span: Span, span: Span,
body: Content,
page: Content,
) -> SourceResult<Content> { ) -> SourceResult<Content> {
let styles = context.styles().at(span)?; let styles = context.styles().at(span)?;
@ -588,7 +618,7 @@ impl OutlineEntry {
seq.push(TextElem::packed("\u{202B}")); seq.push(TextElem::packed("\u{202B}"));
} }
seq.push(self.body().at(span)?); seq.push(body);
if rtl { if rtl {
// "Pop Directional Formatting" // "Pop Directional Formatting"
@ -613,7 +643,7 @@ impl OutlineEntry {
// Add the page number. The word joiner in front ensures that the page // Add the page number. The word joiner in front ensures that the page
// number doesn't stand alone in its line. // number doesn't stand alone in its line.
seq.push(TextElem::packed("\u{2060}")); seq.push(TextElem::packed("\u{2060}"));
seq.push(self.page(engine, context, span)?); seq.push(page);
Ok(Content::sequence(seq)) Ok(Content::sequence(seq))
} }

View File

@ -93,7 +93,7 @@ use crate::model::Numbering;
/// let $a$ be the smallest of the /// let $a$ be the smallest of the
/// three integers. Then, we ... /// three integers. Then, we ...
/// ``` /// ```
#[elem(scope, title = "Paragraph")] #[elem(scope, title = "Paragraph", Locatable)]
pub struct ParElem { pub struct ParElem {
/// The spacing between lines. /// The spacing between lines.
/// ///

View File

@ -343,7 +343,8 @@ fn show_reference(
content = supplement + TextElem::packed("\u{a0}") + content; content = supplement + TextElem::packed("\u{a0}") + content;
} }
Ok(content.linked(Destination::Location(loc))) // TODO: accept user supplied alt text
Ok(content.linked(Destination::Location(loc), None))
} }
/// Turn a reference into a citation. /// Turn a reference into a citation.

View File

@ -4,6 +4,7 @@ use crate::foundations::{
elem, Content, NativeElement, Packed, Show, StyleChain, TargetElem, elem, Content, NativeElement, Packed, Show, StyleChain, TargetElem,
}; };
use crate::html::{tag, HtmlElem}; use crate::html::{tag, HtmlElem};
use crate::introspection::Locatable;
use crate::text::{TextElem, WeightDelta}; use crate::text::{TextElem, WeightDelta};
/// Strongly emphasizes content by increasing the font weight. /// Strongly emphasizes content by increasing the font weight.
@ -24,7 +25,7 @@ use crate::text::{TextElem, WeightDelta};
/// simply enclose it in stars/asterisks (`*`). Note that this only works at /// simply enclose it in stars/asterisks (`*`). Note that this only works at
/// word boundaries. To strongly emphasize part of a word, you have to use the /// word boundaries. To strongly emphasize part of a word, you have to use the
/// function. /// function.
#[elem(title = "Strong Emphasis", keywords = ["bold", "weight"], Show)] #[elem(title = "Strong Emphasis", keywords = ["bold", "weight"], Locatable, Show)]
pub struct StrongElem { pub struct StrongElem {
/// The delta to apply on the font weight. /// The delta to apply on the font weight.
/// ///

View File

@ -1,6 +1,8 @@
use std::num::{NonZeroU32, NonZeroUsize}; use std::num::{NonZeroU32, NonZeroUsize};
use std::sync::Arc; use std::sync::Arc;
use ecow::EcoString;
use typst_macros::Cast;
use typst_utils::NonZeroExt; use typst_utils::NonZeroExt;
use crate::diag::{bail, HintedStrResult, HintedString, SourceResult}; use crate::diag::{bail, HintedStrResult, HintedString, SourceResult};
@ -10,7 +12,7 @@ use crate::foundations::{
TargetElem, TargetElem,
}; };
use crate::html::{attr, tag, HtmlAttrs, HtmlElem, HtmlTag}; use crate::html::{attr, tag, HtmlAttrs, HtmlElem, HtmlTag};
use crate::introspection::Locator; use crate::introspection::{Locatable, Locator};
use crate::layout::grid::resolve::{table_to_cellgrid, Cell, CellGrid, Entry}; use crate::layout::grid::resolve::{table_to_cellgrid, Cell, CellGrid, Entry};
use crate::layout::{ use crate::layout::{
show_grid_cell, Abs, Alignment, BlockElem, Celled, GridCell, GridFooter, GridHLine, show_grid_cell, Abs, Alignment, BlockElem, Celled, GridCell, GridFooter, GridHLine,
@ -121,7 +123,7 @@ use crate::visualize::{Paint, Stroke};
/// [Robert], b, a, b, /// [Robert], b, a, b,
/// ) /// )
/// ``` /// ```
#[elem(scope, Show, LocalName, Figurable)] #[elem(scope, Locatable, Show, LocalName, Figurable)]
pub struct TableElem { pub struct TableElem {
/// The column sizes. See the [grid documentation]($grid) for more /// The column sizes. See the [grid documentation]($grid) for more
/// information on track sizing. /// information on track sizing.
@ -237,6 +239,9 @@ pub struct TableElem {
#[default(Celled::Value(Sides::splat(Some(Abs::pt(5.0).into()))))] #[default(Celled::Value(Sides::splat(Some(Abs::pt(5.0).into()))))]
pub inset: Celled<Sides<Option<Rel<Length>>>>, pub inset: Celled<Sides<Option<Rel<Length>>>>,
// TODO: docs
pub summary: Option<EcoString>,
/// The contents of the table cells, plus any extra table lines specified /// The contents of the table cells, plus any extra table lines specified
/// with the [`table.hline`]($table.hline) and /// with the [`table.hline`]($table.hline) and
/// [`table.vline`]($table.vline) elements. /// [`table.vline`]($table.vline) elements.
@ -531,7 +536,7 @@ impl TryFrom<Content> for TableItem {
/// [7.34], [57], [2], /// [7.34], [57], [2],
/// ) /// )
/// ``` /// ```
#[elem(name = "header", title = "Table Header")] #[elem(name = "header", title = "Table Header", Locatable)]
pub struct TableHeader { pub struct TableHeader {
/// Whether this header should be repeated across pages. /// Whether this header should be repeated across pages.
#[default(true)] #[default(true)]
@ -561,7 +566,7 @@ pub struct TableHeader {
/// totals, or other information that should be visible on every page. /// totals, or other information that should be visible on every page.
/// ///
/// No other table cells may be placed after the footer. /// No other table cells may be placed after the footer.
#[elem(name = "footer", title = "Table Footer")] #[elem(name = "footer", title = "Table Footer", Locatable)]
pub struct TableFooter { pub struct TableFooter {
/// Whether this footer should be repeated across pages. /// Whether this footer should be repeated across pages.
#[default(true)] #[default(true)]
@ -604,7 +609,7 @@ pub struct TableFooter {
/// [19:00], [Day 1 Attendee Mixer], /// [19:00], [Day 1 Attendee Mixer],
/// ) /// )
/// ``` /// ```
#[elem(name = "hline", title = "Table Horizontal Line")] #[elem(name = "hline", title = "Table Horizontal Line", Locatable)]
pub struct TableHLine { pub struct TableHLine {
/// The row above which the horizontal line is placed (zero-indexed). /// The row above which the horizontal line is placed (zero-indexed).
/// Functions identically to the `y` field in [`grid.hline`]($grid.hline.y). /// Functions identically to the `y` field in [`grid.hline`]($grid.hline.y).
@ -649,7 +654,7 @@ pub struct TableHLine {
/// use the [table's `stroke`]($table.stroke) field or [`table.cell`'s /// use the [table's `stroke`]($table.stroke) field or [`table.cell`'s
/// `stroke`]($table.cell.stroke) field instead if the line you want to place is /// `stroke`]($table.cell.stroke) field instead if the line you want to place is
/// part of all your tables' designs. /// part of all your tables' designs.
#[elem(name = "vline", title = "Table Vertical Line")] #[elem(name = "vline", title = "Table Vertical Line", Locatable)]
pub struct TableVLine { pub struct TableVLine {
/// The column before which the horizontal line is placed (zero-indexed). /// The column before which the horizontal line is placed (zero-indexed).
/// Functions identically to the `x` field in [`grid.vline`]($grid.vline). /// Functions identically to the `x` field in [`grid.vline`]($grid.vline).
@ -770,7 +775,7 @@ pub struct TableVLine {
/// [Vikram], [49], [Perseverance], /// [Vikram], [49], [Perseverance],
/// ) /// )
/// ``` /// ```
#[elem(name = "cell", title = "Table Cell", Show)] #[elem(name = "cell", title = "Table Cell", Locatable, Show)]
pub struct TableCell { pub struct TableCell {
/// The cell's body. /// The cell's body.
#[required] #[required]
@ -806,6 +811,12 @@ pub struct TableCell {
#[fold] #[fold]
pub stroke: Sides<Option<Option<Arc<Stroke>>>>, pub stroke: Sides<Option<Option<Arc<Stroke>>>>,
// TODO: feature gate
pub kind: Smart<TableCellKind>,
// TODO: feature gate
pub header_scope: Smart<TableHeaderScope>,
/// Whether rows spanned by this cell can be placed in different pages. /// Whether rows spanned by this cell can be placed in different pages.
/// When equal to `{auto}`, a cell spanning only fixed-size rows is /// When equal to `{auto}`, a cell spanning only fixed-size rows is
/// unbreakable, while a cell spanning at least one `{auto}`-sized row is /// unbreakable, while a cell spanning at least one `{auto}`-sized row is
@ -843,3 +854,18 @@ impl From<Content> for TableCell {
value.unpack::<Self>().unwrap_or_else(Self::new) value.unpack::<Self>().unwrap_or_else(Self::new)
} }
} }
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum TableHeaderScope {
Both,
Column,
Row,
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum TableCellKind {
Header,
Footer,
#[default]
Data,
}

View File

@ -7,6 +7,7 @@ use crate::foundations::{
Styles, TargetElem, Styles, TargetElem,
}; };
use crate::html::{tag, HtmlElem}; use crate::html::{tag, HtmlElem};
use crate::introspection::Locatable;
use crate::layout::{Em, HElem, Length, Sides, StackChild, StackElem, VElem}; use crate::layout::{Em, HElem, Length, Sides, StackChild, StackElem, VElem};
use crate::model::{ListItemLike, ListLike, ParElem, ParbreakElem}; use crate::model::{ListItemLike, ListLike, ParElem, ParbreakElem};
use crate::text::TextElem; use crate::text::TextElem;
@ -27,7 +28,7 @@ use crate::text::TextElem;
/// # Syntax /// # Syntax
/// This function also has dedicated syntax: Starting a line with a slash, /// This function also has dedicated syntax: Starting a line with a slash,
/// followed by a term, a colon and a description creates a term list item. /// followed by a term, a colon and a description creates a term list item.
#[elem(scope, title = "Term List", Show)] #[elem(scope, title = "Term List", Locatable, Show)]
pub struct TermsElem { pub struct TermsElem {
/// Defines the default [spacing]($terms.spacing) of the term list. If it is /// Defines the default [spacing]($terms.spacing) of the term list. If it is
/// `{false}`, the items are spaced apart with /// `{false}`, the items are spaced apart with
@ -205,7 +206,7 @@ impl Show for Packed<TermsElem> {
} }
/// A term list item. /// A term list item.
#[elem(name = "item", title = "Term List Item")] #[elem(name = "item", title = "Term List Item", Locatable)]
pub struct TermItem { pub struct TermItem {
/// The term described by the list item. /// The term described by the list item.
#[required] #[required]

View File

@ -0,0 +1,222 @@
use ecow::EcoString;
use typst_macros::{cast, elem, Cast};
use crate::diag::SourceResult;
use crate::engine::Engine;
use crate::foundations::{Content, Packed, Show, StyleChain};
use crate::introspection::Locatable;
// TODO: docs
#[elem(Locatable, Show)]
pub struct PdfTagElem {
#[default(PdfTagKind::NonStruct)]
pub kind: PdfTagKind,
/// An alternate description.
pub alt: Option<EcoString>,
/// Exact replacement for this structure element and its children.
pub actual_text: Option<EcoString>,
/// The expanded form of an abbreviation/acronym.
pub expansion: Option<EcoString>,
/// The content to underline.
#[required]
pub body: Content,
}
impl Show for Packed<PdfTagElem> {
#[typst_macros::time(name = "pdf.tag", span = self.span())]
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(self.body.clone())
}
}
// TODO: docs
/// PDF structure elements
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum PdfTagKind {
// grouping elements
/// (Part)
Part,
/// (Article)
Art,
/// (Section)
Sect,
/// (Division)
Div,
/// (Block quotation)
BlockQuote,
/// (Caption)
Caption,
/// (Table of contents)
TOC,
/// (Table of contents item)
TOCI,
/// (Index)
Index,
/// (Nonstructural element)
NonStruct,
/// (Private element)
Private,
// paragraph like elements
/// (Heading)
H { title: Option<EcoString> },
/// (Heading level 1)
H1 { title: Option<EcoString> },
/// (Heading level 2)
H2 { title: Option<EcoString> },
/// (Heading level 3)
H4 { title: Option<EcoString> },
/// (Heading level 4)
H3 { title: Option<EcoString> },
/// (Heading level 5)
H5 { title: Option<EcoString> },
/// (Heading level 6)
H6 { title: Option<EcoString> },
/// (Paragraph)
P,
// list elements
/// (List)
L { numbering: ListNumbering },
/// (List item)
LI,
/// (Label)
Lbl,
/// (List body)
LBody,
// table elements
/// (Table)
Table,
/// (Table row)
TR,
/// (Table header)
TH { scope: TableHeaderScope },
/// (Table data cell)
TD,
/// (Table header row group)
THead,
/// (Table body row group)
TBody,
/// (Table footer row group)
TFoot,
// inline elements
/// (Span)
Span,
/// (Quotation)
Quote,
/// (Note)
Note,
/// (Reference)
Reference,
/// (Bibliography Entry)
BibEntry,
/// (Code)
Code,
/// (Link)
Link,
/// (Annotation)
Annot,
/// (Ruby)
Ruby,
/// (Ruby base text)
RB,
/// (Ruby annotation text)
RT,
/// (Ruby punctuation)
RP,
/// (Warichu)
Warichu,
/// (Warichu text)
WT,
/// (Warichu punctuation)
WP,
/// (Figure)
Figure,
/// (Formula)
Formula,
/// (Form)
Form,
}
cast! {
PdfTagKind,
self => match self {
PdfTagKind::Part => "part".into_value(),
_ => todo!(),
},
"part" => Self::Part,
// TODO
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ListNumbering {
/// No numbering.
None,
/// Solid circular bullets.
Disc,
/// Open circular bullets.
Circle,
/// Solid square bullets.
Square,
/// Decimal numbers.
Decimal,
/// Lowercase Roman numerals.
LowerRoman,
/// Uppercase Roman numerals.
UpperRoman,
/// Lowercase letters.
LowerAlpha,
/// Uppercase letters.
UpperAlpha,
}
/// The scope of a table header cell.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum TableHeaderScope {
/// The header cell refers to the row.
Row,
/// The header cell refers to the column.
Column,
/// The header cell refers to both the row and the column.
Both,
}
/// Mark content as a PDF artifact.
/// TODO: maybe generalize this and use it to mark html elements with `aria-hidden="true"`?
#[elem(Locatable, Show)]
pub struct ArtifactElem {
/// The artifact kind.
#[default(ArtifactKind::Other)]
pub kind: ArtifactKind,
/// The content that is an artifact.
#[required]
pub body: Content,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Cast)]
pub enum ArtifactKind {
/// Page header artifacts.
Header,
/// Page footer artifacts.
Footer,
/// Other page artifacts.
Page,
/// Other artifacts.
#[default]
Other,
}
impl Show for Packed<ArtifactElem> {
#[typst_macros::time(name = "pdf.artifact", span = self.span())]
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
Ok(self.body.clone())
}
}

View File

@ -1,7 +1,9 @@
//! PDF-specific functionality. //! PDF-specific functionality.
mod accessibility;
mod embed; mod embed;
pub use self::accessibility::*;
pub use self::embed::*; pub use self::embed::*;
use crate::foundations::{Module, Scope}; use crate::foundations::{Module, Scope};
@ -11,5 +13,7 @@ pub fn module() -> Module {
let mut pdf = Scope::deduplicating(); let mut pdf = Scope::deduplicating();
pdf.start_category(crate::Category::Pdf); pdf.start_category(crate::Category::Pdf);
pdf.define_elem::<EmbedElem>(); pdf.define_elem::<EmbedElem>();
pdf.define_elem::<PdfTagElem>();
pdf.define_elem::<ArtifactElem>();
Module::new("pdf", pdf) Module::new("pdf", pdf)
} }

View File

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

View File

@ -20,6 +20,7 @@ use crate::foundations::{
PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, TargetElem, PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, TargetElem,
}; };
use crate::html::{tag, HtmlElem}; use crate::html::{tag, HtmlElem};
use crate::introspection::Locatable;
use crate::layout::{BlockBody, BlockElem, Em, HAlignment}; use crate::layout::{BlockBody, BlockElem, Em, HAlignment};
use crate::loading::{DataSource, Load}; use crate::loading::{DataSource, Load};
use crate::model::{Figurable, ParElem}; use crate::model::{Figurable, ParElem};
@ -78,6 +79,7 @@ use crate::World;
scope, scope,
title = "Raw Text / Code", title = "Raw Text / Code",
Synthesize, Synthesize,
Locatable,
Show, Show,
ShowSet, ShowSet,
LocalName, LocalName,
@ -636,7 +638,7 @@ fn format_theme_error(error: syntect::LoadingError) -> LoadError {
/// It allows you to access various properties of the line, such as the line /// It allows you to access various properties of the line, such as the line
/// number, the raw non-highlighted text, the highlighted text, and whether it /// number, the raw non-highlighted text, the highlighted text, and whether it
/// is the first or last line of the raw block. /// is the first or last line of the raw block.
#[elem(name = "line", title = "Raw Text / Code Line", Show, PlainText)] #[elem(name = "line", title = "Raw Text / Code Line", Locatable, Show, PlainText)]
pub struct RawLine { pub struct RawLine {
/// The line number of the raw line inside of the raw block, starts at 1. /// The line number of the raw line inside of the raw block, starts at 1.
#[required] #[required]

View File

@ -6,6 +6,7 @@ use crate::foundations::{
elem, Content, NativeElement, Packed, SequenceElem, Show, StyleChain, TargetElem, elem, Content, NativeElement, Packed, SequenceElem, Show, StyleChain, TargetElem,
}; };
use crate::html::{tag, HtmlElem}; use crate::html::{tag, HtmlElem};
use crate::introspection::Locatable;
use crate::layout::{Em, Length}; use crate::layout::{Em, Length};
use crate::text::{variant, SpaceElem, TextElem, TextSize}; use crate::text::{variant, SpaceElem, TextElem, TextSize};
use crate::World; use crate::World;
@ -18,7 +19,7 @@ use crate::World;
/// ```example /// ```example
/// Revenue#sub[yearly] /// Revenue#sub[yearly]
/// ``` /// ```
#[elem(title = "Subscript", Show)] #[elem(title = "Subscript", Locatable, Show)]
pub struct SubElem { pub struct SubElem {
/// Whether to prefer the dedicated subscript characters of the font. /// Whether to prefer the dedicated subscript characters of the font.
/// ///
@ -84,7 +85,7 @@ impl Show for Packed<SubElem> {
/// ```example /// ```example
/// 1#super[st] try! /// 1#super[st] try!
/// ``` /// ```
#[elem(title = "Superscript", Show)] #[elem(title = "Superscript", Locatable, Show)]
pub struct SuperElem { pub struct SuperElem {
/// Whether to prefer the dedicated superscript characters of the font. /// Whether to prefer the dedicated superscript characters of the font.
/// ///

View File

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

View File

@ -21,6 +21,7 @@ use crate::foundations::{
cast, elem, func, scope, Bytes, Cast, Content, Derived, NativeElement, Packed, Show, cast, elem, func, scope, Bytes, Cast, Content, Derived, NativeElement, Packed, Show,
Smart, StyleChain, Smart, StyleChain,
}; };
use crate::introspection::Locatable;
use crate::layout::{BlockElem, Length, Rel, Sizing}; use crate::layout::{BlockElem, Length, Rel, Sizing};
use crate::loading::{DataSource, Load, LoadSource, Loaded, Readable}; use crate::loading::{DataSource, Load, LoadSource, Loaded, Readable};
use crate::model::Figurable; use crate::model::Figurable;
@ -44,7 +45,7 @@ use crate::text::LocalName;
/// ], /// ],
/// ) /// )
/// ``` /// ```
#[elem(scope, Show, LocalName, Figurable)] #[elem(scope, Locatable, Show, LocalName, Figurable)]
pub struct ImageElem { pub struct ImageElem {
/// A [path]($syntax/#paths) to an image file or raw bytes making up an /// A [path]($syntax/#paths) to an image file or raw bytes making up an
/// image in one of the supported [formats]($image.format). /// image in one of the supported [formats]($image.format).

View File

@ -2,7 +2,6 @@ use std::collections::{BTreeMap, HashMap, HashSet};
use std::num::NonZeroU64; use std::num::NonZeroU64;
use ecow::{eco_format, EcoVec}; use ecow::{eco_format, EcoVec};
use krilla::annotation::Annotation;
use krilla::configure::{Configuration, ValidationError, Validator}; use krilla::configure::{Configuration, ValidationError, Validator};
use krilla::destination::{NamedDestination, XyzDestination}; use krilla::destination::{NamedDestination, XyzDestination};
use krilla::embed::EmbedError; use krilla::embed::EmbedError;
@ -14,7 +13,7 @@ use krilla::{Document, SerializeSettings};
use krilla_svg::render_svg_glyph; use krilla_svg::render_svg_glyph;
use typst_library::diag::{bail, error, SourceDiagnostic, SourceResult}; use typst_library::diag::{bail, error, SourceDiagnostic, SourceResult};
use typst_library::foundations::{NativeElement, Repr}; use typst_library::foundations::{NativeElement, Repr};
use typst_library::introspection::Location; use typst_library::introspection::{Location, Tag};
use typst_library::layout::{ use typst_library::layout::{
Abs, Frame, FrameItem, GroupItem, PagedDocument, Size, Transform, Abs, Frame, FrameItem, GroupItem, PagedDocument, Size, Transform,
}; };
@ -25,11 +24,12 @@ use typst_syntax::Span;
use crate::embed::embed_files; use crate::embed::embed_files;
use crate::image::handle_image; use crate::image::handle_image;
use crate::link::handle_link; use crate::link::{handle_link, LinkAnnotation};
use crate::metadata::build_metadata; use crate::metadata::build_metadata;
use crate::outline::build_outline; use crate::outline::build_outline;
use crate::page::PageLabelExt; use crate::page::PageLabelExt;
use crate::shape::handle_shape; use crate::shape::handle_shape;
use crate::tags::{self, Tags};
use crate::text::handle_text; use crate::text::handle_text;
use crate::util::{convert_path, display_font, AbsExt, TransformExt}; use crate::util::{convert_path, display_font, AbsExt, TransformExt};
use crate::PdfOptions; use crate::PdfOptions;
@ -46,7 +46,7 @@ pub fn convert(
xmp_metadata: true, xmp_metadata: true,
cmyk_profile: None, cmyk_profile: None,
configuration: options.standards.config, configuration: options.standards.config,
enable_tagging: false, enable_tagging: !options.disable_tags,
render_svg_glyph_fn: render_svg_glyph, render_svg_glyph_fn: render_svg_glyph,
}; };
@ -54,6 +54,7 @@ pub fn convert(
let page_index_converter = PageIndexConverter::new(typst_document, options); let page_index_converter = PageIndexConverter::new(typst_document, options);
let named_destinations = let named_destinations =
collect_named_destinations(typst_document, &page_index_converter); collect_named_destinations(typst_document, &page_index_converter);
let mut gc = GlobalContext::new( let mut gc = GlobalContext::new(
typst_document, typst_document,
options, options,
@ -66,6 +67,7 @@ pub fn convert(
document.set_outline(build_outline(&gc)); document.set_outline(build_outline(&gc));
document.set_metadata(build_metadata(&gc)); document.set_metadata(build_metadata(&gc));
document.set_tag_tree(gc.tags.build_tree());
finish(document, gc, options.standards.config) finish(document, gc, options.standards.config)
} }
@ -115,9 +117,7 @@ fn convert_pages(gc: &mut GlobalContext, document: &mut Document) -> SourceResul
surface.finish(); surface.finish();
for annotation in fc.annotations { tags::add_annotations(gc, &mut page, fc.link_annotations);
page.add_annotation(annotation);
}
} }
} }
@ -171,14 +171,14 @@ impl State {
/// Context needed for converting a single frame. /// Context needed for converting a single frame.
pub(crate) struct FrameContext { pub(crate) struct FrameContext {
states: Vec<State>, states: Vec<State>,
annotations: Vec<Annotation>, link_annotations: Vec<LinkAnnotation>,
} }
impl FrameContext { impl FrameContext {
pub(crate) fn new(size: Size) -> Self { pub(crate) fn new(size: Size) -> Self {
Self { Self {
states: vec![State::new(size)], states: vec![State::new(size)],
annotations: vec![], link_annotations: Vec::new(),
} }
} }
@ -198,8 +198,18 @@ impl FrameContext {
self.states.last_mut().unwrap() self.states.last_mut().unwrap()
} }
pub(crate) fn push_annotation(&mut self, annotation: Annotation) { pub(crate) fn get_link_annotation(
self.annotations.push(annotation); &mut self,
link_id: tags::LinkId,
) -> Option<&mut LinkAnnotation> {
self.link_annotations
.iter_mut()
.rev()
.find(|annot| annot.id == link_id)
}
pub(crate) fn push_link_annotation(&mut self, annotation: LinkAnnotation) {
self.link_annotations.push(annotation);
} }
} }
@ -225,6 +235,8 @@ pub(crate) struct GlobalContext<'a> {
/// The languages used throughout the document. /// The languages used throughout the document.
pub(crate) languages: BTreeMap<Lang, usize>, pub(crate) languages: BTreeMap<Lang, usize>,
pub(crate) page_index_converter: PageIndexConverter, pub(crate) page_index_converter: PageIndexConverter,
/// Tagged PDF context.
pub(crate) tags: Tags,
} }
impl<'a> GlobalContext<'a> { impl<'a> GlobalContext<'a> {
@ -244,6 +256,8 @@ impl<'a> GlobalContext<'a> {
image_spans: HashSet::new(), image_spans: HashSet::new(),
languages: BTreeMap::new(), languages: BTreeMap::new(),
page_index_converter, page_index_converter,
tags: Tags::new(),
} }
} }
} }
@ -278,8 +292,9 @@ pub(crate) fn handle_frame(
FrameItem::Image(image, size, span) => { FrameItem::Image(image, size, span) => {
handle_image(gc, fc, image, *size, surface, *span)? handle_image(gc, fc, image, *size, surface, *span)?
} }
FrameItem::Link(d, s) => handle_link(fc, gc, d, *s), FrameItem::Link(dest, size) => handle_link(fc, gc, dest, *size),
FrameItem::Tag(_) => {} FrameItem::Tag(Tag::Start(elem)) => tags::handle_start(gc, elem),
FrameItem::Tag(Tag::End(loc, _)) => tags::handle_end(gc, *loc),
} }
fc.pop(); fc.pop();
@ -294,7 +309,7 @@ pub(crate) fn handle_group(
fc: &mut FrameContext, fc: &mut FrameContext,
group: &GroupItem, group: &GroupItem,
surface: &mut Surface, surface: &mut Surface,
context: &mut GlobalContext, gc: &mut GlobalContext,
) -> SourceResult<()> { ) -> SourceResult<()> {
fc.push(); fc.push();
fc.state_mut().pre_concat(group.transform); fc.state_mut().pre_concat(group.transform);
@ -310,10 +325,12 @@ pub(crate) fn handle_group(
.and_then(|p| p.transform(fc.state().transform.to_krilla())); .and_then(|p| p.transform(fc.state().transform.to_krilla()));
if let Some(clip_path) = &clip_path { if let Some(clip_path) = &clip_path {
let mut handle = tags::start_marked(gc, surface);
let surface = handle.surface();
surface.push_clip_path(clip_path, &krilla::paint::FillRule::NonZero); surface.push_clip_path(clip_path, &krilla::paint::FillRule::NonZero);
} }
handle_frame(fc, &group.frame, None, surface, context)?; handle_frame(fc, &group.frame, None, surface, gc)?;
if clip_path.is_some() { if clip_path.is_some() {
surface.pop(); surface.pop();
@ -576,6 +593,16 @@ fn convert_error(
"{prefix} missing document date"; "{prefix} missing document date";
hint: "set the date of the document" hint: "set the date of the document"
), ),
ValidationError::DuplicateTagId(_id, loc) => {
// TODO: display the id and better error message
let span = to_span(*loc);
error!(span, "{prefix} duplicate tag id")
}
ValidationError::UnknownHeaderTagId(_id, loc) => {
// TODO: display the id and better error message
let span = to_span(*loc);
error!(span, "{prefix} unknown header tag id")
}
} }
} }

View File

@ -4,6 +4,7 @@ use std::sync::{Arc, OnceLock};
use image::{DynamicImage, EncodableLayout, GenericImageView, Rgba}; use image::{DynamicImage, EncodableLayout, GenericImageView, Rgba};
use krilla::image::{BitsPerComponent, CustomImage, ImageColorspace}; use krilla::image::{BitsPerComponent, CustomImage, ImageColorspace};
use krilla::surface::Surface; use krilla::surface::Surface;
use krilla::tagging::SpanTag;
use krilla_svg::{SurfaceExt, SvgSettings}; use krilla_svg::{SurfaceExt, SvgSettings};
use typst_library::diag::{bail, SourceResult}; use typst_library::diag::{bail, SourceResult};
use typst_library::foundations::Smart; use typst_library::foundations::Smart;
@ -14,6 +15,7 @@ use typst_library::visualize::{
use typst_syntax::Span; use typst_syntax::Span;
use crate::convert::{FrameContext, GlobalContext}; use crate::convert::{FrameContext, GlobalContext};
use crate::tags;
use crate::util::{SizeExt, TransformExt}; use crate::util::{SizeExt, TransformExt};
#[typst_macros::time(name = "handle image")] #[typst_macros::time(name = "handle image")]
@ -30,12 +32,11 @@ pub(crate) fn handle_image(
let interpolate = image.scaling() == Smart::Custom(ImageScaling::Smooth); let interpolate = image.scaling() == Smart::Custom(ImageScaling::Smooth);
if let Some(alt) = image.alt() {
surface.start_alt_text(alt);
}
gc.image_spans.insert(span); gc.image_spans.insert(span);
let mut handle =
tags::start_span(gc, surface, SpanTag::empty().with_alt_text(image.alt()));
let surface = handle.surface();
match image.kind() { match image.kind() {
ImageKind::Raster(raster) => { ImageKind::Raster(raster) => {
let (exif_transform, new_size) = exif_transform(raster, size); let (exif_transform, new_size) = exif_transform(raster, size);
@ -62,10 +63,6 @@ pub(crate) fn handle_image(
} }
} }
if image.alt().is_some() {
surface.end_alt_text();
}
surface.pop(); surface.pop();
surface.reset_location(); surface.reset_location();

View File

@ -9,6 +9,7 @@ mod outline;
mod page; mod page;
mod paint; mod paint;
mod shape; mod shape;
mod tags;
mod text; mod text;
mod util; mod util;
@ -53,6 +54,11 @@ pub struct PdfOptions<'a> {
pub page_ranges: Option<PageRanges>, pub page_ranges: Option<PageRanges>,
/// A list of PDF standards that Typst will enforce conformance with. /// A list of PDF standards that Typst will enforce conformance with.
pub standards: PdfStandards, pub standards: PdfStandards,
/// By default, even when not producing a `PDF/UA-1` document, a tagged PDF
/// document is written to provide a baseline of accessibility. In some
/// circumstances, for example when trying to reduce the size of a document,
/// it can be desirable to disable tagged PDF.
pub disable_tags: bool,
} }
/// Encapsulates a list of compatible PDF standards. /// Encapsulates a list of compatible PDF standards.
@ -104,6 +110,7 @@ impl PdfStandards {
PdfStandard::A_4 => set_validator(Validator::A4)?, PdfStandard::A_4 => set_validator(Validator::A4)?,
PdfStandard::A_4f => set_validator(Validator::A4F)?, PdfStandard::A_4f => set_validator(Validator::A4F)?,
PdfStandard::A_4e => set_validator(Validator::A4E)?, PdfStandard::A_4e => set_validator(Validator::A4E)?,
PdfStandard::Ua_1 => set_validator(Validator::UA1)?,
} }
} }
@ -187,4 +194,7 @@ pub enum PdfStandard {
/// PDF/A-4e. /// PDF/A-4e.
#[serde(rename = "a-4e")] #[serde(rename = "a-4e")]
A_4e, A_4e,
/// PDF/UA-1.
#[serde(rename = "ua-1")]
Ua_1,
} }

View File

@ -1,19 +1,90 @@
use ecow::EcoString;
use krilla::action::{Action, LinkAction}; use krilla::action::{Action, LinkAction};
use krilla::annotation::{LinkAnnotation, Target}; use krilla::annotation::Target;
use krilla::configure::Validator;
use krilla::destination::XyzDestination; use krilla::destination::XyzDestination;
use krilla::geom::Rect; use krilla::geom as kg;
use typst_library::layout::{Abs, Point, Size}; use typst_library::layout::{Abs, Point, Position, Size};
use typst_library::model::Destination; use typst_library::model::Destination;
use crate::convert::{FrameContext, GlobalContext}; use crate::convert::{FrameContext, GlobalContext};
use crate::tags::{self, Placeholder, StackEntryKind, TagNode};
use crate::util::{AbsExt, PointExt}; use crate::util::{AbsExt, PointExt};
pub(crate) struct LinkAnnotation {
pub(crate) id: tags::LinkId,
pub(crate) placeholder: Placeholder,
pub(crate) alt: Option<String>,
pub(crate) rect: kg::Rect,
pub(crate) quad_points: Vec<kg::Point>,
pub(crate) target: Target,
}
pub(crate) fn handle_link( pub(crate) fn handle_link(
fc: &mut FrameContext, fc: &mut FrameContext,
gc: &mut GlobalContext, gc: &mut GlobalContext,
dest: &Destination, dest: &Destination,
size: Size, size: Size,
) { ) {
let target = match dest {
Destination::Url(u) => {
Target::Action(Action::Link(LinkAction::new(u.to_string())))
}
Destination::Position(p) => match pos_to_target(gc, *p) {
Some(target) => target,
None => return,
},
Destination::Location(loc) => {
if let Some(nd) = gc.loc_to_names.get(loc) {
// If a named destination has been registered, it's already guaranteed to
// not point to an excluded page.
Target::Destination(krilla::destination::Destination::Named(nd.clone()))
} else {
let pos = gc.document.introspector.position(*loc);
match pos_to_target(gc, pos) {
Some(target) => target,
None => return,
}
}
}
};
let entry = gc.tags.stack.last_mut().expect("a link parent");
let StackEntryKind::Link(link_id, ref link) = entry.kind else {
unreachable!("expected a link parent")
};
let alt = link.alt.as_ref().map(EcoString::to_string);
let rect = to_rect(fc, size);
let quadpoints = quadpoints(rect);
// Unfortunately quadpoints still aren't well supported by most PDF readers,
// even by acrobat. Which is understandable since they were only introduced
// in PDF 1.6 (2005) /s
let should_use_quadpoints = gc.options.standards.config.validator() == Validator::UA1;
match fc.get_link_annotation(link_id) {
Some(annotation) if should_use_quadpoints => {
// Update the bounding box and add the quadpoints to an existing link annotation.
annotation.rect = bounding_rect(annotation.rect, rect);
annotation.quad_points.extend_from_slice(&quadpoints);
}
_ => {
let placeholder = gc.tags.reserve_placeholder();
gc.tags.push(TagNode::Placeholder(placeholder));
fc.push_link_annotation(LinkAnnotation {
id: link_id,
placeholder,
rect,
quad_points: quadpoints.to_vec(),
alt,
target,
});
}
}
}
// Compute the bounding box of the transformed link.
fn to_rect(fc: &FrameContext, size: Size) -> kg::Rect {
let mut min_x = Abs::inf(); let mut min_x = Abs::inf();
let mut min_y = Abs::inf(); let mut min_y = Abs::inf();
let mut max_x = -Abs::inf(); let mut max_x = -Abs::inf();
@ -21,7 +92,6 @@ pub(crate) fn handle_link(
let pos = Point::zero(); let pos = Point::zero();
// Compute the bounding box of the transformed link.
for point in [ for point in [
pos, pos,
pos + Point::with_x(size.x), pos + Point::with_x(size.x),
@ -40,55 +110,32 @@ pub(crate) fn handle_link(
let y1 = min_y.to_f32(); let y1 = min_y.to_f32();
let y2 = max_y.to_f32(); let y2 = max_y.to_f32();
let rect = Rect::from_ltrb(x1, y1, x2, y2).unwrap(); kg::Rect::from_ltrb(x1, y1, x2, y2).unwrap()
}
// TODO: Support quad points. fn bounding_rect(a: kg::Rect, b: kg::Rect) -> kg::Rect {
kg::Rect::from_ltrb(
let pos = match dest { a.left().min(b.left()),
Destination::Url(u) => { a.top().min(b.top()),
fc.push_annotation( a.right().max(b.right()),
LinkAnnotation::new( a.bottom().max(b.bottom()),
rect,
None,
Target::Action(Action::Link(LinkAction::new(u.to_string()))),
) )
.into(), .unwrap()
);
return;
} }
Destination::Position(p) => *p,
Destination::Location(loc) => {
if let Some(nd) = gc.loc_to_names.get(loc) {
// If a named destination has been registered, it's already guaranteed to
// not point to an excluded page.
fc.push_annotation(
LinkAnnotation::new(
rect,
None,
Target::Destination(krilla::destination::Destination::Named(
nd.clone(),
)),
)
.into(),
);
return;
} else {
gc.document.introspector.position(*loc)
}
}
};
fn quadpoints(rect: kg::Rect) -> [kg::Point; 4] {
[
kg::Point::from_xy(rect.left(), rect.bottom()),
kg::Point::from_xy(rect.right(), rect.bottom()),
kg::Point::from_xy(rect.right(), rect.top()),
kg::Point::from_xy(rect.left(), rect.top()),
]
}
fn pos_to_target(gc: &mut GlobalContext, pos: Position) -> Option<Target> {
let page_index = pos.page.get() - 1; let page_index = pos.page.get() - 1;
if let Some(index) = gc.page_index_converter.pdf_page_index(page_index) { let index = gc.page_index_converter.pdf_page_index(page_index)?;
fc.push_annotation(
LinkAnnotation::new( let dest = XyzDestination::new(index, pos.point.to_krilla());
rect, Some(Target::Destination(krilla::destination::Destination::Xyz(dest)))
None,
Target::Destination(krilla::destination::Destination::Xyz(
XyzDestination::new(index, pos.point.to_krilla()),
)),
)
.into(),
);
}
} }

View File

@ -5,8 +5,8 @@ use typst_library::visualize::{Geometry, Shape};
use typst_syntax::Span; use typst_syntax::Span;
use crate::convert::{FrameContext, GlobalContext}; use crate::convert::{FrameContext, GlobalContext};
use crate::paint;
use crate::util::{convert_path, AbsExt, TransformExt}; use crate::util::{convert_path, AbsExt, TransformExt};
use crate::{paint, tags};
#[typst_macros::time(name = "handle shape")] #[typst_macros::time(name = "handle shape")]
pub(crate) fn handle_shape( pub(crate) fn handle_shape(
@ -16,6 +16,9 @@ pub(crate) fn handle_shape(
gc: &mut GlobalContext, gc: &mut GlobalContext,
span: Span, span: Span,
) -> SourceResult<()> { ) -> SourceResult<()> {
let mut handle = tags::start_marked(gc, surface);
let surface = handle.surface();
surface.set_location(span.into_raw().get()); surface.set_location(span.into_raw().get());
surface.push_transform(&fc.state().transform().to_krilla()); surface.push_transform(&fc.state().transform().to_krilla());

View File

@ -0,0 +1,625 @@
use std::cell::OnceCell;
use std::num::{NonZeroU32, NonZeroUsize};
use ecow::EcoString;
use krilla::page::Page;
use krilla::surface::Surface;
use krilla::tagging::{
ArtifactType, ContentTag, Identifier, Node, SpanTag, TableCellSpan, TableDataCell,
TableHeaderCell, Tag, TagBuilder, TagGroup, TagKind, TagTree,
};
use typst_library::foundations::{Content, LinkMarker, Packed, Smart, StyleChain};
use typst_library::introspection::Location;
use typst_library::layout::RepeatElem;
use typst_library::model::{
Destination, FigureCaption, FigureElem, HeadingElem, Outlinable, OutlineBody,
OutlineEntry, TableCell, TableCellKind, TableElem, TableHeaderScope,
};
use typst_library::pdf::{ArtifactElem, ArtifactKind, PdfTagElem, PdfTagKind};
use typst_library::visualize::ImageElem;
use crate::convert::GlobalContext;
use crate::link::LinkAnnotation;
pub(crate) struct Tags {
/// The intermediary stack of nested tag groups.
pub(crate) stack: Vec<StackEntry>,
/// A list of placeholders corresponding to a [`TagNode::Placeholder`].
pub(crate) placeholders: Vec<OnceCell<Node>>,
pub(crate) in_artifact: Option<(Location, ArtifactKind)>,
pub(crate) link_id: LinkId,
/// The output.
pub(crate) tree: Vec<TagNode>,
}
pub(crate) struct StackEntry {
pub(crate) loc: Location,
pub(crate) kind: StackEntryKind,
pub(crate) nodes: Vec<TagNode>,
}
pub(crate) enum StackEntryKind {
Standard(Tag),
Outline(OutlineCtx),
OutlineEntry(Packed<OutlineEntry>),
Table(TableCtx),
TableCell(Packed<TableCell>),
Link(LinkId, Packed<LinkMarker>),
}
impl StackEntryKind {
pub(crate) fn as_standard_mut(&mut self) -> Option<&mut Tag> {
if let Self::Standard(v) = self {
Some(v)
} else {
None
}
}
}
pub(crate) struct OutlineCtx {
stack: Vec<OutlineSection>,
}
pub(crate) struct OutlineSection {
entries: Vec<TagNode>,
}
impl OutlineSection {
const fn new() -> Self {
OutlineSection { entries: Vec::new() }
}
fn push(&mut self, entry: TagNode) {
self.entries.push(entry);
}
fn into_tag(self) -> TagNode {
TagNode::Group(TagKind::TOC.into(), self.entries)
}
}
impl OutlineCtx {
fn new() -> Self {
Self { stack: Vec::new() }
}
fn insert(
&mut self,
outline_nodes: &mut Vec<TagNode>,
entry: Packed<OutlineEntry>,
nodes: Vec<TagNode>,
) {
let expected_len = entry.level.get() - 1;
if self.stack.len() < expected_len {
self.stack.resize_with(expected_len, || OutlineSection::new());
} else {
while self.stack.len() > expected_len {
self.finish_section(outline_nodes);
}
}
let section_entry = TagNode::Group(TagKind::TOCI.into(), nodes);
self.push(outline_nodes, section_entry);
}
fn finish_section(&mut self, outline_nodes: &mut Vec<TagNode>) {
let sub_section = self.stack.pop().unwrap().into_tag();
self.push(outline_nodes, sub_section);
}
fn push(&mut self, outline_nodes: &mut Vec<TagNode>, entry: TagNode) {
match self.stack.last_mut() {
Some(section) => section.push(entry),
None => outline_nodes.push(entry),
}
}
fn build_outline(mut self, mut outline_nodes: Vec<TagNode>) -> Vec<TagNode> {
while self.stack.len() > 0 {
self.finish_section(&mut outline_nodes);
}
outline_nodes
}
}
pub(crate) struct TableCtx {
table: Packed<TableElem>,
rows: Vec<Vec<GridCell>>,
}
#[derive(Clone, Default)]
enum GridCell {
Cell(TableCtxCell),
Spanned(usize, usize),
#[default]
Missing,
}
impl GridCell {
fn as_cell(&self) -> Option<&TableCtxCell> {
if let Self::Cell(v) = self {
Some(v)
} else {
None
}
}
fn into_cell(self) -> Option<TableCtxCell> {
if let Self::Cell(v) = self {
Some(v)
} else {
None
}
}
}
#[derive(Clone)]
struct TableCtxCell {
rowspan: NonZeroUsize,
colspan: NonZeroUsize,
kind: TableCellKind,
header_scope: Smart<TableHeaderScope>,
nodes: Vec<TagNode>,
}
impl TableCtx {
fn new(table: Packed<TableElem>) -> Self {
Self { table: table.clone(), rows: Vec::new() }
}
fn insert(&mut self, cell: Packed<TableCell>, nodes: Vec<TagNode>) {
let x = cell.x(StyleChain::default()).unwrap_or_else(|| unreachable!());
let y = cell.y(StyleChain::default()).unwrap_or_else(|| unreachable!());
let rowspan = cell.rowspan(StyleChain::default());
let colspan = cell.colspan(StyleChain::default());
let kind = cell.kind(StyleChain::default());
let header_scope = cell.header_scope(StyleChain::default());
// The explicit cell kind takes precedence, but if it is `auto` and a
// scope was specified, make this a header cell.
let kind = match (kind, header_scope) {
(Smart::Custom(kind), _) => kind,
(Smart::Auto, Smart::Custom(_)) => TableCellKind::Header,
(Smart::Auto, Smart::Auto) => TableCellKind::Data,
};
// Extend the table grid to fit this cell.
let required_height = y + rowspan.get();
let required_width = x + colspan.get();
if self.rows.len() < required_height {
self.rows
.resize(required_height, vec![GridCell::Missing; required_width]);
}
let row = &mut self.rows[y];
if row.len() < required_width {
row.resize_with(required_width, || GridCell::Missing);
}
// Store references to the cell for all spanned cells.
for i in y..y + rowspan.get() {
for j in x..x + colspan.get() {
self.rows[i][j] = GridCell::Spanned(x, y);
}
}
self.rows[y][x] =
GridCell::Cell(TableCtxCell { rowspan, colspan, kind, header_scope, nodes });
}
fn build_table(self, mut nodes: Vec<TagNode>) -> Vec<TagNode> {
// Table layouting ensures that there are no overlapping cells, and that
// any gaps left by the user are filled with empty cells.
// Only generate row groups such as `THead`, `TFoot`, and `TBody` if
// there are no rows with mixed cell kinds.
let mut mixed_row_kinds = false;
let row_kinds = (self.rows.iter())
.map(|row| {
row.iter()
.filter_map(|cell| match cell {
GridCell::Cell(cell) => Some(cell),
&GridCell::Spanned(x, y) => self.rows[y][x].as_cell(),
GridCell::Missing => None,
})
.map(|cell| cell.kind)
.reduce(|a, b| {
if a != b {
mixed_row_kinds = true;
}
a
})
.expect("tables must have at least one column")
})
.collect::<Vec<_>>();
let Some(mut chunk_kind) = row_kinds.first().copied() else {
return nodes;
};
let mut row_chunk = Vec::new();
for (row, row_kind) in self.rows.into_iter().zip(row_kinds) {
let row_nodes = row
.into_iter()
.filter_map(|cell| {
let cell = cell.into_cell()?;
let span = TableCellSpan {
rows: cell.rowspan.get() as i32,
cols: cell.colspan.get() as i32,
};
let tag = match cell.kind {
TableCellKind::Header => {
let scope = match cell.header_scope {
Smart::Custom(scope) => table_header_scope(scope),
Smart::Auto => krilla::tagging::TableHeaderScope::Column,
};
TagKind::TH(TableHeaderCell::new(scope).with_span(span))
}
TableCellKind::Footer | TableCellKind::Data => {
TagKind::TD(TableDataCell::new().with_span(span))
}
};
Some(TagNode::Group(tag.into(), cell.nodes))
})
.collect();
let row = TagNode::Group(TagKind::TR.into(), row_nodes);
// Push the `TR` tags directly.
if mixed_row_kinds {
nodes.push(row);
continue;
}
// Generate row groups.
if row_kind != chunk_kind {
let tag = match chunk_kind {
TableCellKind::Header => TagKind::THead,
TableCellKind::Footer => TagKind::TFoot,
TableCellKind::Data => TagKind::TBody,
};
nodes.push(TagNode::Group(tag.into(), std::mem::take(&mut row_chunk)));
chunk_kind = row_kind;
}
row_chunk.push(row);
}
if !row_chunk.is_empty() {
let tag = match chunk_kind {
TableCellKind::Header => TagKind::THead,
TableCellKind::Footer => TagKind::TFoot,
TableCellKind::Data => TagKind::TBody,
};
nodes.push(TagNode::Group(tag.into(), row_chunk));
}
nodes
}
}
#[derive(Clone)]
pub(crate) enum TagNode {
Group(Tag, Vec<TagNode>),
Leaf(Identifier),
/// Allows inserting a placeholder into the tag tree.
/// Currently used for [`krilla::page::Page::add_tagged_annotation`].
Placeholder(Placeholder),
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct LinkId(u32);
#[derive(Clone, Copy)]
pub(crate) struct Placeholder(usize);
impl Tags {
pub(crate) fn new() -> Self {
Self {
stack: Vec::new(),
placeholders: Vec::new(),
in_artifact: None,
tree: Vec::new(),
link_id: LinkId(0),
}
}
pub(crate) fn reserve_placeholder(&mut self) -> Placeholder {
let idx = self.placeholders.len();
self.placeholders.push(OnceCell::new());
Placeholder(idx)
}
pub(crate) fn init_placeholder(&mut self, placeholder: Placeholder, node: Node) {
self.placeholders[placeholder.0]
.set(node)
.map_err(|_| ())
.expect("placeholder to be uninitialized");
}
pub(crate) fn take_placeholder(&mut self, placeholder: Placeholder) -> Node {
self.placeholders[placeholder.0]
.take()
.expect("initialized placeholder node")
}
/// Returns the current parent's list of children and the structure type ([Tag]).
/// In case of the document root the structure type will be `None`.
pub(crate) fn parent(&mut self) -> Option<&mut StackEntryKind> {
self.stack.last_mut().map(|e| &mut e.kind)
}
pub(crate) fn push(&mut self, node: TagNode) {
if let Some(entry) = self.stack.last_mut() {
entry.nodes.push(node);
} else {
self.tree.push(node);
}
}
pub(crate) fn build_tree(&mut self) -> TagTree {
let children = std::mem::take(&mut self.tree)
.into_iter()
.map(|node| self.resolve_node(node))
.collect::<Vec<_>>();
TagTree::from(children)
}
/// Resolves [`Placeholder`] nodes.
fn resolve_node(&mut self, node: TagNode) -> Node {
match node {
TagNode::Group(tag, nodes) => {
let children = nodes
.into_iter()
.map(|node| self.resolve_node(node))
.collect::<Vec<_>>();
Node::Group(TagGroup::with_children(tag, children))
}
TagNode::Leaf(identifier) => Node::Leaf(identifier),
TagNode::Placeholder(placeholder) => self.take_placeholder(placeholder),
}
}
fn context_supports(&self, _tag: &StackEntryKind) -> bool {
// TODO: generate using: https://pdfa.org/resource/iso-ts-32005-hierarchical-inclusion-rules/
true
}
fn next_link_id(&mut self) -> LinkId {
self.link_id.0 += 1;
self.link_id
}
}
/// Automatically calls [`Surface::end_tagged`] when dropped.
pub(crate) struct TagHandle<'a, 'b> {
surface: &'b mut Surface<'a>,
}
impl Drop for TagHandle<'_, '_> {
fn drop(&mut self) {
self.surface.end_tagged();
}
}
impl<'a> TagHandle<'a, '_> {
pub(crate) fn surface<'c>(&'c mut self) -> &'c mut Surface<'a> {
&mut self.surface
}
}
/// Returns a [`TagHandle`] that automatically calls [`Surface::end_tagged`]
/// when dropped.
pub(crate) fn start_marked<'a, 'b>(
gc: &mut GlobalContext,
surface: &'b mut Surface<'a>,
) -> TagHandle<'a, 'b> {
start_content(gc, surface, ContentTag::Other)
}
/// Returns a [`TagHandle`] that automatically calls [`Surface::end_tagged`]
/// when dropped.
pub(crate) fn start_span<'a, 'b>(
gc: &mut GlobalContext,
surface: &'b mut Surface<'a>,
span: SpanTag,
) -> TagHandle<'a, 'b> {
start_content(gc, surface, ContentTag::Span(span))
}
fn start_content<'a, 'b>(
gc: &mut GlobalContext,
surface: &'b mut Surface<'a>,
content: ContentTag,
) -> TagHandle<'a, 'b> {
let content = if let Some((_, kind)) = gc.tags.in_artifact {
let ty = artifact_type(kind);
ContentTag::Artifact(ty)
} else if let Some(StackEntryKind::Table(_)) = gc.tags.stack.last().map(|e| &e.kind) {
// Mark any direct child of a table as an aritfact. Any real content
// will be wrapped inside a `TableCell`.
ContentTag::Artifact(ArtifactType::Other)
} else {
content
};
let id = surface.start_tagged(content);
gc.tags.push(TagNode::Leaf(id));
TagHandle { surface }
}
/// Add all annotations that were found in the page frame.
pub(crate) fn add_annotations(
gc: &mut GlobalContext,
page: &mut Page,
annotations: Vec<LinkAnnotation>,
) {
for annotation in annotations.into_iter() {
let LinkAnnotation { id: _, placeholder, alt, rect, quad_points, target } =
annotation;
let annot = krilla::annotation::Annotation::new_link(
krilla::annotation::LinkAnnotation::new(rect, Some(quad_points), target),
alt,
);
let annot_id = page.add_tagged_annotation(annot);
gc.tags.init_placeholder(placeholder, Node::Leaf(annot_id));
}
}
pub(crate) fn handle_start(gc: &mut GlobalContext, elem: &Content) {
if gc.tags.in_artifact.is_some() {
// Don't nest artifacts
return;
}
let loc = elem.location().unwrap();
if let Some(artifact) = elem.to_packed::<ArtifactElem>() {
let kind = artifact.kind(StyleChain::default());
start_artifact(gc, loc, kind);
return;
} else if let Some(_) = elem.to_packed::<RepeatElem>() {
start_artifact(gc, loc, ArtifactKind::Other);
return;
}
let tag: Tag = if let Some(pdf_tag) = elem.to_packed::<PdfTagElem>() {
let kind = pdf_tag.kind(StyleChain::default());
match kind {
PdfTagKind::Part => TagKind::Part.into(),
_ => todo!(),
}
} else if let Some(heading) = elem.to_packed::<HeadingElem>() {
let level = heading.level().try_into().unwrap_or(NonZeroU32::MAX);
let name = heading.body.plain_text().to_string();
TagKind::Hn(level, Some(name)).into()
} else if let Some(_) = elem.to_packed::<OutlineBody>() {
push_stack(gc, loc, StackEntryKind::Outline(OutlineCtx::new()));
return;
} else if let Some(entry) = elem.to_packed::<OutlineEntry>() {
push_stack(gc, loc, StackEntryKind::OutlineEntry(entry.clone()));
return;
} else if let Some(_) = elem.to_packed::<FigureElem>() {
let alt = None; // TODO
TagKind::Figure.with_alt_text(alt)
} else if let Some(image) = elem.to_packed::<ImageElem>() {
let alt = image.alt(StyleChain::default()).map(|s| s.to_string());
let figure_tag = (gc.tags.parent())
.and_then(StackEntryKind::as_standard_mut)
.filter(|tag| tag.kind == TagKind::Figure);
if let Some(figure_tag) = figure_tag {
// Set alt text of outer figure tag, if not present.
if figure_tag.alt_text.is_none() {
figure_tag.alt_text = alt;
}
return;
} else {
TagKind::Figure.with_alt_text(alt)
}
} else if let Some(_) = elem.to_packed::<FigureCaption>() {
TagKind::Caption.into()
} else if let Some(table) = elem.to_packed::<TableElem>() {
push_stack(gc, loc, StackEntryKind::Table(TableCtx::new(table.clone())));
return;
} else if let Some(cell) = elem.to_packed::<TableCell>() {
push_stack(gc, loc, StackEntryKind::TableCell(cell.clone()));
return;
} else if let Some(link) = elem.to_packed::<LinkMarker>() {
let link_id = gc.tags.next_link_id();
push_stack(gc, loc, StackEntryKind::Link(link_id, link.clone()));
return;
} else {
return;
};
push_stack(gc, loc, StackEntryKind::Standard(tag));
}
fn push_stack(gc: &mut GlobalContext, loc: Location, kind: StackEntryKind) {
if !gc.tags.context_supports(&kind) {
// TODO: error or warning?
}
gc.tags.stack.push(StackEntry { loc, kind, nodes: Vec::new() });
}
pub(crate) fn handle_end(gc: &mut GlobalContext, loc: Location) {
if let Some((l, _)) = gc.tags.in_artifact {
if l == loc {
gc.tags.in_artifact = None;
}
return;
}
let Some(entry) = gc.tags.stack.pop_if(|e| e.loc == loc) else {
return;
};
let node = match entry.kind {
StackEntryKind::Standard(tag) => TagNode::Group(tag, entry.nodes),
StackEntryKind::Outline(ctx) => {
let nodes = ctx.build_outline(entry.nodes);
TagNode::Group(TagKind::TOC.into(), nodes)
}
StackEntryKind::OutlineEntry(outline_entry) => {
let parent = gc.tags.stack.last_mut().expect("outline");
let StackEntryKind::Outline(outline_ctx) = &mut parent.kind else {
unreachable!("expected outline")
};
outline_ctx.insert(&mut parent.nodes, outline_entry, entry.nodes);
return;
}
StackEntryKind::Table(ctx) => {
let summary = ctx.table.summary(StyleChain::default()).map(EcoString::into);
let nodes = ctx.build_table(entry.nodes);
TagNode::Group(TagKind::Table(summary).into(), nodes)
}
StackEntryKind::TableCell(cell) => {
let parent = gc.tags.stack.last_mut().expect("table");
let StackEntryKind::Table(table_ctx) = &mut parent.kind else {
unreachable!("expected table")
};
table_ctx.insert(cell, entry.nodes);
return;
}
StackEntryKind::Link(_, link) => {
let alt = link.alt.as_ref().map(EcoString::to_string);
let tag = TagKind::Link.with_alt_text(alt);
let mut node = TagNode::Group(tag, entry.nodes);
// Wrap link in reference tag, if it's not a url.
if let Destination::Position(_) | Destination::Location(_) = link.dest {
node = TagNode::Group(TagKind::Reference.into(), vec![node]);
}
node
}
};
gc.tags.push(node);
}
fn start_artifact(gc: &mut GlobalContext, loc: Location, kind: ArtifactKind) {
gc.tags.in_artifact = Some((loc, kind));
}
fn table_header_scope(scope: TableHeaderScope) -> krilla::tagging::TableHeaderScope {
match scope {
TableHeaderScope::Both => krilla::tagging::TableHeaderScope::Both,
TableHeaderScope::Column => krilla::tagging::TableHeaderScope::Column,
TableHeaderScope::Row => krilla::tagging::TableHeaderScope::Row,
}
}
fn artifact_type(kind: ArtifactKind) -> ArtifactType {
match kind {
ArtifactKind::Header => ArtifactType::Header,
ArtifactKind::Footer => ArtifactType::Footer,
ArtifactKind::Page => ArtifactType::Page,
ArtifactKind::Other => ArtifactType::Other,
}
}

View File

@ -3,6 +3,7 @@ use std::sync::Arc;
use bytemuck::TransparentWrapper; use bytemuck::TransparentWrapper;
use krilla::surface::{Location, Surface}; use krilla::surface::{Location, Surface};
use krilla::tagging::SpanTag;
use krilla::text::GlyphId; use krilla::text::GlyphId;
use typst_library::diag::{bail, SourceResult}; use typst_library::diag::{bail, SourceResult};
use typst_library::layout::Size; use typst_library::layout::Size;
@ -11,8 +12,8 @@ use typst_library::visualize::FillRule;
use typst_syntax::Span; use typst_syntax::Span;
use crate::convert::{FrameContext, GlobalContext}; use crate::convert::{FrameContext, GlobalContext};
use crate::paint;
use crate::util::{display_font, AbsExt, TransformExt}; use crate::util::{display_font, AbsExt, TransformExt};
use crate::{paint, tags};
#[typst_macros::time(name = "handle text")] #[typst_macros::time(name = "handle text")]
pub(crate) fn handle_text( pub(crate) fn handle_text(
@ -23,6 +24,10 @@ pub(crate) fn handle_text(
) -> SourceResult<()> { ) -> SourceResult<()> {
*gc.languages.entry(t.lang).or_insert(0) += t.glyphs.len(); *gc.languages.entry(t.lang).or_insert(0) += t.glyphs.len();
let mut handle =
tags::start_span(gc, surface, SpanTag::empty().with_actual_text(Some(&t.text)));
let surface = handle.surface();
let font = convert_font(gc, t.font.clone())?; let font = convert_font(gc, t.font.clone())?;
let fill = paint::convert_fill( let fill = paint::convert_fill(
gc, gc,

View File

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

View File

@ -167,7 +167,7 @@ fn render_frame(canvas: &mut sk::Pixmap, state: State, frame: &Frame) {
FrameItem::Image(image, size, _) => { FrameItem::Image(image, size, _) => {
image::render_image(canvas, state.pre_translate(*pos), image, *size); image::render_image(canvas, state.pre_translate(*pos), image, *size);
} }
FrameItem::Link(_, _) => {} FrameItem::Link(..) => {}
FrameItem::Tag(_) => {} FrameItem::Tag(_) => {}
} }
} }

View File

@ -207,7 +207,7 @@ impl SVGRenderer {
for (pos, item) in frame.items() { for (pos, item) in frame.items() {
// File size optimization. // File size optimization.
// TODO: SVGs could contain links, couldn't they? // TODO: SVGs could contain links, couldn't they?
if matches!(item, FrameItem::Link(_, _) | FrameItem::Tag(_)) { if matches!(item, FrameItem::Link(..) | FrameItem::Tag(_)) {
continue; continue;
} }
@ -228,7 +228,7 @@ impl SVGRenderer {
self.render_shape(state.pre_translate(*pos), shape) self.render_shape(state.pre_translate(*pos), shape)
} }
FrameItem::Image(image, size, _) => self.render_image(image, size), FrameItem::Image(image, size, _) => self.render_image(image, size),
FrameItem::Link(_, _) => unreachable!(), FrameItem::Link(..) => unreachable!(),
FrameItem::Tag(_) => unreachable!(), FrameItem::Tag(_) => unreachable!(),
}; };

View File

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

View File

@ -185,7 +185,7 @@ impl Lexer<'_> {
'h' if self.s.eat_if("ttp://") => self.link(), 'h' if self.s.eat_if("ttp://") => self.link(),
'h' if self.s.eat_if("ttps://") => self.link(), 'h' if self.s.eat_if("ttps://") => self.link(),
'<' if self.s.at(is_id_continue) => self.label(), '<' if self.s.at(is_id_continue) => self.label(),
'@' => self.ref_marker(), '@' if self.s.at(is_id_continue) => self.ref_marker(),
'.' if self.s.eat_if("..") => SyntaxKind::Shorthand, '.' if self.s.eat_if("..") => SyntaxKind::Shorthand,
'-' if self.s.eat_if("--") => SyntaxKind::Shorthand, '-' if self.s.eat_if("--") => SyntaxKind::Shorthand,

View File

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

View File

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

View File

@ -181,11 +181,7 @@
[`sys.version`]($category/foundations/sys) can also be very useful. [`sys.version`]($category/foundations/sys) can also be very useful.
```typ ```typ
#let tiling = if "tiling" in dictionary(std) { #let tiling = if "tiling" in std { tiling } else { pattern }
tiling
} else {
pattern
}
... ...
``` ```

View File

@ -720,18 +720,12 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel {
} }
}; };
for (variant, c) in symbol.variants() { for (variant, c, deprecation) in symbol.variants() {
let shorthand = |list: &[(&'static str, char)]| { let shorthand = |list: &[(&'static str, char)]| {
list.iter().copied().find(|&(_, x)| x == c).map(|(s, _)| s) list.iter().copied().find(|&(_, x)| x == c).map(|(s, _)| s)
}; };
let name = complete(variant); let name = complete(variant);
let deprecation = match name.as_str() {
"integral.sect" => {
Some("`integral.sect` is deprecated, use `integral.inter` instead")
}
_ => binding.deprecation(),
};
list.push(SymbolModel { list.push(SymbolModel {
name, name,
@ -742,10 +736,10 @@ fn symbols_model(resolver: &dyn Resolver, group: &GroupData) -> SymbolsModel {
accent: typst::math::Accent::combine(c).is_some(), accent: typst::math::Accent::combine(c).is_some(),
alternates: symbol alternates: symbol
.variants() .variants()
.filter(|(other, _)| other != &variant) .filter(|(other, _, _)| other != &variant)
.map(|(other, _)| complete(other)) .map(|(other, _, _)| complete(other))
.collect(), .collect(),
deprecation, deprecation: deprecation.or_else(|| binding.deprecation()),
}); });
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

View File

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

View File

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

View File

@ -34,7 +34,7 @@ To the right! Where the sunlight peeks behind the mountain.
#align(start)[Start] #align(start)[Start]
#align(end)[Ende] #align(end)[Ende]
#set text(lang: "ar") #set text(lang: "ar", font: "Noto Sans Arabic")
#align(start)[يبدأ] #align(start)[يبدأ]
#align(end)[نهاية] #align(end)[نهاية]

View File

@ -45,6 +45,7 @@ Lריווח #h(1cm) R
--- bidi-whitespace-reset --- --- bidi-whitespace-reset ---
// Test whether L1 whitespace resetting destroys stuff. // Test whether L1 whitespace resetting destroys stuff.
#set text(font: ("Libertinus Serif", "Noto Sans Arabic"))
الغالب #h(70pt) ن#" الغالب #h(70pt) ن#"
--- bidi-explicit-dir --- --- bidi-explicit-dir ---
@ -87,7 +88,7 @@ Lריווח #h(1cm) R
columns: (1fr, 1fr), columns: (1fr, 1fr),
lines(6), lines(6),
[ [
#text(lang: "ar")[مجرد نص مؤقت لأغراض العرض التوضيحي. ] #text(lang: "ar", font: ("Libertinus Serif", "Noto Sans Arabic"))[مجرد نص مؤقت لأغراض العرض التوضيحي. ]
#text(lang: "ar")[سلام] #text(lang: "ar")[سلام]
], ],
) )

View File

@ -29,6 +29,7 @@ ABCअपार्टमेंट
\ ט \ ט
--- shaping-font-fallback --- --- shaping-font-fallback ---
#set text(font: ("Libertinus Serif", "Noto Sans Arabic"))
// Font fallback for emoji. // Font fallback for emoji.
A😀B A😀B

View File

@ -80,7 +80,7 @@ I'm in#text(tracking: 0.15em + 1.5pt)[ spaace]!
--- text-tracking-arabic --- --- text-tracking-arabic ---
// Test tracking in arabic text (makes no sense whatsoever) // Test tracking in arabic text (makes no sense whatsoever)
#set text(tracking: 0.3em) #set text(tracking: 0.3em, font: "Noto Sans Arabic")
النص النص
--- text-spacing --- --- text-spacing ---

View File

@ -17,7 +17,7 @@
--- repeat-dots-rtl --- --- repeat-dots-rtl ---
// Test dots with RTL. // Test dots with RTL.
#set text(lang: "ar") #set text(lang: "ar", font: ("Libertinus Serif", "Noto Sans Arabic"))
مقدمة #box(width: 1fr, repeat[.]) 15 مقدمة #box(width: 1fr, repeat[.]) 15
--- repeat-empty --- --- repeat-empty ---
@ -35,7 +35,7 @@ A#box(width: 1fr, repeat(rect(width: 6em, height: 0.7em)))B
#set align(center) #set align(center)
A#box(width: 1fr, repeat(rect(width: 6em, height: 0.7em)))B A#box(width: 1fr, repeat(rect(width: 6em, height: 0.7em)))B
#set text(dir: rtl) #set text(dir: rtl, font: "Noto Sans Arabic")
ريجين#box(width: 1fr, repeat(rect(width: 4em, height: 0.7em)))سون ريجين#box(width: 1fr, repeat(rect(width: 4em, height: 0.7em)))سون
--- repeat-unrestricted --- --- repeat-unrestricted ---

View File

@ -121,8 +121,8 @@ $a scripts(=)^"def" b quad a scripts(lt.eq)_"really" b quad a scripts(arrow.r.lo
--- math-attach-integral --- --- math-attach-integral ---
// Test default of scripts attachments on integrals at display size. // Test default of scripts attachments on integrals at display size.
$ integral.sect_a^b quad \u{2a1b}_a^b quad limits(\u{2a1b})_a^b $ $ integral.inter_a^b quad \u{2a1b}_a^b quad limits(\u{2a1b})_a^b $
$integral.sect_a^b quad \u{2a1b}_a^b quad limits(\u{2a1b})_a^b$ $integral.inter_a^b quad \u{2a1b}_a^b quad limits(\u{2a1b})_a^b$
--- math-attach-large-operator --- --- math-attach-large-operator ---
// Test default of limit attachments on large operators at display size only. // Test default of limit attachments on large operators at display size only.

View File

@ -75,6 +75,14 @@ Now we have multiple bibliographies containing @glacier-melt @keshav2007read
// Error: 2-62 CSL style "Alphanumeric" is not suitable for bibliographies // Error: 2-62 CSL style "Alphanumeric" is not suitable for bibliographies
#bibliography("/assets/bib/works.bib", style: "alphanumeric") #bibliography("/assets/bib/works.bib", style: "alphanumeric")
--- bibliography-empty-key ---
#let src = ```yaml
"":
type: Book
```
// Error: 15-30 bibliography contains entry with empty key
#bibliography(bytes(src.text))
--- issue-4618-bibliography-set-heading-level --- --- issue-4618-bibliography-set-heading-level ---
// Test that the bibliography block's heading is set to 2 by the show rule, // Test that the bibliography block's heading is set to 2 by the show rule,
// and therefore should be rendered like a level-2 heading. Notably, this // and therefore should be rendered like a level-2 heading. Notably, this

View File

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

View File

@ -231,7 +231,7 @@ Welcome \ here. Does this work well?
--- par-hanging-indent-rtl --- --- par-hanging-indent-rtl ---
#set par(hanging-indent: 2em) #set par(hanging-indent: 2em)
#set text(dir: rtl) #set text(dir: rtl, font: ("Libertinus Serif", "Noto Sans Arabic"))
لآن وقد أظلم الليل وبدأت النجوم لآن وقد أظلم الليل وبدأت النجوم
تنضخ وجه الطبيعة التي أعْيَتْ من طول ما انبعثت في النهار تنضخ وجه الطبيعة التي أعْيَتْ من طول ما انبعثت في النهار

View File

@ -2,6 +2,7 @@
--- quote-dir-author-pos --- --- quote-dir-author-pos ---
// Text direction affects author positioning // Text direction affects author positioning
#set text(font: ("Libertinus Serif", "Noto Sans Arabic"))
And I quote: #quote(attribution: [René Descartes])[cogito, ergo sum]. And I quote: #quote(attribution: [René Descartes])[cogito, ergo sum].
#set text(lang: "ar") #set text(lang: "ar")
@ -9,6 +10,7 @@ And I quote: #quote(attribution: [René Descartes])[cogito, ergo sum].
--- quote-dir-align --- --- quote-dir-align ---
// Text direction affects block alignment // Text direction affects block alignment
#set text(font: ("Libertinus Serif", "Noto Sans Arabic"))
#set quote(block: true) #set quote(block: true)
#quote(attribution: [René Descartes])[cogito, ergo sum] #quote(attribution: [René Descartes])[cogito, ergo sum]

View File

@ -86,3 +86,14 @@ Text seen on #ref(<text>, form: "page", supplement: "Page").
// Test reference with non-whitespace before it. // Test reference with non-whitespace before it.
#figure[] <1> #figure[] <1>
#test([(#ref(<1>))], [(@1)]) #test([(#ref(<1>))], [(@1)])
--- ref-to-empty-label-not-possible ---
// @ without any following label should just produce the symbol in the output
// and not produce a reference to a label with an empty name.
@
--- ref-function-empty-label ---
// using ref() should also not be possible
// Error: 6-7 unexpected less-than operator
// Error: 7-8 unexpected greater-than operator
#ref(<>)

View File

@ -264,6 +264,8 @@
#test("Hey" not in "abheyCd", true) #test("Hey" not in "abheyCd", true)
#test("a" not #test("a" not
/* fun comment? */ in "abc", false) /* fun comment? */ in "abc", false)
#test("sys" in std, true)
#test("system" in std, false)
--- ops-not-trailing --- --- ops-not-trailing ---
// Error: 10 expected keyword `in` // Error: 10 expected keyword `in`

View File

@ -2,6 +2,7 @@
--- numbers --- --- numbers ---
// Test numbers in text mode. // Test numbers in text mode.
#set text(font: ("Libertinus Serif", "Noto Sans Arabic"))
12 \ 12 \
12.0 \ 12.0 \
3.14 \ 3.14 \

View File

@ -83,3 +83,11 @@ We can also specify a customized value
#highlight(stroke: 2pt + blue)[abc] #highlight(stroke: 2pt + blue)[abc]
#highlight(stroke: (top: blue, left: red, bottom: green, right: orange))[abc] #highlight(stroke: (top: blue, left: red, bottom: green, right: orange))[abc]
#highlight(stroke: 1pt, radius: 3pt)[#lorem(5)] #highlight(stroke: 1pt, radius: 3pt)[#lorem(5)]
--- html-deco html ---
#strike[Struck]
#highlight[Highlighted]
#underline[Underlined]
#overline[Overlined]
#(strike, highlight, underline, overline).fold([Mixed], (it, f) => f(it))

View File

@ -666,3 +666,29 @@ $ A = mat(
#let _ = gradient.linear(..my-gradient.stops()) #let _ = gradient.linear(..my-gradient.stops())
#let my-gradient2 = gradient.linear(red, blue).repeat(5, mirror: true) #let my-gradient2 = gradient.linear(red, blue).repeat(5, mirror: true)
#let _ = gradient.linear(..my-gradient2.stops()) #let _ = gradient.linear(..my-gradient2.stops())
--- issue-6162-coincident-gradient-stops-export-png ---
// Ensure that multiple gradient stops with the same position
// don't cause a panic.
#rect(
fill: gradient.linear(
(red, 0%),
(green, 0%),
(blue, 100%),
)
)
#rect(
fill: gradient.linear(
(red, 0%),
(green, 100%),
(blue, 100%),
)
)
#rect(
fill: gradient.linear(
(white, 0%),
(red, 50%),
(green, 50%),
(blue, 100%),
)
)