mirror of
https://github.com/typst/typst
synced 2025-06-15 00:26:26 +08:00
feat: [WIP] include links in tag tree
skip-checks:true
This commit is contained in:
parent
8c861d2d27
commit
5912b1e6f1
@ -388,8 +388,6 @@ impl IntrospectorBuilder {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
dbg!(elems.len());
|
|
||||||
|
|
||||||
self.finalize(elems)
|
self.finalize(elems)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +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, Tags};
|
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;
|
||||||
@ -42,6 +42,7 @@ pub fn convert(
|
|||||||
options: &PdfOptions,
|
options: &PdfOptions,
|
||||||
) -> SourceResult<Vec<u8>> {
|
) -> SourceResult<Vec<u8>> {
|
||||||
// HACK
|
// HACK
|
||||||
|
// let config = Configuration::new();
|
||||||
let config = Configuration::new_with_validator(Validator::UA1);
|
let config = Configuration::new_with_validator(Validator::UA1);
|
||||||
let settings = SerializeSettings {
|
let settings = SerializeSettings {
|
||||||
compress_content_streams: true,
|
compress_content_streams: true,
|
||||||
@ -73,7 +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.take_tree());
|
document.set_tag_tree(gc.tags.build_tree());
|
||||||
|
|
||||||
finish(document, gc, options.standards.config)
|
finish(document, gc, options.standards.config)
|
||||||
}
|
}
|
||||||
@ -123,7 +124,7 @@ fn convert_pages(gc: &mut GlobalContext, document: &mut Document) -> SourceResul
|
|||||||
};
|
};
|
||||||
// TODO: somehow avoid empty marked-content sequences
|
// TODO: somehow avoid empty marked-content sequences
|
||||||
let id = surface.start_tagged(tag);
|
let id = surface.start_tagged(tag);
|
||||||
nodes.push(Node::Leaf(id));
|
nodes.push(TagNode::Leaf(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_frame(
|
handle_frame(
|
||||||
@ -141,8 +142,9 @@ fn convert_pages(gc: &mut GlobalContext, document: &mut Document) -> SourceResul
|
|||||||
|
|
||||||
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -197,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 {
|
||||||
@ -224,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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
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, 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(
|
||||||
@ -44,15 +45,23 @@ pub(crate) fn handle_link(
|
|||||||
|
|
||||||
// TODO: Support quad points.
|
// TODO: Support quad points.
|
||||||
|
|
||||||
|
let placeholder = gc.tags.reserve_placeholder();
|
||||||
|
gc.tags.push(TagNode::Placeholder(placeholder));
|
||||||
|
|
||||||
|
// TODO: add some way to add alt text to annotations.
|
||||||
|
// probably through [typst_layout::modifiers::FrameModifiers]
|
||||||
let pos = match dest {
|
let pos = match dest {
|
||||||
Destination::Url(u) => {
|
Destination::Url(u) => {
|
||||||
fc.push_annotation(
|
fc.push_annotation(
|
||||||
LinkAnnotation::new(
|
placeholder,
|
||||||
rect,
|
Annotation::new_link(
|
||||||
None,
|
LinkAnnotation::new(
|
||||||
Target::Action(Action::Link(LinkAction::new(u.to_string()))),
|
rect,
|
||||||
)
|
None,
|
||||||
.into(),
|
Target::Action(Action::Link(LinkAction::new(u.to_string()))),
|
||||||
|
),
|
||||||
|
Some(u.to_string()),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -62,6 +71,7 @@ pub(crate) fn handle_link(
|
|||||||
// 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(
|
fc.push_annotation(
|
||||||
|
placeholder,
|
||||||
LinkAnnotation::new(
|
LinkAnnotation::new(
|
||||||
rect,
|
rect,
|
||||||
None,
|
None,
|
||||||
@ -81,6 +91,7 @@ pub(crate) fn handle_link(
|
|||||||
let page_index = pos.page.get() - 1;
|
let page_index = pos.page.get() - 1;
|
||||||
if let Some(index) = gc.page_index_converter.pdf_page_index(page_index) {
|
if let Some(index) = gc.page_index_converter.pdf_page_index(page_index) {
|
||||||
fc.push_annotation(
|
fc.push_annotation(
|
||||||
|
placeholder,
|
||||||
LinkAnnotation::new(
|
LinkAnnotation::new(
|
||||||
rect,
|
rect,
|
||||||
None,
|
None,
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
use std::cell::OnceCell;
|
||||||
|
|
||||||
use krilla::surface::Surface;
|
use krilla::surface::Surface;
|
||||||
use krilla::tagging::{ContentTag, Node, Tag, TagGroup, TagTree};
|
use krilla::tagging::{ContentTag, Identifier, Node, Tag, TagGroup, TagTree};
|
||||||
use typst_library::foundations::{Content, StyleChain};
|
use typst_library::foundations::{Content, StyleChain};
|
||||||
use typst_library::introspection::Location;
|
use typst_library::introspection::Location;
|
||||||
use typst_library::model::{HeadingElem, OutlineElem, OutlineEntry};
|
use typst_library::model::{HeadingElem, OutlineElem, OutlineEntry};
|
||||||
@ -8,24 +10,87 @@ use crate::convert::GlobalContext;
|
|||||||
|
|
||||||
pub(crate) struct Tags {
|
pub(crate) struct Tags {
|
||||||
/// The intermediary stack of nested tag groups.
|
/// The intermediary stack of nested tag groups.
|
||||||
pub(crate) stack: Vec<(Location, Tag, Vec<Node>)>,
|
pub(crate) stack: Vec<(Location, Tag, Vec<TagNode>)>,
|
||||||
|
pub(crate) placeholders: Vec<OnceCell<Node>>,
|
||||||
pub(crate) in_artifact: bool,
|
pub(crate) in_artifact: bool,
|
||||||
|
|
||||||
/// The output.
|
/// The output.
|
||||||
pub(crate) tree: TagTree,
|
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 {
|
impl Tags {
|
||||||
pub(crate) fn new() -> Self {
|
pub(crate) fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
stack: Vec::new(),
|
stack: Vec::new(),
|
||||||
|
placeholders: Vec::new(),
|
||||||
in_artifact: false,
|
in_artifact: false,
|
||||||
tree: TagTree::new(),
|
|
||||||
|
tree: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn take_tree(&mut self) -> TagTree {
|
pub(crate) fn reserve_placeholder(&mut self) -> Placeholder {
|
||||||
std::mem::take(&mut self.tree)
|
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 {
|
pub(crate) fn context_supports(&self, tag: &Tag) -> bool {
|
||||||
@ -118,7 +183,7 @@ pub(crate) fn handle_open_tag(
|
|||||||
}
|
}
|
||||||
let content_id = surface.start_tagged(krilla::tagging::ContentTag::Other);
|
let content_id = surface.start_tagged(krilla::tagging::ContentTag::Other);
|
||||||
|
|
||||||
gc.tags.stack.push((loc, tag, vec![Node::Leaf(content_id)]));
|
gc.tags.stack.push((loc, tag, vec![TagNode::Leaf(content_id)]));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn handle_close_tag(
|
pub(crate) fn handle_close_tag(
|
||||||
@ -129,21 +194,16 @@ pub(crate) fn handle_close_tag(
|
|||||||
let Some((_, tag, nodes)) = gc.tags.stack.pop_if(|(l, ..)| l == loc) else {
|
let Some((_, tag, nodes)) = gc.tags.stack.pop_if(|(l, ..)| l == loc) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
// TODO: contstruct group directly from nodes
|
|
||||||
let mut tag_group = TagGroup::new(tag);
|
|
||||||
for node in nodes {
|
|
||||||
tag_group.push(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
surface.end_tagged();
|
surface.end_tagged();
|
||||||
|
|
||||||
if let Some((_, _, parent_nodes)) = gc.tags.stack.last_mut() {
|
if let Some((_, _, parent_nodes)) = gc.tags.stack.last_mut() {
|
||||||
parent_nodes.push(Node::Group(tag_group));
|
parent_nodes.push(TagNode::Group(tag, nodes));
|
||||||
|
|
||||||
// TODO: somehow avoid empty marked-content sequences
|
// TODO: somehow avoid empty marked-content sequences
|
||||||
let id = surface.start_tagged(ContentTag::Other);
|
let id = surface.start_tagged(ContentTag::Other);
|
||||||
parent_nodes.push(Node::Leaf(id));
|
parent_nodes.push(TagNode::Leaf(id));
|
||||||
} else {
|
} else {
|
||||||
gc.tags.tree.push(Node::Group(tag_group));
|
gc.tags.tree.push(TagNode::Group(tag, nodes));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user