mirror of
https://github.com/typst/typst
synced 2025-08-23 03:04:14 +08:00
Compare commits
4 Commits
e307a44477
...
46229e73ba
Author | SHA1 | Date | |
---|---|---|---|
|
46229e73ba | ||
|
1fa54b751a | ||
|
5a472f77af | ||
|
c6e4059728 |
@ -273,6 +273,7 @@ impl Show for Packed<OutlineElem> {
|
||||
let depth = self.depth(styles).unwrap_or(NonZeroUsize::MAX);
|
||||
|
||||
// Build the outline entries.
|
||||
let mut entries = vec![];
|
||||
for elem in elems {
|
||||
let Some(outlinable) = elem.with::<dyn Outlinable>() else {
|
||||
bail!(span, "cannot outline {}", elem.func().name());
|
||||
@ -281,10 +282,13 @@ impl Show for Packed<OutlineElem> {
|
||||
let level = outlinable.level();
|
||||
if outlinable.outlined() && level <= depth {
|
||||
let entry = OutlineEntry::new(level, elem);
|
||||
seq.push(entry.pack().spanned(span));
|
||||
entries.push(entry.pack().spanned(span));
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap the entries into a marker for pdf tagging.
|
||||
seq.push(OutlineBody::new(Content::sequence(entries)).pack());
|
||||
|
||||
Ok(Content::sequence(seq))
|
||||
}
|
||||
}
|
||||
@ -307,6 +311,19 @@ impl LocalName for Packed<OutlineElem> {
|
||||
const KEY: &'static str = "outline";
|
||||
}
|
||||
|
||||
/// Only used to mark
|
||||
#[elem(Locatable, Show)]
|
||||
pub struct OutlineBody {
|
||||
#[required]
|
||||
body: Content,
|
||||
}
|
||||
|
||||
impl Show for Packed<OutlineBody> {
|
||||
fn show(&self, _: &mut Engine, _: StyleChain) -> SourceResult<Content> {
|
||||
Ok(self.body.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines how an outline is indented.
|
||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||
pub enum OutlineIndent {
|
||||
|
@ -2,6 +2,7 @@ use std::num::{NonZeroU32, NonZeroUsize};
|
||||
use std::sync::Arc;
|
||||
|
||||
use ecow::EcoString;
|
||||
use typst_macros::Cast;
|
||||
use typst_utils::NonZeroExt;
|
||||
|
||||
use crate::diag::{bail, HintedStrResult, HintedString, SourceResult};
|
||||
@ -361,15 +362,40 @@ fn show_cellgrid_html(grid: CellGrid, styles: StyleChain) -> Content {
|
||||
|
||||
impl Show for Packed<TableElem> {
|
||||
fn show(&self, engine: &mut Engine, styles: StyleChain) -> SourceResult<Content> {
|
||||
Ok(if TargetElem::target_in(styles).is_html() {
|
||||
let elem = if TargetElem::target_in(styles).is_html() {
|
||||
// TODO: This is a hack, it is not clear whether the locator is actually used by HTML.
|
||||
// How can we find out whether locator is actually used?
|
||||
let locator = Locator::root();
|
||||
show_cellgrid_html(table_to_cellgrid(self, engine, locator, styles)?, styles)
|
||||
} else {
|
||||
BlockElem::multi_layouter(self.clone(), engine.routines.layout_table).pack()
|
||||
let children = self
|
||||
.children
|
||||
.iter()
|
||||
.map(|c| match c.clone() {
|
||||
TableChild::Header(header) => {
|
||||
let mut header = header.unpack();
|
||||
header.children = header
|
||||
.children
|
||||
.into_iter()
|
||||
.map(|item| match item {
|
||||
TableItem::Cell(cell) => {
|
||||
let mut cell = cell.unpack();
|
||||
cell.push_header_scope(Smart::Custom(TableHeaderScope::Column));
|
||||
TableItem::Cell(Packed::new(cell))
|
||||
}
|
||||
.spanned(self.span()))
|
||||
item => item,
|
||||
})
|
||||
.collect();
|
||||
TableChild::Header(Packed::new(header))
|
||||
}
|
||||
child => child,
|
||||
})
|
||||
.collect();
|
||||
let mut table = self.clone().unpack();
|
||||
table.children = children;
|
||||
BlockElem::multi_layouter(Packed::new(table), engine.routines.layout_table).pack()
|
||||
};
|
||||
Ok(elem.spanned(self.span()))
|
||||
}
|
||||
}
|
||||
|
||||
@ -810,6 +836,9 @@ pub struct TableCell {
|
||||
#[fold]
|
||||
pub stroke: Sides<Option<Option<Arc<Stroke>>>>,
|
||||
|
||||
// TODO: feature gate
|
||||
pub header_scope: Smart<TableHeaderScope>,
|
||||
|
||||
/// Whether rows spanned by this cell can be placed in different pages.
|
||||
/// When equal to `{auto}`, a cell spanning only fixed-size rows is
|
||||
/// unbreakable, while a cell spanning at least one `{auto}`-sized row is
|
||||
@ -847,3 +876,10 @@ impl From<Content> for TableCell {
|
||||
value.unpack::<Self>().unwrap_or_else(Self::new)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
|
||||
pub enum TableHeaderScope {
|
||||
Both,
|
||||
Column,
|
||||
Row,
|
||||
}
|
||||
|
@ -171,14 +171,14 @@ impl State {
|
||||
/// Context needed for converting a single frame.
|
||||
pub(crate) struct FrameContext {
|
||||
states: Vec<State>,
|
||||
pub(crate) link_annotations: HashMap<tags::LinkId, LinkAnnotation>,
|
||||
link_annotations: Vec<LinkAnnotation>,
|
||||
}
|
||||
|
||||
impl FrameContext {
|
||||
pub(crate) fn new(size: Size) -> Self {
|
||||
Self {
|
||||
states: vec![State::new(size)],
|
||||
link_annotations: HashMap::new(),
|
||||
link_annotations: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,6 +197,20 @@ impl FrameContext {
|
||||
pub(crate) fn state_mut(&mut self) -> &mut State {
|
||||
self.states.last_mut().unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn get_link_annotation(
|
||||
&mut self,
|
||||
link_id: tags::LinkId,
|
||||
) -> Option<&mut LinkAnnotation> {
|
||||
self.link_annotations
|
||||
.iter_mut()
|
||||
.rev()
|
||||
.find(|annot| annot.id == link_id)
|
||||
}
|
||||
|
||||
pub(crate) fn push_link_annotation(&mut self, annotation: LinkAnnotation) {
|
||||
self.link_annotations.push(annotation);
|
||||
}
|
||||
}
|
||||
|
||||
/// Globally needed context for converting a typst document.
|
||||
|
@ -1,18 +1,18 @@
|
||||
use std::collections::hash_map::Entry;
|
||||
|
||||
use ecow::EcoString;
|
||||
use krilla::action::{Action, LinkAction};
|
||||
use krilla::annotation::Target;
|
||||
use krilla::configure::Validator;
|
||||
use krilla::destination::XyzDestination;
|
||||
use krilla::geom as kg;
|
||||
use typst_library::layout::{Abs, Point, Position, Size};
|
||||
use typst_library::model::Destination;
|
||||
|
||||
use crate::convert::{FrameContext, GlobalContext};
|
||||
use crate::tags::{Placeholder, StackEntryKind, TagNode};
|
||||
use crate::tags::{self, Placeholder, StackEntryKind, TagNode};
|
||||
use crate::util::{AbsExt, PointExt};
|
||||
|
||||
pub(crate) struct LinkAnnotation {
|
||||
pub(crate) id: tags::LinkId,
|
||||
pub(crate) placeholder: Placeholder,
|
||||
pub(crate) alt: Option<String>,
|
||||
pub(crate) rect: kg::Rect,
|
||||
@ -50,7 +50,7 @@ pub(crate) fn handle_link(
|
||||
};
|
||||
|
||||
let entry = gc.tags.stack.last_mut().expect("a link parent");
|
||||
let StackEntryKind::Link(link_id, link) = &entry.kind else {
|
||||
let StackEntryKind::Link(link_id, ref link) = entry.kind else {
|
||||
unreachable!("expected a link parent")
|
||||
};
|
||||
let alt = link.alt.as_ref().map(EcoString::to_string);
|
||||
@ -58,18 +58,21 @@ pub(crate) fn handle_link(
|
||||
let rect = to_rect(fc, size);
|
||||
let quadpoints = quadpoints(rect);
|
||||
|
||||
match fc.link_annotations.entry(*link_id) {
|
||||
Entry::Occupied(occupied) => {
|
||||
// Update the bounding box and add the quadpoints of an existing link annotation.
|
||||
let annotation = occupied.into_mut();
|
||||
// Unfortunately quadpoints still aren't well supported by most PDF readers,
|
||||
// even by acrobat. Which is understandable since they were only introduced
|
||||
// in PDF 1.6 (2005) /s
|
||||
let should_use_quadpoints = gc.options.standards.config.validator() == Validator::UA1;
|
||||
match fc.get_link_annotation(link_id) {
|
||||
Some(annotation) if should_use_quadpoints => {
|
||||
// Update the bounding box and add the quadpoints to an existing link annotation.
|
||||
annotation.rect = bounding_rect(annotation.rect, rect);
|
||||
annotation.quad_points.extend_from_slice(&quadpoints);
|
||||
}
|
||||
Entry::Vacant(vacant) => {
|
||||
_ => {
|
||||
let placeholder = gc.tags.reserve_placeholder();
|
||||
gc.tags.push(TagNode::Placeholder(placeholder));
|
||||
|
||||
vacant.insert(LinkAnnotation {
|
||||
fc.push_link_annotation(LinkAnnotation {
|
||||
id: link_id,
|
||||
placeholder,
|
||||
rect,
|
||||
quad_points: quadpoints.to_vec(),
|
||||
|
@ -1,19 +1,18 @@
|
||||
use std::cell::OnceCell;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use ecow::EcoString;
|
||||
use krilla::page::Page;
|
||||
use krilla::surface::Surface;
|
||||
use krilla::tagging::{
|
||||
ArtifactType, ContentTag, Identifier, Node, SpanTag, TableCellSpan, TableDataCell,
|
||||
TableHeaderCell, TableHeaderScope, Tag, TagBuilder, TagGroup, TagKind, TagTree,
|
||||
TableHeaderCell, Tag, TagBuilder, TagGroup, TagKind, TagTree,
|
||||
};
|
||||
use typst_library::foundations::{Content, LinkMarker, Packed, StyleChain};
|
||||
use typst_library::foundations::{Content, LinkMarker, Packed, StyleChain, Smart};
|
||||
use typst_library::introspection::Location;
|
||||
use typst_library::layout::RepeatElem;
|
||||
use typst_library::model::{
|
||||
Destination, FigureCaption, FigureElem, HeadingElem, Outlinable, OutlineElem,
|
||||
OutlineEntry, TableCell, TableElem,
|
||||
Destination, FigureCaption, FigureElem, HeadingElem, Outlinable, OutlineBody,
|
||||
OutlineEntry, TableCell, TableElem, TableHeaderScope,
|
||||
};
|
||||
use typst_library::pdf::{ArtifactElem, ArtifactKind, PdfTagElem, PdfTagKind};
|
||||
use typst_library::visualize::ImageElem;
|
||||
@ -41,11 +40,11 @@ pub(crate) struct StackEntry {
|
||||
|
||||
pub(crate) enum StackEntryKind {
|
||||
Standard(Tag),
|
||||
Link(LinkId, Packed<LinkMarker>),
|
||||
Outline(OutlineCtx),
|
||||
OutlineEntry(Packed<OutlineEntry>),
|
||||
Table(TableCtx),
|
||||
TableCell(Packed<TableCell>),
|
||||
Link(LinkId, Packed<LinkMarker>),
|
||||
}
|
||||
|
||||
impl StackEntryKind {
|
||||
@ -142,10 +141,14 @@ impl TableCtx {
|
||||
|
||||
// TODO: possibly set internal field on TableCell when resolving
|
||||
// the cell grid.
|
||||
let is_header = false;
|
||||
let header_scope = cell.header_scope(StyleChain::default());
|
||||
let span = TableCellSpan { rows: rowspan as i32, cols: colspan as i32 };
|
||||
let tag = if is_header {
|
||||
let scope = TableHeaderScope::Column; // TODO
|
||||
let tag = if let Smart::Custom(scope) = header_scope {
|
||||
let scope = match scope {
|
||||
TableHeaderScope::Both => krilla::tagging::TableHeaderScope::Both,
|
||||
TableHeaderScope::Column => krilla::tagging::TableHeaderScope::Column,
|
||||
TableHeaderScope::Row => krilla::tagging::TableHeaderScope::Row,
|
||||
};
|
||||
TagKind::TH(TableHeaderCell::new(scope).with_span(span))
|
||||
} else {
|
||||
TagKind::TD(TableDataCell::new().with_span(span))
|
||||
@ -168,14 +171,37 @@ impl TableCtx {
|
||||
fn build_table(self, mut nodes: Vec<TagNode>) -> Vec<TagNode> {
|
||||
// Table layouting ensures that there are no overlapping cells, and that
|
||||
// any gaps left by the user are filled with empty cells.
|
||||
let mut row_chunk = Vec::new();
|
||||
let mut is_header_chunk = false;
|
||||
for row in self.rows.into_iter() {
|
||||
let mut row_nodes = Vec::new();
|
||||
for (_, tag, nodes) in row.into_iter().flatten() {
|
||||
row_nodes.push(TagNode::Group(tag, nodes));
|
||||
let mut is_header_row = true;
|
||||
let row_nodes = row
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|(cell, tag, nodes)| {
|
||||
is_header_row &= cell.header_scope(StyleChain::default()).is_custom();
|
||||
TagNode::Group(tag, nodes)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let row = TagNode::Group(TagKind::TR.into(), row_nodes);
|
||||
if row_chunk.is_empty() {
|
||||
is_header_chunk = is_header_row;
|
||||
row_chunk.push(row);
|
||||
} else if is_header_chunk == is_header_row {
|
||||
row_chunk.push(row);
|
||||
} else {
|
||||
let tag = if is_header_chunk { TagKind::THead } else { TagKind::TBody };
|
||||
nodes.push(TagNode::Group(tag.into(), std::mem::take(&mut row_chunk)));
|
||||
|
||||
is_header_chunk = is_header_row;
|
||||
row_chunk.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: generate `THead`, `TBody`, and `TFoot`
|
||||
nodes.push(TagNode::Group(TagKind::TR.into(), row_nodes));
|
||||
if !row_chunk.is_empty() {
|
||||
let tag = if is_header_chunk { TagKind::THead } else { TagKind::TBody };
|
||||
nodes.push(TagNode::Group(tag.into(), row_chunk));
|
||||
}
|
||||
|
||||
nodes
|
||||
@ -335,10 +361,11 @@ fn start_content<'a, 'b>(
|
||||
pub(crate) fn add_annotations(
|
||||
gc: &mut GlobalContext,
|
||||
page: &mut Page,
|
||||
annotations: HashMap<LinkId, LinkAnnotation>,
|
||||
annotations: Vec<LinkAnnotation>,
|
||||
) {
|
||||
for annotation in annotations.into_values() {
|
||||
let LinkAnnotation { placeholder, alt, rect, quad_points, target } = annotation;
|
||||
for annotation in annotations.into_iter() {
|
||||
let LinkAnnotation { id: _, placeholder, alt, rect, quad_points, target } =
|
||||
annotation;
|
||||
let annot = krilla::annotation::Annotation::new_link(
|
||||
krilla::annotation::LinkAnnotation::new(rect, Some(quad_points), target),
|
||||
alt,
|
||||
@ -383,7 +410,7 @@ pub(crate) fn handle_start(gc: &mut GlobalContext, elem: &Content) {
|
||||
// TODO: when targeting PDF 2.0 headings `> 6` are supported
|
||||
_ => TagKind::H6(Some(name)).into(),
|
||||
}
|
||||
} else if let Some(_) = elem.to_packed::<OutlineElem>() {
|
||||
} else if let Some(_) = elem.to_packed::<OutlineBody>() {
|
||||
push_stack(gc, loc, StackEntryKind::Outline(OutlineCtx::new()));
|
||||
return;
|
||||
} else if let Some(entry) = elem.to_packed::<OutlineEntry>() {
|
||||
@ -409,16 +436,16 @@ pub(crate) fn handle_start(gc: &mut GlobalContext, elem: &Content) {
|
||||
}
|
||||
} else if let Some(_) = elem.to_packed::<FigureCaption>() {
|
||||
TagKind::Caption.into()
|
||||
} else if let Some(link) = elem.to_packed::<LinkMarker>() {
|
||||
let link_id = gc.tags.next_link_id();
|
||||
push_stack(gc, loc, StackEntryKind::Link(link_id, link.clone()));
|
||||
return;
|
||||
} else if let Some(table) = elem.to_packed::<TableElem>() {
|
||||
push_stack(gc, loc, StackEntryKind::Table(TableCtx::new(table.clone())));
|
||||
return;
|
||||
} else if let Some(cell) = elem.to_packed::<TableCell>() {
|
||||
push_stack(gc, loc, StackEntryKind::TableCell(cell.clone()));
|
||||
return;
|
||||
} else if let Some(link) = elem.to_packed::<LinkMarker>() {
|
||||
let link_id = gc.tags.next_link_id();
|
||||
push_stack(gc, loc, StackEntryKind::Link(link_id, link.clone()));
|
||||
return;
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
@ -448,16 +475,6 @@ pub(crate) fn handle_end(gc: &mut GlobalContext, loc: Location) {
|
||||
|
||||
let node = match entry.kind {
|
||||
StackEntryKind::Standard(tag) => TagNode::Group(tag, entry.nodes),
|
||||
StackEntryKind::Link(_, link) => {
|
||||
let alt = link.alt.as_ref().map(EcoString::to_string);
|
||||
let tag = TagKind::Link.with_alt_text(alt);
|
||||
let mut node = TagNode::Group(tag, entry.nodes);
|
||||
// Wrap link in reference tag, if it's not a url.
|
||||
if let Destination::Position(_) | Destination::Location(_) = link.dest {
|
||||
node = TagNode::Group(TagKind::Reference.into(), vec![node]);
|
||||
}
|
||||
node
|
||||
}
|
||||
StackEntryKind::Outline(ctx) => {
|
||||
let nodes = ctx.build_outline(entry.nodes);
|
||||
TagNode::Group(TagKind::TOC.into(), nodes)
|
||||
@ -487,6 +504,16 @@ pub(crate) fn handle_end(gc: &mut GlobalContext, loc: Location) {
|
||||
|
||||
return;
|
||||
}
|
||||
StackEntryKind::Link(_, link) => {
|
||||
let alt = link.alt.as_ref().map(EcoString::to_string);
|
||||
let tag = TagKind::Link.with_alt_text(alt);
|
||||
let mut node = TagNode::Group(tag, entry.nodes);
|
||||
// Wrap link in reference tag, if it's not a url.
|
||||
if let Destination::Position(_) | Destination::Location(_) = link.dest {
|
||||
node = TagNode::Group(TagKind::Reference.into(), vec![node]);
|
||||
}
|
||||
node
|
||||
}
|
||||
};
|
||||
|
||||
gc.tags.push(node);
|
||||
|
Loading…
x
Reference in New Issue
Block a user