Compare commits

...

6 Commits

Author SHA1 Message Date
Tobias Schmitz
d99ebe42ac
feat: [WIP] allow specifying alt text for links
skip-checks:true
2025-05-28 17:47:35 +02:00
Tobias Schmitz
0d2c43c4be
feat: [WIP] include links in tag tree
skip-checks:true
2025-05-28 15:10:00 +02:00
Tobias Schmitz
19d54b6310
feat: [WIP] write tags
skip-checks:true
2025-05-28 15:10:00 +02:00
Tobias Schmitz
f185ff4953
feat: [WIP] make more things locatable
skip-checks:true
2025-05-28 15:10:00 +02:00
Tobias Schmitz
ec53eb3da2
feat: [draft] generate accessibility tag tree for headings
skip-checks:true
2025-05-28 15:09:55 +02:00
Tobias Schmitz
2a258a0c38
Remove unused Marginal type (#6321) 2025-05-23 07:31:26 +00:00
38 changed files with 406 additions and 154 deletions

View File

@ -36,7 +36,7 @@ pub fn jump_from_click(
) -> Option<Jump> { ) -> Option<Jump> {
// Try to find a link first. // Try to find a link first.
for (pos, item) in frame.items() { for (pos, item) in frame.items() {
if let FrameItem::Link(dest, size) = item { if let FrameItem::Link(_, dest, size) = item {
if is_in_rect(*pos, *size, click) { if is_in_rect(*pos, *size, click) {
return Some(match dest { return Some(match dest {
Destination::Url(url) => Jump::Url(url.clone()), Destination::Url(url) => Jump::Url(url.clone()),

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

@ -1,3 +1,4 @@
use ecow::EcoString;
use typst_library::foundations::StyleChain; use typst_library::foundations::StyleChain;
use typst_library::layout::{Abs, Fragment, Frame, FrameItem, HideElem, Point, Sides}; use typst_library::layout::{Abs, Fragment, Frame, FrameItem, HideElem, Point, Sides};
use typst_library::model::{Destination, LinkElem, ParElem}; use typst_library::model::{Destination, LinkElem, ParElem};
@ -21,6 +22,7 @@ use typst_library::model::{Destination, LinkElem, ParElem};
pub struct FrameModifiers { pub struct FrameModifiers {
/// A destination to link to. /// A destination to link to.
dest: Option<Destination>, dest: Option<Destination>,
alt: Option<EcoString>,
/// Whether the contents of the frame should be hidden. /// Whether the contents of the frame should be hidden.
hidden: bool, hidden: bool,
} }
@ -28,8 +30,10 @@ pub struct FrameModifiers {
impl FrameModifiers { impl FrameModifiers {
/// Retrieve all modifications that should be applied per-frame. /// Retrieve all modifications that should be applied per-frame.
pub fn get_in(styles: StyleChain) -> Self { pub fn get_in(styles: StyleChain) -> Self {
// TODO: maybe verify that an alt text was provided here
Self { Self {
dest: LinkElem::current_in(styles), dest: LinkElem::current_in(styles),
alt: LinkElem::alt_in(styles),
hidden: HideElem::hidden_in(styles), hidden: HideElem::hidden_in(styles),
} }
} }
@ -102,7 +106,7 @@ fn modify_frame(
pos.x -= outset.left; pos.x -= outset.left;
size += outset.sum_by_axis(); size += outset.sum_by_axis();
} }
frame.push(pos, FrameItem::Link(dest.clone(), size)); frame.push(pos, FrameItem::Link(modifiers.alt.clone(), dest.clone(), size));
} }
if modifiers.hidden { if modifiers.hidden {

View File

@ -185,6 +185,8 @@ fn layout_page_run_impl(
)?; )?;
// Layouts a single marginal. // Layouts a single marginal.
// TODO: add some sort of tag that indicates the marginals and use it to
// mark them as artifacts for PDF/UA.
let mut layout_marginal = |content: &Option<Content>, area, align| { let mut layout_marginal = |content: &Option<Content>, area, align| {
let Some(content) = content else { return Ok(None) }; let Some(content) = content else { return Ok(None) };
let aligned = content.clone().styled(AlignElem::set_alignment(align)); let aligned = content.clone().styled(AlignElem::set_alignment(align));

View File

@ -503,8 +503,9 @@ impl Content {
} }
/// Link the content somewhere. /// Link the content somewhere.
pub fn linked(self, dest: Destination) -> Self { pub fn linked(self, alt: Option<EcoString>, dest: Destination) -> Self {
self.styled(LinkElem::set_current(Some(dest))) self.styled(LinkElem::set_alt(alt))
.styled(LinkElem::set_current(Some(dest)))
} }
/// Set alignments for this content. /// Set alignments for this content.

View File

@ -7,6 +7,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 {
@ -38,7 +39,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

@ -4,6 +4,7 @@ use std::fmt::{self, Debug, Formatter};
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use std::sync::Arc; use std::sync::Arc;
use ecow::EcoString;
use typst_syntax::Span; use typst_syntax::Span;
use typst_utils::{LazyHash, Numeric}; use typst_utils::{LazyHash, Numeric};
@ -473,7 +474,7 @@ pub enum FrameItem {
/// An image and its size. /// An image and its size.
Image(Image, Size, Span), Image(Image, Size, Span),
/// An internal or external link to a destination. /// An internal or external link to a destination.
Link(Destination, Size), Link(Option<EcoString>, Destination, Size),
/// An introspectable element that produced something within this frame. /// An introspectable element that produced something within this frame.
Tag(Tag), Tag(Tag),
} }
@ -485,7 +486,7 @@ impl Debug for FrameItem {
Self::Text(text) => write!(f, "{text:?}"), Self::Text(text) => write!(f, "{text:?}"),
Self::Shape(shape, _) => write!(f, "{shape:?}"), Self::Shape(shape, _) => write!(f, "{shape:?}"),
Self::Image(image, _, _) => write!(f, "{image:?}"), Self::Image(image, _, _) => write!(f, "{image:?}"),
Self::Link(dest, _) => write!(f, "Link({dest:?})"), Self::Link(alt, dest, _) => write!(f, "Link({alt:?}, {dest:?})"),
Self::Tag(tag) => write!(f, "{tag:?}"), Self::Tag(tag) => write!(f, "{tag:?}"),
} }
} }

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)]
@ -479,7 +480,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)]
@ -646,7 +647,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

@ -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,16 +1,14 @@
use std::borrow::Cow;
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
use std::str::FromStr; use std::str::FromStr;
use comemo::Track;
use typst_utils::{singleton, NonZeroExt, Scalar}; use typst_utils::{singleton, NonZeroExt, Scalar};
use crate::diag::{bail, SourceResult}; use crate::diag::{bail, SourceResult};
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{ use crate::foundations::{
cast, elem, Args, AutoValue, Cast, Construct, Content, Context, Dict, Fold, Func, cast, elem, Args, AutoValue, Cast, Construct, Content, Dict, Fold, NativeElement,
NativeElement, Set, Smart, StyleChain, Value, Set, Smart, Value,
}; };
use crate::introspection::Introspector; use crate::introspection::Introspector;
use crate::layout::{ use crate::layout::{
@ -649,43 +647,6 @@ cast! {
}, },
} }
/// A header, footer, foreground or background definition.
#[derive(Debug, Clone, Hash)]
pub enum Marginal {
/// Bare content.
Content(Content),
/// A closure mapping from a page number to content.
Func(Func),
}
impl Marginal {
/// Resolve the marginal based on the page number.
pub fn resolve(
&self,
engine: &mut Engine,
styles: StyleChain,
page: usize,
) -> SourceResult<Cow<'_, Content>> {
Ok(match self {
Self::Content(content) => Cow::Borrowed(content),
Self::Func(func) => Cow::Owned(
func.call(engine, Context::new(None, Some(styles)).track(), [page])?
.display(),
),
})
}
}
cast! {
Marginal,
self => match self {
Self::Content(v) => v.into_value(),
Self::Func(v) => v.into_value(),
},
v: Content => Self::Content(v),
v: Func => Self::Func(v),
}
/// A list of page ranges to be exported. /// A list of page ranges to be exported.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct PageRanges(Vec<PageRange>); pub struct PageRanges(Vec<PageRange>);

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

@ -852,7 +852,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(None, dest);
} }
StrResult::Ok(content) StrResult::Ok(content)
}) })
@ -987,7 +988,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(None, dest);
} }
} }

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

@ -470,7 +470,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(None, Destination::Location(loc)))
} }
} }
@ -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(None, Destination::Location(loc))
.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(alt, dest.clone()),
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.clone().linked(alt, dest)
} }
} }
}) })

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,7 @@ 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, Region, Rel, RepeatElem, Sides
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};
@ -368,7 +368,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.
@ -422,7 +422,17 @@ impl Show for Packed<OutlineEntry> {
let context = context.track(); let context = context.track();
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(engine, 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()
@ -434,7 +444,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(Some(alt), Destination::Location(loc)))
} }
} }
@ -571,6 +581,8 @@ impl OutlineEntry {
engine: &mut Engine, 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)?;
@ -591,7 +603,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"
@ -616,7 +628,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

@ -308,7 +308,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(None, Destination::Location(loc)))
} }
/// 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

@ -10,7 +10,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 +121,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.
@ -486,7 +486,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)]
@ -505,7 +505,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)]
@ -548,7 +548,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).
@ -593,7 +593,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).
@ -714,7 +714,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]

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

@ -3,6 +3,7 @@ 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, Packed, Show, Smart, StyleChain};
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 +14,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.
/// ///
@ -99,7 +100,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.
/// ///
@ -191,7 +192,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.
/// ///
@ -268,7 +269,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.
/// ///

View File

@ -18,6 +18,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};
@ -76,6 +77,7 @@ use crate::World;
scope, scope,
title = "Raw Text / Code", title = "Raw Text / Code",
Synthesize, Synthesize,
Locatable,
Show, Show,
ShowSet, ShowSet,
LocalName, LocalName,
@ -619,7 +621,7 @@ impl RawTheme {
/// 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

@ -3,6 +3,7 @@ use ecow::EcoString;
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{elem, Content, Packed, SequenceElem, Show, StyleChain}; use crate::foundations::{elem, Content, Packed, SequenceElem, Show, StyleChain};
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;
@ -15,7 +16,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.
/// ///
@ -74,7 +75,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

@ -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, Readable}; use crate::loading::{DataSource, Load, 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

@ -1,7 +1,7 @@
use std::collections::{BTreeMap, HashMap, HashSet}; use std::collections::{BTreeMap, HashMap, HashSet};
use std::num::NonZeroU64; use std::num::NonZeroU64;
use ecow::{eco_format, EcoVec}; use ecow::{eco_format, EcoString, EcoVec};
use krilla::annotation::Annotation; 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};
@ -10,11 +10,12 @@ use krilla::error::KrillaError;
use krilla::geom::PathBuilder; use krilla::geom::PathBuilder;
use krilla::page::{PageLabel, PageSettings}; use krilla::page::{PageLabel, PageSettings};
use krilla::surface::Surface; use krilla::surface::Surface;
use krilla::tagging::{ArtifactType, ContentTag, Node};
use krilla::{Document, SerializeSettings}; use krilla::{Document, SerializeSettings};
use krilla_svg::render_svg_glyph; use krilla_svg::render_svg_glyph;
use typst_library::diag::{bail, error, SourceDiagnostic, SourceResult}; use typst_library::diag::{bail, error, SourceDiagnostic, SourceResult};
use typst_library::foundations::NativeElement; use typst_library::foundations::NativeElement;
use typst_library::introspection::Location; use typst_library::introspection::{self, Location};
use typst_library::layout::{ use typst_library::layout::{
Abs, Frame, FrameItem, GroupItem, PagedDocument, Size, Transform, Abs, Frame, FrameItem, GroupItem, PagedDocument, Size, Transform,
}; };
@ -30,6 +31,7 @@ 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::{handle_close_tag, handle_open_tag, Placeholder, TagNode, 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;
@ -39,14 +41,19 @@ pub fn convert(
typst_document: &PagedDocument, typst_document: &PagedDocument,
options: &PdfOptions, options: &PdfOptions,
) -> SourceResult<Vec<u8>> { ) -> SourceResult<Vec<u8>> {
// HACK
// let config = Configuration::new();
let config = Configuration::new_with_validator(Validator::UA1);
let settings = SerializeSettings { let settings = SerializeSettings {
compress_content_streams: true, compress_content_streams: true,
no_device_cs: true, no_device_cs: true,
ascii_compatible: false, ascii_compatible: false,
xmp_metadata: true, xmp_metadata: true,
cmyk_profile: None, cmyk_profile: None,
configuration: options.standards.config, configuration: config,
enable_tagging: false, // TODO: Should we just set this to false? If set to `false` this will
// automatically be enabled if the `UA1` validator is used.
enable_tagging: true,
render_svg_glyph_fn: render_svg_glyph, render_svg_glyph_fn: render_svg_glyph,
}; };
@ -54,6 +61,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 +74,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)
} }
@ -105,6 +114,19 @@ fn convert_pages(gc: &mut GlobalContext, document: &mut Document) -> SourceResul
let mut surface = page.surface(); let mut surface = page.surface();
let mut fc = FrameContext::new(typst_page.frame.size()); let mut fc = FrameContext::new(typst_page.frame.size());
// Marked-content may not cross page boundaries: reopen tag
// that was closed at the end of the last page.
if let Some((_, _, nodes)) = gc.tags.stack.last_mut() {
let tag = if gc.tags.in_artifact {
ContentTag::Artifact(ArtifactType::Other)
} else {
ContentTag::Other
};
// TODO: somehow avoid empty marked-content sequences
let id = surface.start_tagged(tag);
nodes.push(TagNode::Leaf(id));
}
handle_frame( handle_frame(
&mut fc, &mut fc,
&typst_page.frame, &typst_page.frame,
@ -113,10 +135,16 @@ fn convert_pages(gc: &mut GlobalContext, document: &mut Document) -> SourceResul
gc, gc,
)?; )?;
// Marked-content may not cross page boundaries: close open tag.
if !gc.tags.stack.is_empty() {
surface.end_tagged();
}
surface.finish(); surface.finish();
for annotation in fc.annotations { for (placeholder, annotation) in fc.annotations {
page.add_annotation(annotation); let annotation_id = page.add_tagged_annotation(annotation);
gc.tags.init_placeholder(placeholder, Node::Leaf(annotation_id));
} }
} }
} }
@ -171,7 +199,7 @@ 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>, annotations: Vec<(Placeholder, Annotation)>,
} }
impl FrameContext { impl FrameContext {
@ -198,8 +226,12 @@ impl FrameContext {
self.states.last_mut().unwrap() self.states.last_mut().unwrap()
} }
pub(crate) fn push_annotation(&mut self, annotation: Annotation) { pub(crate) fn push_annotation(
self.annotations.push(annotation); &mut self,
placeholder: Placeholder,
annotation: Annotation,
) {
self.annotations.push((placeholder, annotation));
} }
} }
@ -225,6 +257,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 +278,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 +314,15 @@ 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(alt, dest, size) => {
FrameItem::Tag(_) => {} handle_link(fc, gc, alt.as_ref().map(EcoString::to_string), dest, *size)
}
FrameItem::Tag(introspection::Tag::Start(elem)) => {
handle_open_tag(gc, surface, elem)
}
FrameItem::Tag(introspection::Tag::End(loc, _)) => {
handle_close_tag(gc, surface, loc);
}
} }
fc.pop(); fc.pop();

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;

View File

@ -1,16 +1,18 @@
use krilla::action::{Action, LinkAction}; use krilla::action::{Action, LinkAction};
use krilla::annotation::{LinkAnnotation, Target}; use krilla::annotation::{Annotation, LinkAnnotation, Target};
use krilla::destination::XyzDestination; use krilla::destination::XyzDestination;
use krilla::geom::Rect; use krilla::geom::Rect;
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::TagNode;
use crate::util::{AbsExt, PointExt}; use crate::util::{AbsExt, PointExt};
pub(crate) fn handle_link( pub(crate) fn handle_link(
fc: &mut FrameContext, fc: &mut FrameContext,
gc: &mut GlobalContext, gc: &mut GlobalContext,
alt: Option<String>,
dest: &Destination, dest: &Destination,
size: Size, size: Size,
) { ) {
@ -44,51 +46,42 @@ pub(crate) fn handle_link(
// TODO: Support quad points. // TODO: Support quad points.
let pos = match dest { let target = match dest {
Destination::Url(u) => { Destination::Url(u) => {
fc.push_annotation( Target::Action(Action::Link(LinkAction::new(u.to_string())))
LinkAnnotation::new(
rect,
None,
Target::Action(Action::Link(LinkAction::new(u.to_string()))),
)
.into(),
);
return;
} }
Destination::Position(p) => *p, Destination::Position(p) => match pos_to_target(gc, *p) {
Some(target) => target,
None => return,
},
Destination::Location(loc) => { Destination::Location(loc) => {
if let Some(nd) = gc.loc_to_names.get(loc) { if let Some(nd) = gc.loc_to_names.get(loc) {
// If a named destination has been registered, it's already guaranteed to // If a named destination has been registered, it's already guaranteed to
// not point to an excluded page. // not point to an excluded page.
fc.push_annotation( Target::Destination(krilla::destination::Destination::Named(nd.clone()))
LinkAnnotation::new(
rect,
None,
Target::Destination(krilla::destination::Destination::Named(
nd.clone(),
)),
)
.into(),
);
return;
} else { } else {
gc.document.introspector.position(*loc) let pos = gc.document.introspector.position(*loc);
match pos_to_target(gc, pos) {
Some(target) => target,
None => return,
}
} }
} }
}; };
let page_index = pos.page.get() - 1; let placeholder = gc.tags.reserve_placeholder();
if let Some(index) = gc.page_index_converter.pdf_page_index(page_index) { gc.tags.push(TagNode::Placeholder(placeholder));
fc.push_annotation( fc.push_annotation(
LinkAnnotation::new( placeholder,
rect, Annotation::new_link(LinkAnnotation::new(rect, None, target), alt),
None,
Target::Destination(krilla::destination::Destination::Xyz(
XyzDestination::new(index, pos.point.to_krilla()),
)),
)
.into(),
); );
} }
fn pos_to_target(gc: &mut GlobalContext, pos: Position) -> Option<Target> {
let page_index = pos.page.get() - 1;
let index = gc.page_index_converter.pdf_page_index(page_index)?;
let dest = XyzDestination::new(index, pos.point.to_krilla());
Some(Target::Destination(krilla::destination::Destination::Xyz(dest)))
} }

View File

@ -0,0 +1,209 @@
use std::cell::OnceCell;
use krilla::surface::Surface;
use krilla::tagging::{ContentTag, Identifier, Node, Tag, TagGroup, TagTree};
use typst_library::foundations::{Content, StyleChain};
use typst_library::introspection::Location;
use typst_library::model::{HeadingElem, OutlineElem, OutlineEntry};
use crate::convert::GlobalContext;
pub(crate) struct Tags {
/// The intermediary stack of nested tag groups.
pub(crate) stack: Vec<(Location, Tag, Vec<TagNode>)>,
pub(crate) placeholders: Vec<OnceCell<Node>>,
pub(crate) in_artifact: bool,
/// The output.
pub(crate) tree: Vec<TagNode>,
}
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)]
pub(crate) struct Placeholder(usize);
impl Tags {
pub(crate) fn new() -> Self {
Self {
stack: Vec::new(),
placeholders: Vec::new(),
in_artifact: false,
tree: Vec::new(),
}
}
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")
}
pub(crate) fn push(&mut self, node: TagNode) {
if let Some((_, _, nodes)) = self.stack.last_mut() {
nodes.push(node);
} else {
self.tree.push(node);
}
}
pub(crate) fn build_tree(&mut self) -> TagTree {
let mut tree = TagTree::new();
let nodes = std::mem::take(&mut self.tree);
// PERF: collect into vec and construct TagTree directly from tag nodes.
for node in nodes.into_iter().map(|node| self.resolve_node(node)) {
tree.push(node);
}
tree
}
/// Resolves [`Placeholder`] nodes.
fn resolve_node(&mut self, node: TagNode) -> Node {
match node {
TagNode::Group(tag, nodes) => {
let mut group = TagGroup::new(tag);
// PERF: collect into vec and construct TagTree directly from tag nodes.
for node in nodes.into_iter().map(|node| self.resolve_node(node)) {
group.push(node);
}
Node::Group(group)
}
TagNode::Leaf(identifier) => Node::Leaf(identifier),
TagNode::Placeholder(placeholder) => self.take_placeholder(placeholder),
}
}
pub(crate) fn context_supports(&self, tag: &Tag) -> bool {
let Some((_, parent, _)) = self.stack.last() else { return true };
use Tag::*;
match parent {
Part => true,
Article => !matches!(tag, Article),
Section => true,
BlockQuote => todo!(),
Caption => todo!(),
TOC => matches!(tag, TOC | TOCI),
// TODO: NonStruct is allowed to but (currently?) not supported by krilla
TOCI => matches!(tag, TOC | Lbl | Reference | P),
Index => todo!(),
P => todo!(),
H1(_) => todo!(),
H2(_) => todo!(),
H3(_) => todo!(),
H4(_) => todo!(),
H5(_) => todo!(),
H6(_) => todo!(),
L(_list_numbering) => todo!(),
LI => todo!(),
Lbl => todo!(),
LBody => todo!(),
Table => todo!(),
TR => todo!(),
TH(_table_header_scope) => todo!(),
TD => todo!(),
THead => todo!(),
TBody => todo!(),
TFoot => todo!(),
InlineQuote => todo!(),
Note => todo!(),
Reference => todo!(),
BibEntry => todo!(),
Code => todo!(),
Link => todo!(),
Annot => todo!(),
Figure(_) => todo!(),
Formula(_) => todo!(),
Datetime => todo!(),
Terms => todo!(),
Title => todo!(),
}
}
}
pub(crate) fn handle_open_tag(
gc: &mut GlobalContext,
surface: &mut Surface,
elem: &Content,
) {
if gc.tags.in_artifact {
return;
}
let Some(loc) = elem.location() else { return };
let tag = if let Some(heading) = elem.to_packed::<HeadingElem>() {
let level = heading.resolve_level(StyleChain::default());
let name = heading.body.plain_text().to_string();
match level.get() {
1 => Tag::H1(Some(name)),
2 => Tag::H2(Some(name)),
3 => Tag::H3(Some(name)),
4 => Tag::H4(Some(name)),
5 => Tag::H5(Some(name)),
// TODO: when targeting PDF 2.0 headings `> 6` are supported
_ => Tag::H6(Some(name)),
}
} else if let Some(_) = elem.to_packed::<OutlineElem>() {
Tag::TOC
} else if let Some(_outline_entry) = elem.to_packed::<OutlineEntry>() {
Tag::TOCI
} else {
return;
};
if !gc.tags.context_supports(&tag) {
// TODO: error or warning?
}
// close previous marked-content and open a nested tag.
if !gc.tags.stack.is_empty() {
surface.end_tagged();
}
let content_id = surface.start_tagged(krilla::tagging::ContentTag::Other);
gc.tags.stack.push((loc, tag, vec![TagNode::Leaf(content_id)]));
}
pub(crate) fn handle_close_tag(
gc: &mut GlobalContext,
surface: &mut Surface,
loc: &Location,
) {
let Some((_, tag, nodes)) = gc.tags.stack.pop_if(|(l, ..)| l == loc) else {
return;
};
surface.end_tagged();
if let Some((_, _, parent_nodes)) = gc.tags.stack.last_mut() {
parent_nodes.push(TagNode::Group(tag, nodes));
// TODO: somehow avoid empty marked-content sequences
let id = surface.start_tagged(ContentTag::Other);
parent_nodes.push(TagNode::Leaf(id));
} else {
gc.tags.tree.push(TagNode::Group(tag, nodes));
}
}

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

@ -505,7 +505,7 @@ fn render_links(canvas: &mut sk::Pixmap, ts: sk::Transform, frame: &Frame) {
let ts = ts.pre_concat(to_sk_transform(&group.transform)); let ts = ts.pre_concat(to_sk_transform(&group.transform));
render_links(canvas, ts, &group.frame); render_links(canvas, ts, &group.frame);
} }
FrameItem::Link(_, size) => { FrameItem::Link(_, _, size) => {
let w = size.x.to_pt() as f32; let w = size.x.to_pt() as f32;
let h = size.y.to_pt() as f32; let h = size.y.to_pt() as f32;
let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap(); let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap();