mirror of
https://github.com/typst/typst
synced 2025-08-20 09:49:02 +08:00
Compare commits
6 Commits
5ada3bb3cd
...
d99ebe42ac
Author | SHA1 | Date | |
---|---|---|---|
|
d99ebe42ac | ||
|
0d2c43c4be | ||
|
19d54b6310 | ||
|
f185ff4953 | ||
|
ec53eb3da2 | ||
|
2a258a0c38 |
@ -36,7 +36,7 @@ pub fn jump_from_click(
|
||||
) -> Option<Jump> {
|
||||
// Try to find a link first.
|
||||
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) {
|
||||
return Some(match dest {
|
||||
Destination::Url(url) => Jump::Url(url.clone()),
|
||||
|
@ -93,7 +93,7 @@ impl Item<'_, '_> {
|
||||
Self::Frame(frame, _) => {
|
||||
frame.size().is_zero()
|
||||
&& frame.items().all(|(_, item)| {
|
||||
matches!(item, FrameItem::Link(_, _) | FrameItem::Tag(_))
|
||||
matches!(item, FrameItem::Link(..) | FrameItem::Tag(_))
|
||||
})
|
||||
}
|
||||
Self::Placed(_, placed) => !placed.float,
|
||||
|
@ -1,3 +1,4 @@
|
||||
use ecow::EcoString;
|
||||
use typst_library::foundations::StyleChain;
|
||||
use typst_library::layout::{Abs, Fragment, Frame, FrameItem, HideElem, Point, Sides};
|
||||
use typst_library::model::{Destination, LinkElem, ParElem};
|
||||
@ -21,6 +22,7 @@ use typst_library::model::{Destination, LinkElem, ParElem};
|
||||
pub struct FrameModifiers {
|
||||
/// A destination to link to.
|
||||
dest: Option<Destination>,
|
||||
alt: Option<EcoString>,
|
||||
/// Whether the contents of the frame should be hidden.
|
||||
hidden: bool,
|
||||
}
|
||||
@ -28,8 +30,10 @@ pub struct FrameModifiers {
|
||||
impl FrameModifiers {
|
||||
/// Retrieve all modifications that should be applied per-frame.
|
||||
pub fn get_in(styles: StyleChain) -> Self {
|
||||
// TODO: maybe verify that an alt text was provided here
|
||||
Self {
|
||||
dest: LinkElem::current_in(styles),
|
||||
alt: LinkElem::alt_in(styles),
|
||||
hidden: HideElem::hidden_in(styles),
|
||||
}
|
||||
}
|
||||
@ -102,7 +106,7 @@ fn modify_frame(
|
||||
pos.x -= outset.left;
|
||||
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 {
|
||||
|
@ -185,6 +185,8 @@ fn layout_page_run_impl(
|
||||
)?;
|
||||
|
||||
// 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 Some(content) = content else { return Ok(None) };
|
||||
let aligned = content.clone().styled(AlignElem::set_alignment(align));
|
||||
|
@ -503,8 +503,9 @@ impl Content {
|
||||
}
|
||||
|
||||
/// Link the content somewhere.
|
||||
pub fn linked(self, dest: Destination) -> Self {
|
||||
self.styled(LinkElem::set_current(Some(dest)))
|
||||
pub fn linked(self, alt: Option<EcoString>, dest: Destination) -> Self {
|
||||
self.styled(LinkElem::set_alt(alt))
|
||||
.styled(LinkElem::set_current(Some(dest)))
|
||||
}
|
||||
|
||||
/// Set alignments for this content.
|
||||
|
@ -7,6 +7,7 @@ pub use self::dom::*;
|
||||
use ecow::EcoString;
|
||||
|
||||
use crate::foundations::{elem, Content, Module, Scope};
|
||||
use crate::introspection::Locatable;
|
||||
|
||||
/// Create a module with all HTML definitions.
|
||||
pub fn module() -> Module {
|
||||
@ -38,7 +39,7 @@ pub fn module() -> Module {
|
||||
/// A div with _Typst content_ inside!
|
||||
/// ]
|
||||
/// ```
|
||||
#[elem(name = "elem")]
|
||||
#[elem(name = "elem", Locatable)]
|
||||
pub struct HtmlElem {
|
||||
/// The element's tag.
|
||||
#[required]
|
||||
|
@ -4,6 +4,7 @@ use std::fmt::{self, Debug, Formatter};
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ecow::EcoString;
|
||||
use typst_syntax::Span;
|
||||
use typst_utils::{LazyHash, Numeric};
|
||||
|
||||
@ -473,7 +474,7 @@ pub enum FrameItem {
|
||||
/// An image and its size.
|
||||
Image(Image, Size, Span),
|
||||
/// 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.
|
||||
Tag(Tag),
|
||||
}
|
||||
@ -485,7 +486,7 @@ impl Debug for FrameItem {
|
||||
Self::Text(text) => write!(f, "{text:?}"),
|
||||
Self::Shape(shape, _) => write!(f, "{shape:?}"),
|
||||
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:?}"),
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ use crate::foundations::{
|
||||
cast, elem, scope, Array, CastInfo, Content, Context, Fold, FromValue, Func,
|
||||
IntoValue, NativeElement, Packed, Reflect, Resolve, Show, Smart, StyleChain, Value,
|
||||
};
|
||||
use crate::introspection::Locatable;
|
||||
use crate::layout::{
|
||||
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
|
||||
/// precedence over regular cell strokes.
|
||||
#[elem(scope, Show)]
|
||||
#[elem(scope, Locatable, Show)]
|
||||
pub struct GridElem {
|
||||
/// 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
|
||||
/// an example, refer to the [`table.header`]($table.header) element and the
|
||||
/// [`grid.stroke`]($grid.stroke) parameter.
|
||||
#[elem(name = "header", title = "Grid Header")]
|
||||
#[elem(name = "header", title = "Grid Header", Locatable)]
|
||||
pub struct GridHeader {
|
||||
/// Whether this header should be repeated across pages.
|
||||
#[default(true)]
|
||||
@ -479,7 +480,7 @@ pub struct GridHeader {
|
||||
/// itself on every page of the table.
|
||||
///
|
||||
/// 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 {
|
||||
/// Whether this footer should be repeated across pages.
|
||||
#[default(true)]
|
||||
@ -646,7 +647,7 @@ pub struct GridVLine {
|
||||
/// 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
|
||||
/// more about this.
|
||||
#[elem(name = "cell", title = "Grid Cell", Show)]
|
||||
#[elem(name = "cell", title = "Grid Cell", Locatable, Show)]
|
||||
pub struct GridCell {
|
||||
/// The cell's body.
|
||||
#[required]
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::diag::SourceResult;
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{elem, Content, Packed, Show, StyleChain};
|
||||
use crate::introspection::Locatable;
|
||||
|
||||
/// Hides content without affecting layout.
|
||||
///
|
||||
@ -14,7 +15,7 @@ use crate::foundations::{elem, Content, Packed, Show, StyleChain};
|
||||
/// Hello Jane \
|
||||
/// #hide[Hello] Joe
|
||||
/// ```
|
||||
#[elem(Show)]
|
||||
#[elem(Locatable, Show)]
|
||||
pub struct HideElem {
|
||||
/// The content to hide.
|
||||
#[required]
|
||||
|
@ -1,16 +1,14 @@
|
||||
use std::borrow::Cow;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::ops::RangeInclusive;
|
||||
use std::str::FromStr;
|
||||
|
||||
use comemo::Track;
|
||||
use typst_utils::{singleton, NonZeroExt, Scalar};
|
||||
|
||||
use crate::diag::{bail, SourceResult};
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{
|
||||
cast, elem, Args, AutoValue, Cast, Construct, Content, Context, Dict, Fold, Func,
|
||||
NativeElement, Set, Smart, StyleChain, Value,
|
||||
cast, elem, Args, AutoValue, Cast, Construct, Content, Dict, Fold, NativeElement,
|
||||
Set, Smart, Value,
|
||||
};
|
||||
use crate::introspection::Introspector;
|
||||
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.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PageRanges(Vec<PageRange>);
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::diag::SourceResult;
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{elem, Content, NativeElement, Packed, Show, StyleChain};
|
||||
use crate::introspection::Locatable;
|
||||
use crate::layout::{BlockElem, Length};
|
||||
|
||||
/// Repeats content to the available space.
|
||||
@ -24,7 +25,7 @@ use crate::layout::{BlockElem, Length};
|
||||
/// Berlin, the 22nd of December, 2022
|
||||
/// ]
|
||||
/// ```
|
||||
#[elem(Show)]
|
||||
#[elem(Locatable, Show)]
|
||||
pub struct RepeatElem {
|
||||
/// The content to repeat.
|
||||
#[required]
|
||||
|
@ -28,6 +28,7 @@ use typst_utils::singleton;
|
||||
use unicode_math_class::MathClass;
|
||||
|
||||
use crate::foundations::{elem, Content, Module, NativeElement, Scope};
|
||||
use crate::introspection::Locatable;
|
||||
use crate::layout::{Em, HElem};
|
||||
use crate::text::TextElem;
|
||||
|
||||
@ -109,7 +110,7 @@ pub fn module() -> Module {
|
||||
pub trait Mathy {}
|
||||
|
||||
/// A math alignment point: `&`, `&&`.
|
||||
#[elem(title = "Alignment Point", Mathy)]
|
||||
#[elem(title = "Alignment Point", Mathy, Locatable)]
|
||||
pub struct AlignPointElem {}
|
||||
|
||||
impl AlignPointElem {
|
||||
@ -136,7 +137,7 @@ impl AlignPointElem {
|
||||
///
|
||||
/// $x loves y and y loves 5$
|
||||
/// ```
|
||||
#[elem(Mathy)]
|
||||
#[elem(Mathy, Locatable)]
|
||||
pub struct ClassElem {
|
||||
/// The class to apply to the content.
|
||||
#[required]
|
||||
|
@ -1,6 +1,7 @@
|
||||
use typst_syntax::Span;
|
||||
|
||||
use crate::foundations::{elem, func, Content, NativeElement};
|
||||
use crate::introspection::Locatable;
|
||||
use crate::math::Mathy;
|
||||
|
||||
/// A square root.
|
||||
@ -22,7 +23,7 @@ pub fn sqrt(
|
||||
/// ```example
|
||||
/// $ root(3, x) $
|
||||
/// ```
|
||||
#[elem(Mathy)]
|
||||
#[elem(Mathy, Locatable)]
|
||||
pub struct RootElem {
|
||||
/// Which root of the radicand to take.
|
||||
#[positional]
|
||||
|
@ -852,7 +852,8 @@ impl<'a> Generator<'a> {
|
||||
renderer.display_elem_child(elem, &mut None, false)?;
|
||||
if let Some(location) = first_occurrences.get(item.key.as_str()) {
|
||||
let dest = Destination::Location(*location);
|
||||
content = content.linked(dest);
|
||||
// TODO: accept user supplied alt text
|
||||
content = content.linked(None, dest);
|
||||
}
|
||||
StrResult::Ok(content)
|
||||
})
|
||||
@ -987,7 +988,8 @@ impl ElemRenderer<'_> {
|
||||
if let Some(hayagriva::ElemMeta::Entry(i)) = elem.meta {
|
||||
if let Some(location) = (self.link)(i) {
|
||||
let dest = Destination::Location(location);
|
||||
content = content.linked(dest);
|
||||
// TODO: accept user supplied alt text
|
||||
content = content.linked(None, dest);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@ use crate::text::{Lang, Region, TextElem};
|
||||
/// This function indirectly has dedicated syntax. [References]($ref) can be
|
||||
/// used to cite works from the bibliography. The label then corresponds to the
|
||||
/// citation key.
|
||||
#[elem(Synthesize)]
|
||||
#[elem(Locatable, Synthesize)]
|
||||
pub struct CiteElem {
|
||||
/// The citation key that identifies the entry in the bibliography that
|
||||
/// shall be cited, as a label.
|
||||
|
@ -4,6 +4,7 @@ use crate::foundations::{
|
||||
elem, Content, NativeElement, Packed, Show, StyleChain, TargetElem,
|
||||
};
|
||||
use crate::html::{tag, HtmlElem};
|
||||
use crate::introspection::Locatable;
|
||||
use crate::text::{ItalicToggle, TextElem};
|
||||
|
||||
/// Emphasizes content by toggling italics.
|
||||
@ -29,7 +30,7 @@ use crate::text::{ItalicToggle, TextElem};
|
||||
/// This function also has dedicated syntax: To emphasize content, simply
|
||||
/// enclose it in underscores (`_`). Note that this only works at word
|
||||
/// 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 {
|
||||
/// The content to emphasize.
|
||||
#[required]
|
||||
|
@ -10,6 +10,7 @@ use crate::foundations::{
|
||||
Styles, TargetElem,
|
||||
};
|
||||
use crate::html::{attr, tag, HtmlElem};
|
||||
use crate::introspection::Locatable;
|
||||
use crate::layout::{Alignment, BlockElem, Em, HAlignment, Length, VAlignment, VElem};
|
||||
use crate::model::{
|
||||
ListItemLike, ListLike, Numbering, NumberingPattern, ParElem, ParbreakElem,
|
||||
@ -71,7 +72,7 @@ use crate::model::{
|
||||
/// Enumeration items can contain multiple paragraphs and other block-level
|
||||
/// content. All content that is indented more than an item's marker becomes
|
||||
/// part of that item.
|
||||
#[elem(scope, title = "Numbered List", Show)]
|
||||
#[elem(scope, title = "Numbered List", Locatable, Show)]
|
||||
pub struct EnumElem {
|
||||
/// Defines the default [spacing]($enum.spacing) of the enumeration. If it
|
||||
/// is `{false}`, the items are spaced apart with
|
||||
@ -271,7 +272,7 @@ impl Show for Packed<EnumElem> {
|
||||
}
|
||||
|
||||
/// An enumeration item.
|
||||
#[elem(name = "item", title = "Numbered List Item")]
|
||||
#[elem(name = "item", title = "Numbered List Item", Locatable)]
|
||||
pub struct EnumItem {
|
||||
/// The item's number.
|
||||
#[positional]
|
||||
|
@ -470,7 +470,7 @@ impl Outlinable for Packed<FigureElem> {
|
||||
/// caption: [A rectangle],
|
||||
/// )
|
||||
/// ```
|
||||
#[elem(name = "caption", Synthesize, Show)]
|
||||
#[elem(name = "caption", Locatable, Synthesize, Show)]
|
||||
pub struct FigureCaption {
|
||||
/// The caption's position in the figure. Either `{top}` or `{bottom}`.
|
||||
///
|
||||
|
@ -147,7 +147,8 @@ impl Show for Packed<FootnoteElem> {
|
||||
let sup = SuperElem::new(num).pack().spanned(span);
|
||||
let loc = loc.variant(1);
|
||||
// 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).
|
||||
/// 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.
|
||||
#[elem(name = "entry", title = "Footnote Entry", Show, ShowSet)]
|
||||
#[elem(name = "entry", title = "Footnote Entry", Locatable, Show, ShowSet)]
|
||||
pub struct FootnoteEntry {
|
||||
/// The footnote for this entry. Its location can be used to determine
|
||||
/// the footnote counter state.
|
||||
@ -296,7 +297,8 @@ impl Show for Packed<FootnoteEntry> {
|
||||
let sup = SuperElem::new(num)
|
||||
.pack()
|
||||
.spanned(span)
|
||||
.linked(Destination::Location(loc))
|
||||
// TODO: accept user supplied alt text
|
||||
.linked(None, Destination::Location(loc))
|
||||
.located(loc.variant(1));
|
||||
|
||||
Ok(Content::sequence([
|
||||
|
@ -9,7 +9,7 @@ use crate::foundations::{
|
||||
StyleChain, Styles, TargetElem,
|
||||
};
|
||||
use crate::html::{attr, tag, HtmlElem};
|
||||
use crate::introspection::Location;
|
||||
use crate::introspection::{Locatable, Location};
|
||||
use crate::layout::Position;
|
||||
use crate::text::TextElem;
|
||||
|
||||
@ -38,8 +38,11 @@ use crate::text::TextElem;
|
||||
/// # Syntax
|
||||
/// This function also has dedicated syntax: Text that starts with `http://` or
|
||||
/// `https://` is automatically turned into a link.
|
||||
#[elem(Show)]
|
||||
#[elem(Locatable, Show)]
|
||||
pub struct LinkElem {
|
||||
/// A text describing the link.
|
||||
pub alt: Option<EcoString>,
|
||||
|
||||
/// The destination the link points to.
|
||||
///
|
||||
/// - 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
|
||||
}
|
||||
} else {
|
||||
let alt = self.alt(styles);
|
||||
match &self.dest {
|
||||
LinkTarget::Dest(dest) => body.linked(dest.clone()),
|
||||
LinkTarget::Dest(dest) => body.linked(alt, dest.clone()),
|
||||
LinkTarget::Label(label) => {
|
||||
let elem = engine.introspector.query_label(*label).at(self.span())?;
|
||||
let dest = Destination::Location(elem.location().unwrap());
|
||||
body.clone().linked(dest)
|
||||
body.clone().linked(alt, dest)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -7,6 +7,7 @@ use crate::foundations::{
|
||||
Smart, StyleChain, Styles, TargetElem, Value,
|
||||
};
|
||||
use crate::html::{tag, HtmlElem};
|
||||
use crate::introspection::Locatable;
|
||||
use crate::layout::{BlockElem, Em, Length, VElem};
|
||||
use crate::model::{ParElem, ParbreakElem};
|
||||
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
|
||||
/// paragraphs and other block-level content. All content that is indented
|
||||
/// 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 {
|
||||
/// Defines the default [spacing]($list.spacing) of the list. If it is
|
||||
/// `{false}`, the items are spaced apart with
|
||||
@ -178,7 +179,7 @@ impl Show for Packed<ListElem> {
|
||||
}
|
||||
|
||||
/// A bullet list item.
|
||||
#[elem(name = "item", title = "Bullet List Item")]
|
||||
#[elem(name = "item", title = "Bullet List Item", Locatable)]
|
||||
pub struct ListItem {
|
||||
/// The item's body.
|
||||
#[required]
|
||||
|
@ -2,6 +2,7 @@ use std::num::NonZeroUsize;
|
||||
use std::str::FromStr;
|
||||
|
||||
use comemo::{Track, Tracked};
|
||||
use ecow::eco_format;
|
||||
use smallvec::SmallVec;
|
||||
use typst_syntax::Span;
|
||||
use typst_utils::{Get, NonZeroExt};
|
||||
@ -17,8 +18,7 @@ use crate::introspection::{
|
||||
Counter, CounterKey, Introspector, Locatable, Location, Locator, LocatorLink,
|
||||
};
|
||||
use crate::layout::{
|
||||
Abs, Axes, BlockBody, BlockElem, BoxElem, Dir, Em, Fr, HElem, Length, Region, Rel,
|
||||
RepeatElem, Sides,
|
||||
Abs, Axes, BlockBody, BlockElem, BoxElem, Dir, Em, Fr, HElem, Length, PageElem, Region, Rel, RepeatElem, Sides
|
||||
};
|
||||
use crate::math::EquationElem;
|
||||
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
|
||||
/// the outline's appearance. See the
|
||||
/// [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 {
|
||||
/// The nesting level of this outline entry. Starts at `{1}` for top-level
|
||||
/// entries.
|
||||
@ -422,7 +422,17 @@ impl Show for Packed<OutlineEntry> {
|
||||
let context = context.track();
|
||||
|
||||
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 body = prefix.unwrap_or_default() + inner;
|
||||
BlockElem::new()
|
||||
@ -434,7 +444,7 @@ impl Show for Packed<OutlineEntry> {
|
||||
};
|
||||
|
||||
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,
|
||||
context: Tracked<Context>,
|
||||
span: Span,
|
||||
body: Content,
|
||||
page: Content,
|
||||
) -> SourceResult<Content> {
|
||||
let styles = context.styles().at(span)?;
|
||||
|
||||
@ -591,7 +603,7 @@ impl OutlineEntry {
|
||||
seq.push(TextElem::packed("\u{202B}"));
|
||||
}
|
||||
|
||||
seq.push(self.body().at(span)?);
|
||||
seq.push(body);
|
||||
|
||||
if rtl {
|
||||
// "Pop Directional Formatting"
|
||||
@ -616,7 +628,7 @@ impl OutlineEntry {
|
||||
// Add the page number. The word joiner in front ensures that the page
|
||||
// number doesn't stand alone in its line.
|
||||
seq.push(TextElem::packed("\u{2060}"));
|
||||
seq.push(self.page(engine, context, span)?);
|
||||
seq.push(page);
|
||||
|
||||
Ok(Content::sequence(seq))
|
||||
}
|
||||
|
@ -93,7 +93,7 @@ use crate::model::Numbering;
|
||||
/// let $a$ be the smallest of the
|
||||
/// three integers. Then, we ...
|
||||
/// ```
|
||||
#[elem(scope, title = "Paragraph")]
|
||||
#[elem(scope, title = "Paragraph", Locatable)]
|
||||
pub struct ParElem {
|
||||
/// The spacing between lines.
|
||||
///
|
||||
|
@ -308,7 +308,8 @@ fn show_reference(
|
||||
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.
|
||||
|
@ -4,6 +4,7 @@ use crate::foundations::{
|
||||
elem, Content, NativeElement, Packed, Show, StyleChain, TargetElem,
|
||||
};
|
||||
use crate::html::{tag, HtmlElem};
|
||||
use crate::introspection::Locatable;
|
||||
use crate::text::{TextElem, WeightDelta};
|
||||
|
||||
/// 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
|
||||
/// word boundaries. To strongly emphasize part of a word, you have to use the
|
||||
/// function.
|
||||
#[elem(title = "Strong Emphasis", keywords = ["bold", "weight"], Show)]
|
||||
#[elem(title = "Strong Emphasis", keywords = ["bold", "weight"], Locatable, Show)]
|
||||
pub struct StrongElem {
|
||||
/// The delta to apply on the font weight.
|
||||
///
|
||||
|
@ -10,7 +10,7 @@ use crate::foundations::{
|
||||
TargetElem,
|
||||
};
|
||||
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::{
|
||||
show_grid_cell, Abs, Alignment, BlockElem, Celled, GridCell, GridFooter, GridHLine,
|
||||
@ -121,7 +121,7 @@ use crate::visualize::{Paint, Stroke};
|
||||
/// [Robert], b, a, b,
|
||||
/// )
|
||||
/// ```
|
||||
#[elem(scope, Show, LocalName, Figurable)]
|
||||
#[elem(scope, Locatable, Show, LocalName, Figurable)]
|
||||
pub struct TableElem {
|
||||
/// The column sizes. See the [grid documentation]($grid) for more
|
||||
/// information on track sizing.
|
||||
@ -486,7 +486,7 @@ impl TryFrom<Content> for TableItem {
|
||||
/// [7.34], [57], [2],
|
||||
/// )
|
||||
/// ```
|
||||
#[elem(name = "header", title = "Table Header")]
|
||||
#[elem(name = "header", title = "Table Header", Locatable)]
|
||||
pub struct TableHeader {
|
||||
/// Whether this header should be repeated across pages.
|
||||
#[default(true)]
|
||||
@ -505,7 +505,7 @@ pub struct TableHeader {
|
||||
/// totals, or other information that should be visible on every page.
|
||||
///
|
||||
/// 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 {
|
||||
/// Whether this footer should be repeated across pages.
|
||||
#[default(true)]
|
||||
@ -548,7 +548,7 @@ pub struct TableFooter {
|
||||
/// [19:00], [Day 1 Attendee Mixer],
|
||||
/// )
|
||||
/// ```
|
||||
#[elem(name = "hline", title = "Table Horizontal Line")]
|
||||
#[elem(name = "hline", title = "Table Horizontal Line", Locatable)]
|
||||
pub struct TableHLine {
|
||||
/// The row above which the horizontal line is placed (zero-indexed).
|
||||
/// 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
|
||||
/// `stroke`]($table.cell.stroke) field instead if the line you want to place is
|
||||
/// part of all your tables' designs.
|
||||
#[elem(name = "vline", title = "Table Vertical Line")]
|
||||
#[elem(name = "vline", title = "Table Vertical Line", Locatable)]
|
||||
pub struct TableVLine {
|
||||
/// The column before which the horizontal line is placed (zero-indexed).
|
||||
/// Functions identically to the `x` field in [`grid.vline`]($grid.vline).
|
||||
@ -714,7 +714,7 @@ pub struct TableVLine {
|
||||
/// [Vikram], [49], [Perseverance],
|
||||
/// )
|
||||
/// ```
|
||||
#[elem(name = "cell", title = "Table Cell", Show)]
|
||||
#[elem(name = "cell", title = "Table Cell", Locatable, Show)]
|
||||
pub struct TableCell {
|
||||
/// The cell's body.
|
||||
#[required]
|
||||
|
@ -7,6 +7,7 @@ use crate::foundations::{
|
||||
Styles, TargetElem,
|
||||
};
|
||||
use crate::html::{tag, HtmlElem};
|
||||
use crate::introspection::Locatable;
|
||||
use crate::layout::{Em, HElem, Length, Sides, StackChild, StackElem, VElem};
|
||||
use crate::model::{ListItemLike, ListLike, ParElem, ParbreakElem};
|
||||
use crate::text::TextElem;
|
||||
@ -27,7 +28,7 @@ use crate::text::TextElem;
|
||||
/// # Syntax
|
||||
/// 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.
|
||||
#[elem(scope, title = "Term List", Show)]
|
||||
#[elem(scope, title = "Term List", Locatable, Show)]
|
||||
pub struct TermsElem {
|
||||
/// Defines the default [spacing]($terms.spacing) of the term list. If it is
|
||||
/// `{false}`, the items are spaced apart with
|
||||
@ -205,7 +206,7 @@ impl Show for Packed<TermsElem> {
|
||||
}
|
||||
|
||||
/// A term list item.
|
||||
#[elem(name = "item", title = "Term List Item")]
|
||||
#[elem(name = "item", title = "Term List Item", Locatable)]
|
||||
pub struct TermItem {
|
||||
/// The term described by the list item.
|
||||
#[required]
|
||||
|
@ -3,6 +3,7 @@ use smallvec::smallvec;
|
||||
use crate::diag::SourceResult;
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{elem, Content, Packed, Show, Smart, StyleChain};
|
||||
use crate::introspection::Locatable;
|
||||
use crate::layout::{Abs, Corners, Length, Rel, Sides};
|
||||
use crate::text::{BottomEdge, BottomEdgeMetric, TextElem, TopEdge, TopEdgeMetric};
|
||||
use crate::visualize::{Color, FixedStroke, Paint, Stroke};
|
||||
@ -13,7 +14,7 @@ use crate::visualize::{Color, FixedStroke, Paint, Stroke};
|
||||
/// ```example
|
||||
/// This is #underline[important].
|
||||
/// ```
|
||||
#[elem(Show)]
|
||||
#[elem(Locatable, Show)]
|
||||
pub struct UnderlineElem {
|
||||
/// How to [stroke] the line.
|
||||
///
|
||||
@ -99,7 +100,7 @@ impl Show for Packed<UnderlineElem> {
|
||||
/// ```example
|
||||
/// #overline[A line over text.]
|
||||
/// ```
|
||||
#[elem(Show)]
|
||||
#[elem(Locatable, Show)]
|
||||
pub struct OverlineElem {
|
||||
/// How to [stroke] the line.
|
||||
///
|
||||
@ -191,7 +192,7 @@ impl Show for Packed<OverlineElem> {
|
||||
/// ```example
|
||||
/// This is #strike[not] relevant.
|
||||
/// ```
|
||||
#[elem(title = "Strikethrough", Show)]
|
||||
#[elem(title = "Strikethrough", Locatable, Show)]
|
||||
pub struct StrikeElem {
|
||||
/// How to [stroke] the line.
|
||||
///
|
||||
@ -268,7 +269,7 @@ impl Show for Packed<StrikeElem> {
|
||||
/// ```example
|
||||
/// This is #highlight[important].
|
||||
/// ```
|
||||
#[elem(Show)]
|
||||
#[elem(Locatable, Show)]
|
||||
pub struct HighlightElem {
|
||||
/// The color to highlight the text with.
|
||||
///
|
||||
|
@ -18,6 +18,7 @@ use crate::foundations::{
|
||||
PlainText, Show, ShowSet, Smart, StyleChain, Styles, Synthesize, TargetElem,
|
||||
};
|
||||
use crate::html::{tag, HtmlElem};
|
||||
use crate::introspection::Locatable;
|
||||
use crate::layout::{BlockBody, BlockElem, Em, HAlignment};
|
||||
use crate::loading::{DataSource, Load};
|
||||
use crate::model::{Figurable, ParElem};
|
||||
@ -76,6 +77,7 @@ use crate::World;
|
||||
scope,
|
||||
title = "Raw Text / Code",
|
||||
Synthesize,
|
||||
Locatable,
|
||||
Show,
|
||||
ShowSet,
|
||||
LocalName,
|
||||
@ -619,7 +621,7 @@ impl RawTheme {
|
||||
/// 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
|
||||
/// 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 {
|
||||
/// The line number of the raw line inside of the raw block, starts at 1.
|
||||
#[required]
|
||||
|
@ -3,6 +3,7 @@ use ecow::EcoString;
|
||||
use crate::diag::SourceResult;
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{elem, Content, Packed, SequenceElem, Show, StyleChain};
|
||||
use crate::introspection::Locatable;
|
||||
use crate::layout::{Em, Length};
|
||||
use crate::text::{variant, SpaceElem, TextElem, TextSize};
|
||||
use crate::World;
|
||||
@ -15,7 +16,7 @@ use crate::World;
|
||||
/// ```example
|
||||
/// Revenue#sub[yearly]
|
||||
/// ```
|
||||
#[elem(title = "Subscript", Show)]
|
||||
#[elem(title = "Subscript", Locatable, Show)]
|
||||
pub struct SubElem {
|
||||
/// Whether to prefer the dedicated subscript characters of the font.
|
||||
///
|
||||
@ -74,7 +75,7 @@ impl Show for Packed<SubElem> {
|
||||
/// ```example
|
||||
/// 1#super[st] try!
|
||||
/// ```
|
||||
#[elem(title = "Superscript", Show)]
|
||||
#[elem(title = "Superscript", Locatable, Show)]
|
||||
pub struct SuperElem {
|
||||
/// Whether to prefer the dedicated superscript characters of the font.
|
||||
///
|
||||
|
@ -21,6 +21,7 @@ use crate::foundations::{
|
||||
cast, elem, func, scope, Bytes, Cast, Content, Derived, NativeElement, Packed, Show,
|
||||
Smart, StyleChain,
|
||||
};
|
||||
use crate::introspection::Locatable;
|
||||
use crate::layout::{BlockElem, Length, Rel, Sizing};
|
||||
use crate::loading::{DataSource, Load, Readable};
|
||||
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 {
|
||||
/// A [path]($syntax/#paths) to an image file or raw bytes making up an
|
||||
/// image in one of the supported [formats]($image.format).
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::num::NonZeroU64;
|
||||
|
||||
use ecow::{eco_format, EcoVec};
|
||||
use ecow::{eco_format, EcoString, EcoVec};
|
||||
use krilla::annotation::Annotation;
|
||||
use krilla::configure::{Configuration, ValidationError, Validator};
|
||||
use krilla::destination::{NamedDestination, XyzDestination};
|
||||
@ -10,11 +10,12 @@ use krilla::error::KrillaError;
|
||||
use krilla::geom::PathBuilder;
|
||||
use krilla::page::{PageLabel, PageSettings};
|
||||
use krilla::surface::Surface;
|
||||
use krilla::tagging::{ArtifactType, ContentTag, Node};
|
||||
use krilla::{Document, SerializeSettings};
|
||||
use krilla_svg::render_svg_glyph;
|
||||
use typst_library::diag::{bail, error, SourceDiagnostic, SourceResult};
|
||||
use typst_library::foundations::NativeElement;
|
||||
use typst_library::introspection::Location;
|
||||
use typst_library::introspection::{self, Location};
|
||||
use typst_library::layout::{
|
||||
Abs, Frame, FrameItem, GroupItem, PagedDocument, Size, Transform,
|
||||
};
|
||||
@ -30,6 +31,7 @@ use crate::metadata::build_metadata;
|
||||
use crate::outline::build_outline;
|
||||
use crate::page::PageLabelExt;
|
||||
use crate::shape::handle_shape;
|
||||
use crate::tags::{handle_close_tag, handle_open_tag, Placeholder, TagNode, Tags};
|
||||
use crate::text::handle_text;
|
||||
use crate::util::{convert_path, display_font, AbsExt, TransformExt};
|
||||
use crate::PdfOptions;
|
||||
@ -39,14 +41,19 @@ pub fn convert(
|
||||
typst_document: &PagedDocument,
|
||||
options: &PdfOptions,
|
||||
) -> SourceResult<Vec<u8>> {
|
||||
// HACK
|
||||
// let config = Configuration::new();
|
||||
let config = Configuration::new_with_validator(Validator::UA1);
|
||||
let settings = SerializeSettings {
|
||||
compress_content_streams: true,
|
||||
no_device_cs: true,
|
||||
ascii_compatible: false,
|
||||
xmp_metadata: true,
|
||||
cmyk_profile: None,
|
||||
configuration: options.standards.config,
|
||||
enable_tagging: false,
|
||||
configuration: config,
|
||||
// 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,
|
||||
};
|
||||
|
||||
@ -54,6 +61,7 @@ pub fn convert(
|
||||
let page_index_converter = PageIndexConverter::new(typst_document, options);
|
||||
let named_destinations =
|
||||
collect_named_destinations(typst_document, &page_index_converter);
|
||||
|
||||
let mut gc = GlobalContext::new(
|
||||
typst_document,
|
||||
options,
|
||||
@ -66,6 +74,7 @@ pub fn convert(
|
||||
|
||||
document.set_outline(build_outline(&gc));
|
||||
document.set_metadata(build_metadata(&gc));
|
||||
document.set_tag_tree(gc.tags.build_tree());
|
||||
|
||||
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 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(
|
||||
&mut fc,
|
||||
&typst_page.frame,
|
||||
@ -113,10 +135,16 @@ fn convert_pages(gc: &mut GlobalContext, document: &mut Document) -> SourceResul
|
||||
gc,
|
||||
)?;
|
||||
|
||||
// Marked-content may not cross page boundaries: close open tag.
|
||||
if !gc.tags.stack.is_empty() {
|
||||
surface.end_tagged();
|
||||
}
|
||||
|
||||
surface.finish();
|
||||
|
||||
for annotation in fc.annotations {
|
||||
page.add_annotation(annotation);
|
||||
for (placeholder, annotation) in fc.annotations {
|
||||
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.
|
||||
pub(crate) struct FrameContext {
|
||||
states: Vec<State>,
|
||||
annotations: Vec<Annotation>,
|
||||
annotations: Vec<(Placeholder, Annotation)>,
|
||||
}
|
||||
|
||||
impl FrameContext {
|
||||
@ -198,8 +226,12 @@ impl FrameContext {
|
||||
self.states.last_mut().unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn push_annotation(&mut self, annotation: Annotation) {
|
||||
self.annotations.push(annotation);
|
||||
pub(crate) fn 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.
|
||||
pub(crate) languages: BTreeMap<Lang, usize>,
|
||||
pub(crate) page_index_converter: PageIndexConverter,
|
||||
/// Tagged PDF context.
|
||||
pub(crate) tags: Tags,
|
||||
}
|
||||
|
||||
impl<'a> GlobalContext<'a> {
|
||||
@ -244,6 +278,8 @@ impl<'a> GlobalContext<'a> {
|
||||
image_spans: HashSet::new(),
|
||||
languages: BTreeMap::new(),
|
||||
page_index_converter,
|
||||
|
||||
tags: Tags::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -278,8 +314,15 @@ pub(crate) fn handle_frame(
|
||||
FrameItem::Image(image, size, span) => {
|
||||
handle_image(gc, fc, image, *size, surface, *span)?
|
||||
}
|
||||
FrameItem::Link(d, s) => handle_link(fc, gc, d, *s),
|
||||
FrameItem::Tag(_) => {}
|
||||
FrameItem::Link(alt, dest, size) => {
|
||||
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();
|
||||
|
@ -9,6 +9,7 @@ mod outline;
|
||||
mod page;
|
||||
mod paint;
|
||||
mod shape;
|
||||
mod tags;
|
||||
mod text;
|
||||
mod util;
|
||||
|
||||
|
@ -1,16 +1,18 @@
|
||||
use krilla::action::{Action, LinkAction};
|
||||
use krilla::annotation::{LinkAnnotation, Target};
|
||||
use krilla::annotation::{Annotation, LinkAnnotation, Target};
|
||||
use krilla::destination::XyzDestination;
|
||||
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 crate::convert::{FrameContext, GlobalContext};
|
||||
use crate::tags::TagNode;
|
||||
use crate::util::{AbsExt, PointExt};
|
||||
|
||||
pub(crate) fn handle_link(
|
||||
fc: &mut FrameContext,
|
||||
gc: &mut GlobalContext,
|
||||
alt: Option<String>,
|
||||
dest: &Destination,
|
||||
size: Size,
|
||||
) {
|
||||
@ -44,51 +46,42 @@ pub(crate) fn handle_link(
|
||||
|
||||
// TODO: Support quad points.
|
||||
|
||||
let pos = match dest {
|
||||
let target = match dest {
|
||||
Destination::Url(u) => {
|
||||
fc.push_annotation(
|
||||
LinkAnnotation::new(
|
||||
rect,
|
||||
None,
|
||||
Target::Action(Action::Link(LinkAction::new(u.to_string()))),
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
return;
|
||||
Target::Action(Action::Link(LinkAction::new(u.to_string())))
|
||||
}
|
||||
Destination::Position(p) => *p,
|
||||
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.
|
||||
fc.push_annotation(
|
||||
LinkAnnotation::new(
|
||||
rect,
|
||||
None,
|
||||
Target::Destination(krilla::destination::Destination::Named(
|
||||
nd.clone(),
|
||||
)),
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
return;
|
||||
Target::Destination(krilla::destination::Destination::Named(nd.clone()))
|
||||
} 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;
|
||||
if let Some(index) = gc.page_index_converter.pdf_page_index(page_index) {
|
||||
let placeholder = gc.tags.reserve_placeholder();
|
||||
gc.tags.push(TagNode::Placeholder(placeholder));
|
||||
|
||||
fc.push_annotation(
|
||||
LinkAnnotation::new(
|
||||
rect,
|
||||
None,
|
||||
Target::Destination(krilla::destination::Destination::Xyz(
|
||||
XyzDestination::new(index, pos.point.to_krilla()),
|
||||
)),
|
||||
)
|
||||
.into(),
|
||||
placeholder,
|
||||
Annotation::new_link(LinkAnnotation::new(rect, None, target), alt),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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)))
|
||||
}
|
||||
|
209
crates/typst-pdf/src/tags.rs
Normal file
209
crates/typst-pdf/src/tags.rs
Normal 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));
|
||||
}
|
||||
}
|
@ -167,7 +167,7 @@ fn render_frame(canvas: &mut sk::Pixmap, state: State, frame: &Frame) {
|
||||
FrameItem::Image(image, size, _) => {
|
||||
image::render_image(canvas, state.pre_translate(*pos), image, *size);
|
||||
}
|
||||
FrameItem::Link(_, _) => {}
|
||||
FrameItem::Link(..) => {}
|
||||
FrameItem::Tag(_) => {}
|
||||
}
|
||||
}
|
||||
|
@ -207,7 +207,7 @@ impl SVGRenderer {
|
||||
for (pos, item) in frame.items() {
|
||||
// File size optimization.
|
||||
// TODO: SVGs could contain links, couldn't they?
|
||||
if matches!(item, FrameItem::Link(_, _) | FrameItem::Tag(_)) {
|
||||
if matches!(item, FrameItem::Link(..) | FrameItem::Tag(_)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -228,7 +228,7 @@ impl SVGRenderer {
|
||||
self.render_shape(state.pre_translate(*pos), shape)
|
||||
}
|
||||
FrameItem::Image(image, size, _) => self.render_image(image, size),
|
||||
FrameItem::Link(_, _) => unreachable!(),
|
||||
FrameItem::Link(..) => unreachable!(),
|
||||
FrameItem::Tag(_) => unreachable!(),
|
||||
};
|
||||
|
||||
|
@ -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));
|
||||
render_links(canvas, ts, &group.frame);
|
||||
}
|
||||
FrameItem::Link(_, size) => {
|
||||
FrameItem::Link(_, _, size) => {
|
||||
let w = size.x.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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user