Compare commits

...

4 Commits

Author SHA1 Message Date
Tobias Schmitz
4b57373653
feat: derive Debug for StackEntry 2025-07-14 13:18:43 +02:00
Tobias Schmitz
e43b8bbb7f
fix: out of bounds access when tagging table cells 2025-07-14 12:40:18 +02:00
Tobias Schmitz
9bbfe4c14a
fix: make figure captions sibling elements
if the caption is contained within the figure screen readers might ignore it
2025-07-14 10:46:00 +02:00
Tobias Schmitz
e4021390a3
feat: don't wrap table cell content in paragraph 2025-07-14 10:46:00 +02:00
6 changed files with 33 additions and 22 deletions

View File

@ -294,7 +294,7 @@ const HEADING_RULE: ShowFn<HeadingElem> = |elem, engine, styles| {
const FIGURE_RULE: ShowFn<FigureElem> = |elem, _, styles| {
let span = elem.span();
let mut realized = elem.body.clone();
let mut realized = PdfMarkerTag::FigureBody(elem.body.clone());
// Build the caption, if any.
if let Some(caption) = elem.caption.get_cloned(styles) {

View File

@ -132,6 +132,8 @@ macro_rules! pdf_marker_tag {
pdf_marker_tag! {
/// `TOC`
OutlineBody,
/// `Figure`
FigureBody,
/// `Lbl` (marker) of the list item
ListItemLabel,
/// `LBody` of the enum item

View File

@ -2,11 +2,13 @@ use krilla::tagging::{ListNumbering, TagKind};
use crate::tags::TagNode;
#[derive(Debug)]
pub(crate) struct ListCtx {
numbering: ListNumbering,
items: Vec<ListItem>,
}
#[derive(Debug)]
struct ListItem {
label: Vec<TagNode>,
body: Option<Vec<TagNode>>,

View File

@ -60,6 +60,7 @@ pub(crate) fn handle_start(
push_stack(gc, loc, StackEntryKind::Outline(OutlineCtx::new()))?;
return Ok(());
}
PdfMarkerTagKind::FigureBody => TagKind::Figure.into(),
PdfMarkerTagKind::ListItemLabel => {
push_stack(gc, loc, StackEntryKind::ListItemLabel)?;
return Ok(());
@ -81,8 +82,13 @@ pub(crate) fn handle_start(
push_stack(gc, loc, StackEntryKind::List(ListCtx::new(numbering)))?;
return Ok(());
} else if let Some(_) = elem.to_packed::<FigureElem>() {
let alt = None; // TODO
TagKind::Figure.with_alt_text(alt)
// Wrap the figure tag and the sibling caption in a container, if the
// caption is contained within the figure like recommended for tables
// screen readers might ignore it.
// TODO: maybe this could be a `NonStruct` tag?
TagKind::P.into()
} else if let Some(_) = elem.to_packed::<FigureCaption>() {
TagKind::Caption.into()
} else if let Some(image) = elem.to_packed::<ImageElem>() {
let alt = image.alt.get_as_ref().map(|s| s.to_string());
@ -98,8 +104,6 @@ pub(crate) fn handle_start(
} else {
TagKind::Figure.with_alt_text(alt)
}
} else if let Some(_) = elem.to_packed::<FigureCaption>() {
TagKind::Caption.into()
} else if let Some(table) = elem.to_packed::<TableElem>() {
let table_id = gc.tags.next_table_id();
let summary = table.summary.get_as_ref().map(|s| s.to_string());
@ -391,18 +395,20 @@ impl Tags {
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) struct TableId(u32);
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) struct LinkId(u32);
#[derive(Debug)]
pub(crate) struct StackEntry {
pub(crate) loc: Location,
pub(crate) kind: StackEntryKind,
pub(crate) nodes: Vec<TagNode>,
}
#[derive(Debug)]
pub(crate) enum StackEntryKind {
Standard(Tag),
Outline(OutlineCtx),

View File

@ -4,6 +4,7 @@ use typst_library::model::OutlineEntry;
use crate::tags::TagNode;
#[derive(Debug)]
pub(crate) struct OutlineCtx {
stack: Vec<OutlineSection>,
}
@ -52,6 +53,7 @@ impl OutlineCtx {
}
}
#[derive(Debug)]
pub(crate) struct OutlineSection {
entries: Vec<TagNode>,
}

View File

@ -12,15 +12,17 @@ use typst_library::pdf::{TableCellKind, TableHeaderScope};
use crate::tags::{TableId, TagNode};
#[derive(Debug)]
pub(crate) struct TableCtx {
pub(crate) id: TableId,
pub(crate) summary: Option<String>,
rows: Vec<Vec<GridCell>>,
min_width: usize,
}
impl TableCtx {
pub(crate) fn new(id: TableId, summary: Option<String>) -> Self {
Self { id, summary, rows: Vec::new() }
Self { id, summary, rows: Vec::new(), min_width: 0 }
}
fn get(&self, x: usize, y: usize) -> Option<&TableCtxCell> {
@ -64,14 +66,15 @@ impl TableCtx {
// Extend the table grid to fit this cell.
let required_height = y + rowspan.get();
let required_width = x + colspan.get();
self.min_width = self.min_width.max(x + colspan.get());
if self.rows.len() < required_height {
self.rows
.resize(required_height, vec![GridCell::Missing; required_width]);
.resize(required_height, vec![GridCell::Missing; self.min_width]);
}
for row in self.rows.iter_mut() {
if row.len() < self.min_width {
row.resize_with(self.min_width, || GridCell::Missing);
}
let row = &mut self.rows[y];
if row.len() < required_width {
row.resize_with(required_width, || GridCell::Missing);
}
// Store references to the cell for all spanned cells.
@ -180,11 +183,7 @@ impl TableCtx {
)
.into(),
};
// Wrap content in a paragraph.
// TODO: maybe avoid nested paragraphs?
let par = TagNode::Group(TagKind::P.into(), cell.nodes);
Some(TagNode::Group(tag, vec![par]))
Some(TagNode::Group(tag, cell.nodes))
})
.collect();
@ -254,7 +253,7 @@ impl TableCtx {
}
}
#[derive(Clone, Default)]
#[derive(Clone, Debug, Default)]
enum GridCell {
Cell(TableCtxCell),
Spanned(usize, usize),
@ -288,7 +287,7 @@ impl GridCell {
}
}
#[derive(Clone)]
#[derive(Clone, Debug)]
struct TableCtxCell {
x: u32,
y: u32,
@ -422,7 +421,7 @@ mod tests {
TagNode::Group(
TagKind::TH(TableHeaderCell::new(scope).with_headers(TagIdRefs { ids }))
.with_id(Some(id)),
vec![TagNode::Group(TagKind::P.into(), Vec::new())],
Vec::new(),
)
}
@ -433,7 +432,7 @@ mod tests {
.collect();
TagNode::Group(
TagKind::TD(TableDataCell::new().with_headers(TagIdRefs { ids })).into(),
vec![TagNode::Group(TagKind::P.into(), Vec::new())],
Vec::new(),
)
}