mirror of
https://github.com/typst/typst
synced 2025-07-27 22:37:54 +08:00
feat: report spans for missing alt text and unknown/duplicate tag ids
This commit is contained in:
parent
99815f449c
commit
d2105dcc35
4
Cargo.lock
generated
4
Cargo.lock
generated
@ -1373,7 +1373,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "krilla"
|
name = "krilla"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
source = "git+https://github.com/LaurenzV/krilla?branch=main#d40f81a01ca8f8654510a76effeef12518437800"
|
source = "git+https://github.com/LaurenzV/krilla?branch=main#32d070e737cd8ae4c3aa4ff901d15cb22bd052f3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
@ -1402,7 +1402,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "krilla-svg"
|
name = "krilla-svg"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/LaurenzV/krilla?branch=main#d40f81a01ca8f8654510a76effeef12518437800"
|
source = "git+https://github.com/LaurenzV/krilla?branch=main#32d070e737cd8ae4c3aa4ff901d15cb22bd052f3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"flate2",
|
"flate2",
|
||||||
"fontdb",
|
"fontdb",
|
||||||
|
@ -23,10 +23,10 @@ use serde::{Serialize, Serializer};
|
|||||||
use typst_syntax::Span;
|
use typst_syntax::Span;
|
||||||
use typst_utils::singleton;
|
use typst_utils::singleton;
|
||||||
|
|
||||||
use crate::diag::{SourceResult, StrResult};
|
use crate::diag::{bail, SourceResult, StrResult};
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
use crate::foundations::{
|
use crate::foundations::{
|
||||||
func, repr, scope, ty, Context, Dict, IntoValue, Label, Property, Recipe,
|
func, repr, scope, ty, Args, Context, Dict, IntoValue, Label, Property, Recipe,
|
||||||
RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles, Value,
|
RecipeIndex, Repr, Selector, Str, Style, StyleChain, Styles, Value,
|
||||||
};
|
};
|
||||||
use crate::introspection::{Locatable, Location};
|
use crate::introspection::{Locatable, Location};
|
||||||
@ -479,7 +479,7 @@ impl Content {
|
|||||||
/// Link the content somewhere.
|
/// Link the content somewhere.
|
||||||
pub fn linked(self, dest: Destination, alt: Option<EcoString>) -> Self {
|
pub fn linked(self, dest: Destination, alt: Option<EcoString>) -> Self {
|
||||||
let span = self.span();
|
let span = self.span();
|
||||||
LinkMarker::new(self, dest.clone(), alt)
|
LinkMarker::new(self, dest.clone(), alt, span)
|
||||||
.pack()
|
.pack()
|
||||||
.spanned(span)
|
.spanned(span)
|
||||||
.set(LinkElem::current, Some(dest))
|
.set(LinkElem::current, Some(dest))
|
||||||
@ -785,15 +785,27 @@ impl Repr for StyledElem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// An element that associates the body of a link with the destination.
|
/// An element that associates the body of a link with the destination.
|
||||||
#[elem(Locatable)]
|
#[elem(Locatable, Construct)]
|
||||||
pub struct LinkMarker {
|
pub struct LinkMarker {
|
||||||
/// The content.
|
/// The content.
|
||||||
|
#[internal]
|
||||||
#[required]
|
#[required]
|
||||||
pub body: Content,
|
pub body: Content,
|
||||||
|
#[internal]
|
||||||
#[required]
|
#[required]
|
||||||
pub dest: Destination,
|
pub dest: Destination,
|
||||||
|
#[internal]
|
||||||
#[required]
|
#[required]
|
||||||
pub alt: Option<EcoString>,
|
pub alt: Option<EcoString>,
|
||||||
|
#[internal]
|
||||||
|
#[required]
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Construct for LinkMarker {
|
||||||
|
fn construct(_: &mut Engine, args: &mut Args) -> SourceResult<Content> {
|
||||||
|
bail!(args.span, "cannot be constructed manually");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: NativeElement> IntoValue for T {
|
impl<T: NativeElement> IntoValue for T {
|
||||||
|
@ -9,6 +9,7 @@ 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::TagId;
|
||||||
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};
|
||||||
@ -373,11 +374,21 @@ fn finish(
|
|||||||
.collect::<EcoVec<_>>();
|
.collect::<EcoVec<_>>();
|
||||||
Err(errors)
|
Err(errors)
|
||||||
}
|
}
|
||||||
KrillaError::DuplicateTagId(_, _) => {
|
KrillaError::DuplicateTagId(id, loc) => {
|
||||||
unreachable!("duplicate IDs shouldn't be generated")
|
let span = to_span(loc);
|
||||||
|
let id = display_tag_id(&id);
|
||||||
|
bail!(
|
||||||
|
span, "duplicate tag id `{id}`";
|
||||||
|
hint: "please report this as a bug"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
KrillaError::UnknownTagId(_, _) => {
|
KrillaError::UnknownTagId(id, loc) => {
|
||||||
unreachable!("all referenced IDs should be present in the tag tree")
|
let span = to_span(loc);
|
||||||
|
let id = display_tag_id(&id);
|
||||||
|
bail!(
|
||||||
|
span, "unknown tag id `{id}`";
|
||||||
|
hint: "please report this as a bug"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
KrillaError::Image(_, loc) => {
|
KrillaError::Image(_, loc) => {
|
||||||
let span = to_span(loc);
|
let span = to_span(loc);
|
||||||
@ -394,6 +405,20 @@ fn finish(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn display_tag_id(id: &TagId) -> impl std::fmt::Display + use<'_> {
|
||||||
|
typst_utils::display(|f| {
|
||||||
|
if let Ok(str) = std::str::from_utf8(id.as_bytes()) {
|
||||||
|
f.write_str(str)
|
||||||
|
} else {
|
||||||
|
f.write_str("0x")?;
|
||||||
|
for b in id.as_bytes() {
|
||||||
|
write!(f, "{b:x}")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Converts a krilla error into a Typst error.
|
/// Converts a krilla error into a Typst error.
|
||||||
fn convert_error(
|
fn convert_error(
|
||||||
gc: &GlobalContext,
|
gc: &GlobalContext,
|
||||||
@ -562,16 +587,20 @@ fn convert_error(
|
|||||||
}
|
}
|
||||||
// The below errors cannot occur yet, only once Typst supports full PDF/A
|
// The below errors cannot occur yet, only once Typst supports full PDF/A
|
||||||
// and PDF/UA. But let's still add a message just to be on the safe side.
|
// and PDF/UA. But let's still add a message just to be on the safe side.
|
||||||
ValidationError::MissingAnnotationAltText => error!(
|
ValidationError::MissingAnnotationAltText(loc) => {
|
||||||
Span::detached(),
|
let span = to_span(*loc);
|
||||||
"{prefix} missing annotation alt text";
|
error!(
|
||||||
hint: "please report this as a bug"
|
span, "{prefix} missing annotation alt text";
|
||||||
),
|
hint: "please report this as a bug"
|
||||||
ValidationError::MissingAltText => error!(
|
)
|
||||||
Span::detached(),
|
}
|
||||||
"{prefix} missing alt text";
|
ValidationError::MissingAltText(loc) => {
|
||||||
hint: "make sure your images and equations have alt text"
|
let span = to_span(*loc);
|
||||||
),
|
error!(
|
||||||
|
span, "{prefix} missing alt text";
|
||||||
|
hint: "make sure your images and equations have alt text"
|
||||||
|
)
|
||||||
|
}
|
||||||
ValidationError::NoDocumentLanguage => error!(
|
ValidationError::NoDocumentLanguage => error!(
|
||||||
Span::detached(),
|
Span::detached(),
|
||||||
"{prefix} missing document language";
|
"{prefix} missing document language";
|
||||||
|
@ -6,6 +6,7 @@ use krilla::destination::XyzDestination;
|
|||||||
use krilla::geom as kg;
|
use krilla::geom as kg;
|
||||||
use typst_library::layout::{Point, Position, Size};
|
use typst_library::layout::{Point, Position, Size};
|
||||||
use typst_library::model::Destination;
|
use typst_library::model::Destination;
|
||||||
|
use typst_syntax::Span;
|
||||||
|
|
||||||
use crate::convert::{FrameContext, GlobalContext};
|
use crate::convert::{FrameContext, GlobalContext};
|
||||||
use crate::tags::{self, Placeholder, TagNode};
|
use crate::tags::{self, Placeholder, TagNode};
|
||||||
@ -17,6 +18,7 @@ pub(crate) struct LinkAnnotation {
|
|||||||
pub(crate) alt: Option<String>,
|
pub(crate) alt: Option<String>,
|
||||||
pub(crate) quad_points: Vec<kg::Quadrilateral>,
|
pub(crate) quad_points: Vec<kg::Quadrilateral>,
|
||||||
pub(crate) target: Target,
|
pub(crate) target: Target,
|
||||||
|
pub(crate) span: Span,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn handle_link(
|
pub(crate) fn handle_link(
|
||||||
@ -70,6 +72,7 @@ pub(crate) fn handle_link(
|
|||||||
quad_points: vec![quad],
|
quad_points: vec![quad],
|
||||||
alt,
|
alt,
|
||||||
target,
|
target,
|
||||||
|
span: link.span,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -176,6 +176,7 @@ pub(crate) fn handle_start(
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let tag = tag.with_location(Some(elem.span().into_raw().get()));
|
||||||
push_stack(gc, loc, StackEntryKind::Standard(tag))?;
|
push_stack(gc, loc, StackEntryKind::Standard(tag))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -202,7 +203,8 @@ pub(crate) fn handle_end(gc: &mut GlobalContext, surface: &mut Surface, loc: Loc
|
|||||||
// PDF/UA compliance of the structure hierarchy is checked
|
// PDF/UA compliance of the structure hierarchy is checked
|
||||||
// elsewhere. While this doesn't make a lot of sense, just
|
// elsewhere. While this doesn't make a lot of sense, just
|
||||||
// avoid crashing here.
|
// avoid crashing here.
|
||||||
let tag = TagKind::TOCI.into();
|
let tag = TagKind::TOCI
|
||||||
|
.with_location(Some(outline_entry.span().into_raw().get()));
|
||||||
gc.tags.push(TagNode::Group(tag, entry.nodes));
|
gc.tags.push(TagNode::Group(tag, entry.nodes));
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@ -216,7 +218,8 @@ pub(crate) fn handle_end(gc: &mut GlobalContext, surface: &mut Surface, loc: Loc
|
|||||||
// PDF/UA compliance of the structure hierarchy is checked
|
// PDF/UA compliance of the structure hierarchy is checked
|
||||||
// elsewhere. While this doesn't make a lot of sense, just
|
// elsewhere. While this doesn't make a lot of sense, just
|
||||||
// avoid crashing here.
|
// avoid crashing here.
|
||||||
let tag = TagKind::TD(TableDataCell::new()).into();
|
let tag = TagKind::TD(TableDataCell::new())
|
||||||
|
.with_location(Some(cell.span().into_raw().get()));
|
||||||
gc.tags.push(TagNode::Group(tag, entry.nodes));
|
gc.tags.push(TagNode::Group(tag, entry.nodes));
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@ -324,11 +327,13 @@ pub(crate) fn add_annotations(
|
|||||||
annotations: Vec<LinkAnnotation>,
|
annotations: Vec<LinkAnnotation>,
|
||||||
) {
|
) {
|
||||||
for annotation in annotations.into_iter() {
|
for annotation in annotations.into_iter() {
|
||||||
let LinkAnnotation { id: _, placeholder, alt, quad_points, target } = annotation;
|
let LinkAnnotation { id: _, placeholder, alt, quad_points, target, span } =
|
||||||
|
annotation;
|
||||||
let annot = krilla::annotation::Annotation::new_link(
|
let annot = krilla::annotation::Annotation::new_link(
|
||||||
krilla::annotation::LinkAnnotation::new_with_quad_points(quad_points, target),
|
krilla::annotation::LinkAnnotation::new_with_quad_points(quad_points, target),
|
||||||
alt,
|
alt,
|
||||||
);
|
)
|
||||||
|
.with_location(Some(span.into_raw().get()));
|
||||||
let annot_id = page.add_tagged_annotation(annot);
|
let annot_id = page.add_tagged_annotation(annot);
|
||||||
gc.tags.placeholders.init(placeholder, Node::Leaf(annot_id));
|
gc.tags.placeholders.init(placeholder, Node::Leaf(annot_id));
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ use smallvec::SmallVec;
|
|||||||
use typst_library::foundations::{Packed, Smart, StyleChain};
|
use typst_library::foundations::{Packed, Smart, StyleChain};
|
||||||
use typst_library::model::TableCell;
|
use typst_library::model::TableCell;
|
||||||
use typst_library::pdf::{TableCellKind, TableHeaderScope};
|
use typst_library::pdf::{TableCellKind, TableHeaderScope};
|
||||||
|
use typst_syntax::Span;
|
||||||
|
|
||||||
use crate::tags::{TableId, TagNode};
|
use crate::tags::{TableId, TagNode};
|
||||||
|
|
||||||
@ -57,7 +58,7 @@ impl TableCtx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn insert(&mut self, cell: &TableCell, nodes: Vec<TagNode>) {
|
pub(crate) fn insert(&mut self, cell: &Packed<TableCell>, nodes: Vec<TagNode>) {
|
||||||
let x = cell.x.get(StyleChain::default()).unwrap_or_else(|| unreachable!());
|
let x = cell.x.get(StyleChain::default()).unwrap_or_else(|| unreachable!());
|
||||||
let y = cell.y.get(StyleChain::default()).unwrap_or_else(|| unreachable!());
|
let y = cell.y.get(StyleChain::default()).unwrap_or_else(|| unreachable!());
|
||||||
let rowspan = cell.rowspan.get(StyleChain::default());
|
let rowspan = cell.rowspan.get(StyleChain::default());
|
||||||
@ -92,6 +93,7 @@ impl TableCtx {
|
|||||||
kind,
|
kind,
|
||||||
headers: SmallVec::new(),
|
headers: SmallVec::new(),
|
||||||
nodes,
|
nodes,
|
||||||
|
span: cell.span(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,13 +177,14 @@ impl TableCtx {
|
|||||||
.with_headers(cell.headers),
|
.with_headers(cell.headers),
|
||||||
)
|
)
|
||||||
.with_id(Some(id))
|
.with_id(Some(id))
|
||||||
|
.with_location(Some(cell.span.into_raw().get()))
|
||||||
}
|
}
|
||||||
TableCellKind::Footer | TableCellKind::Data => TagKind::TD(
|
TableCellKind::Footer | TableCellKind::Data => TagKind::TD(
|
||||||
TableDataCell::new()
|
TableDataCell::new()
|
||||||
.with_span(span)
|
.with_span(span)
|
||||||
.with_headers(cell.headers),
|
.with_headers(cell.headers),
|
||||||
)
|
)
|
||||||
.into(),
|
.with_location(Some(cell.span.into_raw().get())),
|
||||||
};
|
};
|
||||||
Some(TagNode::Group(tag, cell.nodes))
|
Some(TagNode::Group(tag, cell.nodes))
|
||||||
})
|
})
|
||||||
@ -296,6 +299,7 @@ struct TableCtxCell {
|
|||||||
kind: Smart<TableCellKind>,
|
kind: Smart<TableCellKind>,
|
||||||
headers: SmallVec<[TagId; 1]>,
|
headers: SmallVec<[TagId; 1]>,
|
||||||
nodes: Vec<TagNode>,
|
nodes: Vec<TagNode>,
|
||||||
|
span: Span,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TableCtxCell {
|
impl TableCtxCell {
|
||||||
@ -344,7 +348,7 @@ mod tests {
|
|||||||
fn table<const SIZE: usize>(cells: [TableCell; SIZE]) -> TableCtx {
|
fn table<const SIZE: usize>(cells: [TableCell; SIZE]) -> TableCtx {
|
||||||
let mut table = TableCtx::new(TableId(324), Some("summary".into()));
|
let mut table = TableCtx::new(TableId(324), Some("summary".into()));
|
||||||
for cell in cells {
|
for cell in cells {
|
||||||
table.insert(&cell, Vec::new());
|
table.insert(&Packed::new(cell), Vec::new());
|
||||||
}
|
}
|
||||||
table
|
table
|
||||||
}
|
}
|
||||||
@ -416,7 +420,9 @@ mod tests {
|
|||||||
let id = table_cell_id(TableId(324), x, y);
|
let id = table_cell_id(TableId(324), x, y);
|
||||||
let ids = headers.map(|(x, y)| table_cell_id(TableId(324), x, y));
|
let ids = headers.map(|(x, y)| table_cell_id(TableId(324), x, y));
|
||||||
TagNode::Group(
|
TagNode::Group(
|
||||||
TagKind::TH(TableHeaderCell::new(scope).with_headers(ids)).with_id(Some(id)),
|
TagKind::TH(TableHeaderCell::new(scope).with_headers(ids))
|
||||||
|
.with_id(Some(id))
|
||||||
|
.with_location(Some(Span::detached().into_raw().get())),
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -424,7 +430,8 @@ mod tests {
|
|||||||
fn td<const SIZE: usize>(headers: [(u32, u32); SIZE]) -> TagNode {
|
fn td<const SIZE: usize>(headers: [(u32, u32); SIZE]) -> TagNode {
|
||||||
let ids = headers.map(|(x, y)| table_cell_id(TableId(324), x, y));
|
let ids = headers.map(|(x, y)| table_cell_id(TableId(324), x, y));
|
||||||
TagNode::Group(
|
TagNode::Group(
|
||||||
TagKind::TD(TableDataCell::new().with_headers(ids)).into(),
|
TagKind::TD(TableDataCell::new().with_headers(ids))
|
||||||
|
.with_location(Some(Span::detached().into_raw().get())),
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user