mirror of
https://github.com/typst/typst
synced 2025-08-03 01:37:54 +08:00
feat!: write language attribute on tags and change document language selection
The document language is now inferred from the first non-artifact text item and will always be written.
This commit is contained in:
parent
ded845ec34
commit
e816af11f7
@ -1,4 +1,4 @@
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use ecow::{EcoVec, eco_format};
|
||||
use krilla::configure::{Configuration, ValidationError, Validator};
|
||||
@ -19,7 +19,7 @@ use typst_library::layout::{
|
||||
Abs, Frame, FrameItem, GroupItem, PagedDocument, Size, Transform,
|
||||
};
|
||||
use typst_library::model::HeadingElem;
|
||||
use typst_library::text::{Font, Lang};
|
||||
use typst_library::text::Font;
|
||||
use typst_library::visualize::{Geometry, Paint};
|
||||
use typst_syntax::Span;
|
||||
|
||||
@ -257,8 +257,6 @@ pub(crate) struct GlobalContext<'a> {
|
||||
pub(crate) options: &'a PdfOptions<'a>,
|
||||
/// Mapping between locations in the document and named destinations.
|
||||
pub(crate) loc_to_names: HashMap<Location, NamedDestination>,
|
||||
/// 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,
|
||||
@ -279,7 +277,6 @@ impl<'a> GlobalContext<'a> {
|
||||
loc_to_names,
|
||||
image_to_spans: HashMap::new(),
|
||||
image_spans: HashSet::new(),
|
||||
languages: BTreeMap::new(),
|
||||
page_index_converter,
|
||||
|
||||
tags: Tags::new(),
|
||||
|
@ -1,17 +1,19 @@
|
||||
use ecow::EcoString;
|
||||
use krilla::metadata::{Metadata, TextDirection};
|
||||
use typst_library::foundations::{Datetime, Smart};
|
||||
use typst_library::foundations::{Datetime, Smart, StyleChain};
|
||||
use typst_library::layout::Dir;
|
||||
use typst_library::text::Lang;
|
||||
use typst_library::text::TextElem;
|
||||
|
||||
use crate::convert::GlobalContext;
|
||||
|
||||
pub(crate) fn build_metadata(gc: &GlobalContext) -> Metadata {
|
||||
let creator = format!("Typst {}", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
let lang = gc.languages.iter().max_by_key(|&(_, &count)| count).map(|(&l, _)| l);
|
||||
// Always write a language, PDF/UA-1 implicitly requires a document language
|
||||
// so the metadata and outline entries have an applicable language.
|
||||
let lang = gc.tags.doc_lang.unwrap_or(StyleChain::default().get(TextElem::lang));
|
||||
|
||||
let dir = if lang.map(Lang::dir) == Some(Dir::RTL) {
|
||||
let dir = if lang.dir() == Dir::RTL {
|
||||
TextDirection::RightToLeft
|
||||
} else {
|
||||
TextDirection::LeftToRight
|
||||
@ -20,11 +22,8 @@ pub(crate) fn build_metadata(gc: &GlobalContext) -> Metadata {
|
||||
let mut metadata = Metadata::new()
|
||||
.creator(creator)
|
||||
.keywords(gc.document.info.keywords.iter().map(EcoString::to_string).collect())
|
||||
.authors(gc.document.info.author.iter().map(EcoString::to_string).collect());
|
||||
|
||||
if let Some(lang) = lang {
|
||||
metadata = metadata.language(lang.as_str().to_string());
|
||||
}
|
||||
.authors(gc.document.info.author.iter().map(EcoString::to_string).collect())
|
||||
.language(lang.as_str().to_string());
|
||||
|
||||
if let Some(title) = &gc.document.info.title {
|
||||
metadata = metadata.title(title.to_string());
|
||||
|
@ -1,6 +1,6 @@
|
||||
use krilla::tagging::{ListNumbering, Tag, TagKind};
|
||||
|
||||
use crate::tags::TagNode;
|
||||
use crate::tags::{GroupContents, TagNode};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ListCtx {
|
||||
@ -10,8 +10,8 @@ pub(crate) struct ListCtx {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ListItem {
|
||||
label: Vec<TagNode>,
|
||||
body: Option<Vec<TagNode>>,
|
||||
label: TagNode,
|
||||
body: Option<TagNode>,
|
||||
sub_list: Option<TagNode>,
|
||||
}
|
||||
|
||||
@ -20,11 +20,15 @@ impl ListCtx {
|
||||
Self { numbering, items: Vec::new() }
|
||||
}
|
||||
|
||||
pub(crate) fn push_label(&mut self, nodes: Vec<TagNode>) {
|
||||
self.items.push(ListItem { label: nodes, body: None, sub_list: None });
|
||||
pub(crate) fn push_label(&mut self, contents: GroupContents) {
|
||||
self.items.push(ListItem {
|
||||
label: TagNode::group(Tag::Lbl, contents),
|
||||
body: None,
|
||||
sub_list: None,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn push_body(&mut self, mut nodes: Vec<TagNode>) {
|
||||
pub(crate) fn push_body(&mut self, mut contents: GroupContents) {
|
||||
let item = self.items.last_mut().expect("ListItemLabel");
|
||||
|
||||
// Nested lists are expected to have the following structure:
|
||||
@ -60,40 +64,43 @@ impl ListCtx {
|
||||
// ```
|
||||
//
|
||||
// So move the nested list out of the list item.
|
||||
if let [_, TagNode::Group(TagKind::L(_), _)] = nodes.as_slice() {
|
||||
item.sub_list = nodes.pop();
|
||||
if let [_, TagNode::Group(group)] = contents.nodes.as_slice()
|
||||
&& let TagKind::L(_) = group.tag
|
||||
{
|
||||
item.sub_list = contents.nodes.pop();
|
||||
}
|
||||
|
||||
item.body = Some(nodes);
|
||||
item.body = Some(TagNode::group(Tag::LBody, contents));
|
||||
}
|
||||
|
||||
pub(crate) fn push_bib_entry(&mut self, nodes: Vec<TagNode>) {
|
||||
let nodes = vec![TagNode::group(Tag::BibEntry, nodes)];
|
||||
pub(crate) fn push_bib_entry(&mut self, contents: GroupContents) {
|
||||
let nodes = vec![TagNode::group(Tag::BibEntry, contents)];
|
||||
// Bibliography lists cannot be nested, but may be missing labels.
|
||||
let body = TagNode::virtual_group(Tag::LBody, nodes);
|
||||
if let Some(item) = self.items.last_mut().filter(|item| item.body.is_none()) {
|
||||
item.body = Some(nodes);
|
||||
item.body = Some(body);
|
||||
} else {
|
||||
self.items.push(ListItem {
|
||||
label: Vec::new(),
|
||||
body: Some(nodes),
|
||||
label: TagNode::empty_group(Tag::Lbl),
|
||||
body: Some(body),
|
||||
sub_list: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_list(self, mut nodes: Vec<TagNode>) -> TagNode {
|
||||
pub(crate) fn build_list(self, mut contents: GroupContents) -> TagNode {
|
||||
for item in self.items.into_iter() {
|
||||
nodes.push(TagNode::group(
|
||||
contents.nodes.push(TagNode::virtual_group(
|
||||
Tag::LI,
|
||||
vec![
|
||||
TagNode::group(Tag::Lbl, item.label),
|
||||
TagNode::group(Tag::LBody, item.body.unwrap_or_default()),
|
||||
item.label,
|
||||
item.body.unwrap_or_else(|| TagNode::empty_group(Tag::LBody)),
|
||||
],
|
||||
));
|
||||
if let Some(sub_list) = item.sub_list {
|
||||
nodes.push(sub_list);
|
||||
contents.nodes.push(sub_list);
|
||||
}
|
||||
}
|
||||
TagNode::group(Tag::L(self.numbering), nodes)
|
||||
TagNode::group(Tag::L(self.numbering), contents)
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ use krilla::page::Page;
|
||||
use krilla::surface::Surface;
|
||||
use krilla::tagging::{
|
||||
ArtifactType, BBox, ContentTag, Identifier, ListNumbering, Node, SpanTag, Tag,
|
||||
TagGroup, TagKind, TagTree,
|
||||
TagKind, TagTree,
|
||||
};
|
||||
use typst_library::diag::{SourceResult, bail};
|
||||
use typst_library::foundations::{Content, LinkMarker, Packed};
|
||||
@ -23,7 +23,7 @@ use typst_library::model::{
|
||||
TermsElem,
|
||||
};
|
||||
use typst_library::pdf::{ArtifactElem, ArtifactKind, PdfMarkerTag, PdfMarkerTagKind};
|
||||
use typst_library::text::RawElem;
|
||||
use typst_library::text::{Lang, RawElem};
|
||||
use typst_library::visualize::ImageElem;
|
||||
use typst_syntax::Span;
|
||||
|
||||
@ -220,7 +220,9 @@ fn push_stack(
|
||||
}
|
||||
}
|
||||
|
||||
gc.tags.stack.push(StackEntry { loc, span, kind, nodes: Vec::new() });
|
||||
gc.tags
|
||||
.stack
|
||||
.push(StackEntry { loc, span, lang: None, kind, nodes: Vec::new() });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -324,6 +326,7 @@ pub(crate) fn handle_end(
|
||||
broken_entries.push(StackEntry {
|
||||
loc: entry.loc,
|
||||
span: entry.span,
|
||||
lang: None,
|
||||
kind,
|
||||
nodes: Vec::new(),
|
||||
});
|
||||
@ -341,73 +344,77 @@ pub(crate) fn handle_end(
|
||||
}
|
||||
|
||||
fn pop_stack(gc: &mut GlobalContext, entry: StackEntry) {
|
||||
// Try to propagate the tag language to the parent tag, or the document.
|
||||
// If successfull omit the language attribute on this tag.
|
||||
let lang = entry.lang.and_then(|lang| {
|
||||
let parent_lang = (gc.tags.stack.last_mut())
|
||||
.map(|e| &mut e.lang)
|
||||
.unwrap_or(&mut gc.tags.doc_lang);
|
||||
if parent_lang.is_none_or(|l| l == lang) {
|
||||
*parent_lang = Some(lang);
|
||||
return None;
|
||||
}
|
||||
Some(lang)
|
||||
});
|
||||
|
||||
let contents = GroupContents { span: entry.span, lang, nodes: entry.nodes };
|
||||
let node = match entry.kind {
|
||||
StackEntryKind::Standard(tag) => TagNode::Group(tag, entry.nodes),
|
||||
StackEntryKind::Outline(ctx) => ctx.build_outline(entry.nodes),
|
||||
StackEntryKind::Standard(tag) => TagNode::group(tag, contents),
|
||||
StackEntryKind::Outline(ctx) => ctx.build_outline(contents),
|
||||
StackEntryKind::OutlineEntry(outline_entry) => {
|
||||
let Some((outline_ctx, outline_nodes)) = gc.tags.stack.parent_outline()
|
||||
else {
|
||||
// PDF/UA compliance of the structure hierarchy is checked
|
||||
// elsewhere. While this doesn't make a lot of sense, just
|
||||
// avoid crashing here.
|
||||
gc.tags.push(TagNode::group(Tag::TOCI, entry.nodes));
|
||||
return;
|
||||
};
|
||||
// FIXME(accessibility): disallow usage of `outline.entry` outside of `outline`
|
||||
let (outline_ctx, outline_nodes) = (gc.tags.stack.parent_outline())
|
||||
.expect("outline entries may only exist within an outline");
|
||||
|
||||
outline_ctx.insert(outline_nodes, outline_entry, entry.nodes);
|
||||
outline_ctx.insert(outline_nodes, outline_entry, contents);
|
||||
return;
|
||||
}
|
||||
StackEntryKind::Table(ctx) => ctx.build_table(entry.nodes),
|
||||
StackEntryKind::Table(ctx) => ctx.build_table(contents),
|
||||
StackEntryKind::TableCell(cell) => {
|
||||
let Some(table_ctx) = gc.tags.stack.parent_table() else {
|
||||
// PDF/UA compliance of the structure hierarchy is checked
|
||||
// elsewhere. While this doesn't make a lot of sense, just
|
||||
// avoid crashing here.
|
||||
let tag = Tag::TD.with_location(Some(cell.span().into_raw()));
|
||||
gc.tags.push(TagNode::group(tag, entry.nodes));
|
||||
return;
|
||||
};
|
||||
// FIXME(accessibility): disallow usage of `table.cell` and `grid.cell` outside of table/grid
|
||||
let table_ctx = (gc.tags.stack.parent_table())
|
||||
.expect("table cells may only exist within a table");
|
||||
|
||||
table_ctx.insert(&cell, entry.nodes);
|
||||
table_ctx.insert(&cell, contents);
|
||||
return;
|
||||
}
|
||||
StackEntryKind::List(list) => list.build_list(entry.nodes),
|
||||
StackEntryKind::List(list) => list.build_list(contents),
|
||||
StackEntryKind::ListItemLabel => {
|
||||
let list_ctx = gc.tags.stack.parent_list().expect("parent list");
|
||||
list_ctx.push_label(entry.nodes);
|
||||
list_ctx.push_label(contents);
|
||||
return;
|
||||
}
|
||||
StackEntryKind::ListItemBody => {
|
||||
let list_ctx = gc.tags.stack.parent_list().expect("parent list");
|
||||
list_ctx.push_body(entry.nodes);
|
||||
list_ctx.push_body(contents);
|
||||
return;
|
||||
}
|
||||
StackEntryKind::BibEntry => {
|
||||
let list_ctx = gc.tags.stack.parent_list().expect("parent list");
|
||||
list_ctx.push_bib_entry(entry.nodes);
|
||||
list_ctx.push_bib_entry(contents);
|
||||
return;
|
||||
}
|
||||
StackEntryKind::Figure(ctx) => {
|
||||
let tag = Tag::Figure(ctx.alt).with_bbox(ctx.bbox.get());
|
||||
TagNode::group(tag, entry.nodes)
|
||||
TagNode::group(tag, contents)
|
||||
}
|
||||
StackEntryKind::Formula(ctx) => {
|
||||
let tag = Tag::Formula(ctx.alt).with_bbox(ctx.bbox.get());
|
||||
TagNode::group(tag, entry.nodes)
|
||||
TagNode::group(tag, contents)
|
||||
}
|
||||
StackEntryKind::Link(_, link) => {
|
||||
let alt = link.alt.as_ref().map(EcoString::to_string);
|
||||
let tag = Tag::Link.with_alt_text(alt);
|
||||
let mut node = TagNode::group(tag, entry.nodes);
|
||||
let mut node = TagNode::group(tag, contents);
|
||||
// Wrap link in reference tag, if it's not a url.
|
||||
if let Destination::Position(_) | Destination::Location(_) = link.dest {
|
||||
node = TagNode::group(Tag::Reference, vec![node]);
|
||||
node = TagNode::virtual_group(Tag::Reference, vec![node]);
|
||||
}
|
||||
node
|
||||
}
|
||||
StackEntryKind::FootnoteRef(decl_loc) => {
|
||||
// transparently insert all children.
|
||||
gc.tags.extend(entry.nodes);
|
||||
gc.tags.extend(contents.nodes);
|
||||
|
||||
let ctx = gc.tags.footnotes.entry(decl_loc).or_insert(FootnoteCtx::new());
|
||||
|
||||
@ -421,16 +428,16 @@ fn pop_stack(gc: &mut GlobalContext, entry: StackEntry) {
|
||||
StackEntryKind::FootnoteEntry(footnote_loc) => {
|
||||
// Store footnotes separately so they can be inserted directly after
|
||||
// the footnote reference in the reading order.
|
||||
let tag = TagNode::group(Tag::Note, entry.nodes);
|
||||
let tag = TagNode::group(Tag::Note, contents);
|
||||
let ctx = gc.tags.footnotes.entry(footnote_loc).or_insert(FootnoteCtx::new());
|
||||
ctx.entry = Some(tag);
|
||||
return;
|
||||
}
|
||||
StackEntryKind::Code(desc) => {
|
||||
let code = TagNode::group(Tag::Code, entry.nodes);
|
||||
let code = TagNode::group(Tag::Code, contents);
|
||||
if desc.is_some() {
|
||||
let desc = TagNode::group(Tag::Span.with_alt_text(desc), Vec::new());
|
||||
TagNode::group(Tag::NonStruct, vec![desc, code])
|
||||
let desc = TagNode::empty_group(Tag::Span.with_alt_text(desc));
|
||||
TagNode::virtual_group(Tag::NonStruct, vec![desc, code])
|
||||
} else {
|
||||
code
|
||||
}
|
||||
@ -505,6 +512,8 @@ pub(crate) fn update_bbox(
|
||||
}
|
||||
|
||||
pub(crate) struct Tags {
|
||||
/// The language of the first text item that has been encountered.
|
||||
pub(crate) doc_lang: Option<Lang>,
|
||||
/// The intermediary stack of nested tag groups.
|
||||
pub(crate) stack: TagStack,
|
||||
/// A list of placeholders corresponding to a [`TagNode::Placeholder`].
|
||||
@ -528,6 +537,7 @@ pub(crate) struct Tags {
|
||||
impl Tags {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
doc_lang: None,
|
||||
stack: TagStack::new(),
|
||||
placeholders: Placeholders(Vec::new()),
|
||||
footnotes: HashMap::new(),
|
||||
@ -564,15 +574,37 @@ impl Tags {
|
||||
TagTree::from(children)
|
||||
}
|
||||
|
||||
/// Try to set the language of a parent tag, or the entire document.
|
||||
/// If the language couldn't be set and is different from the existing one,
|
||||
/// this will return `Some`, and the language should be specified on the
|
||||
/// marked content directly.
|
||||
pub(crate) fn try_set_lang(&mut self, lang: Lang) -> Option<Lang> {
|
||||
// Discard languages within artifacts.
|
||||
if self.in_artifact.is_some() {
|
||||
return None;
|
||||
}
|
||||
if self.doc_lang.is_none_or(|l| l == lang) {
|
||||
self.doc_lang = Some(lang);
|
||||
return None;
|
||||
}
|
||||
if let Some(last) = self.stack.last_mut()
|
||||
&& last.lang.is_none_or(|l| l == lang)
|
||||
{
|
||||
last.lang = Some(lang);
|
||||
return None;
|
||||
}
|
||||
Some(lang)
|
||||
}
|
||||
|
||||
/// Resolves [`Placeholder`] nodes.
|
||||
fn resolve_node(&mut self, node: TagNode) -> Node {
|
||||
match node {
|
||||
TagNode::Group(tag, nodes) => {
|
||||
TagNode::Group(TagGroup { tag, nodes }) => {
|
||||
let children = nodes
|
||||
.into_iter()
|
||||
.map(|node| self.resolve_node(node))
|
||||
.collect::<Vec<_>>();
|
||||
Node::Group(TagGroup::with_children(tag, children))
|
||||
Node::Group(krilla::tagging::TagGroup::with_children(tag, children))
|
||||
}
|
||||
TagNode::Leaf(identifier) => Node::Leaf(identifier),
|
||||
TagNode::Placeholder(placeholder) => self.placeholders.take(placeholder),
|
||||
@ -762,6 +794,7 @@ pub(crate) struct LinkId(u32);
|
||||
pub(crate) struct StackEntry {
|
||||
pub(crate) loc: Location,
|
||||
pub(crate) span: Span,
|
||||
pub(crate) lang: Option<Lang>,
|
||||
pub(crate) kind: StackEntryKind,
|
||||
pub(crate) nodes: Vec<TagNode>,
|
||||
}
|
||||
@ -995,7 +1028,7 @@ impl BBoxCtx {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum TagNode {
|
||||
Group(TagKind, Vec<TagNode>),
|
||||
Group(TagGroup),
|
||||
Leaf(Identifier),
|
||||
/// Allows inserting a placeholder into the tag tree.
|
||||
/// Currently used for [`krilla::page::Page::add_tagged_annotation`].
|
||||
@ -1004,9 +1037,38 @@ pub(crate) enum TagNode {
|
||||
}
|
||||
|
||||
impl TagNode {
|
||||
pub fn group(tag: impl Into<TagKind>, children: Vec<TagNode>) -> Self {
|
||||
TagNode::Group(tag.into(), children)
|
||||
pub fn group(tag: impl Into<TagKind>, contents: GroupContents) -> Self {
|
||||
let lang = contents.lang.map(|l| l.as_str().to_string());
|
||||
let tag = tag
|
||||
.into()
|
||||
.with_lang(lang)
|
||||
.with_location(Some(contents.span.into_raw()));
|
||||
TagNode::Group(TagGroup { tag, nodes: contents.nodes })
|
||||
}
|
||||
|
||||
/// A tag group not directly related to a typst element, generated to
|
||||
/// accomodate the tag structure.
|
||||
pub fn virtual_group(tag: impl Into<TagKind>, nodes: Vec<TagNode>) -> Self {
|
||||
let tag = tag.into();
|
||||
TagNode::Group(TagGroup { tag, nodes })
|
||||
}
|
||||
|
||||
pub fn empty_group(tag: impl Into<TagKind>) -> Self {
|
||||
Self::virtual_group(tag, Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct TagGroup {
|
||||
tag: TagKind,
|
||||
nodes: Vec<TagNode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct GroupContents {
|
||||
span: Span,
|
||||
lang: Option<Lang>,
|
||||
nodes: Vec<TagNode>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
|
@ -2,7 +2,7 @@ use krilla::tagging::Tag;
|
||||
use typst_library::foundations::Packed;
|
||||
use typst_library::model::OutlineEntry;
|
||||
|
||||
use crate::tags::TagNode;
|
||||
use crate::tags::{GroupContents, TagNode};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct OutlineCtx {
|
||||
@ -18,7 +18,7 @@ impl OutlineCtx {
|
||||
&mut self,
|
||||
outline_nodes: &mut Vec<TagNode>,
|
||||
entry: Packed<OutlineEntry>,
|
||||
nodes: Vec<TagNode>,
|
||||
contents: GroupContents,
|
||||
) {
|
||||
let expected_len = entry.level.get() - 1;
|
||||
if self.stack.len() < expected_len {
|
||||
@ -29,7 +29,7 @@ impl OutlineCtx {
|
||||
}
|
||||
}
|
||||
|
||||
let section_entry = TagNode::group(Tag::TOCI, nodes);
|
||||
let section_entry = TagNode::group(Tag::TOCI, contents);
|
||||
self.push(outline_nodes, section_entry);
|
||||
}
|
||||
|
||||
@ -45,11 +45,11 @@ impl OutlineCtx {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_outline(mut self, mut outline_nodes: Vec<TagNode>) -> TagNode {
|
||||
pub(crate) fn build_outline(mut self, mut contents: GroupContents) -> TagNode {
|
||||
while !self.stack.is_empty() {
|
||||
self.finish_section(&mut outline_nodes);
|
||||
self.finish_section(&mut contents.nodes);
|
||||
}
|
||||
TagNode::group(Tag::TOC, outline_nodes)
|
||||
TagNode::group(Tag::TOC, contents)
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,6 +68,6 @@ impl OutlineSection {
|
||||
}
|
||||
|
||||
fn into_tag(self) -> TagNode {
|
||||
TagNode::group(Tag::TOC, self.entries)
|
||||
TagNode::virtual_group(Tag::TOC, self.entries)
|
||||
}
|
||||
}
|
||||
|
@ -7,10 +7,9 @@ use smallvec::SmallVec;
|
||||
use typst_library::foundations::{Packed, Smart};
|
||||
use typst_library::model::TableCell;
|
||||
use typst_library::pdf::{TableCellKind, TableHeaderScope};
|
||||
use typst_syntax::Span;
|
||||
|
||||
use crate::tags::util::PropertyValCopied;
|
||||
use crate::tags::{BBoxCtx, TableId, TagNode};
|
||||
use crate::tags::{BBoxCtx, GroupContents, TableId, TagNode};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct TableCtx {
|
||||
@ -64,7 +63,7 @@ impl TableCtx {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn insert(&mut self, cell: &Packed<TableCell>, nodes: Vec<TagNode>) {
|
||||
pub(crate) fn insert(&mut self, cell: &Packed<TableCell>, contents: GroupContents) {
|
||||
let x = cell.x.val().unwrap_or_else(|| unreachable!());
|
||||
let y = cell.y.val().unwrap_or_else(|| unreachable!());
|
||||
let rowspan = cell.rowspan.val();
|
||||
@ -98,16 +97,15 @@ impl TableCtx {
|
||||
colspan: colspan.try_into().unwrap_or(NonZeroU32::MAX),
|
||||
kind,
|
||||
headers: SmallVec::new(),
|
||||
nodes,
|
||||
span: cell.span(),
|
||||
contents,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn build_table(mut self, mut nodes: Vec<TagNode>) -> TagNode {
|
||||
pub(crate) fn build_table(mut self, mut contents: GroupContents) -> TagNode {
|
||||
// Table layouting ensures that there are no overlapping cells, and that
|
||||
// any gaps left by the user are filled with empty cells.
|
||||
if self.rows.is_empty() {
|
||||
return TagNode::group(Tag::Table.with_summary(self.summary), nodes);
|
||||
return TagNode::group(Tag::Table.with_summary(self.summary), contents);
|
||||
}
|
||||
let height = self.rows.len();
|
||||
let width = self.rows[0].len();
|
||||
@ -174,7 +172,7 @@ impl TableCtx {
|
||||
let cell = cell.into_cell()?;
|
||||
let rowspan = (cell.rowspan.get() != 1).then_some(cell.rowspan);
|
||||
let colspan = (cell.colspan.get() != 1).then_some(cell.colspan);
|
||||
let tag = match cell.unwrap_kind() {
|
||||
let tag: TagKind = match cell.unwrap_kind() {
|
||||
TableCellKind::Header(_, scope) => {
|
||||
let id = table_cell_id(self.id, cell.x, cell.y);
|
||||
let scope = table_header_scope(scope);
|
||||
@ -183,25 +181,23 @@ impl TableCtx {
|
||||
.with_headers(Some(cell.headers))
|
||||
.with_row_span(rowspan)
|
||||
.with_col_span(colspan)
|
||||
.with_location(Some(cell.span.into_raw()))
|
||||
.into()
|
||||
}
|
||||
TableCellKind::Footer | TableCellKind::Data => Tag::TD
|
||||
.with_headers(Some(cell.headers))
|
||||
.with_row_span(rowspan)
|
||||
.with_col_span(colspan)
|
||||
.with_location(Some(cell.span.into_raw()))
|
||||
.into(),
|
||||
};
|
||||
Some(TagNode::Group(tag, cell.nodes))
|
||||
Some(TagNode::group(tag, cell.contents))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let row = TagNode::group(Tag::TR, row_nodes);
|
||||
let row = TagNode::virtual_group(Tag::TR, row_nodes);
|
||||
|
||||
// Push the `TR` tags directly.
|
||||
if !gen_row_groups {
|
||||
nodes.push(row);
|
||||
contents.nodes.push(row);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -212,7 +208,8 @@ impl TableCtx {
|
||||
TableCellKind::Footer => Tag::TFoot.into(),
|
||||
TableCellKind::Data => Tag::TBody.into(),
|
||||
};
|
||||
nodes.push(TagNode::Group(tag, std::mem::take(&mut row_chunk)));
|
||||
let chunk_nodes = std::mem::take(&mut row_chunk);
|
||||
contents.nodes.push(TagNode::virtual_group(tag, chunk_nodes));
|
||||
|
||||
chunk_kind = row_kind;
|
||||
}
|
||||
@ -225,14 +222,11 @@ impl TableCtx {
|
||||
TableCellKind::Footer => Tag::TFoot.into(),
|
||||
TableCellKind::Data => Tag::TBody.into(),
|
||||
};
|
||||
nodes.push(TagNode::Group(tag, row_chunk));
|
||||
contents.nodes.push(TagNode::virtual_group(tag, row_chunk));
|
||||
}
|
||||
|
||||
let tag = Tag::Table
|
||||
.with_summary(self.summary)
|
||||
.with_bbox(self.bbox.get())
|
||||
.into();
|
||||
TagNode::Group(tag, nodes)
|
||||
let tag = Tag::Table.with_summary(self.summary).with_bbox(self.bbox.get());
|
||||
TagNode::group(tag, contents)
|
||||
}
|
||||
|
||||
fn resolve_cell_headers<F>(
|
||||
@ -297,8 +291,7 @@ struct TableCtxCell {
|
||||
colspan: NonZeroU32,
|
||||
kind: Smart<TableCellKind>,
|
||||
headers: SmallVec<[TagId; 1]>,
|
||||
nodes: Vec<TagNode>,
|
||||
span: Span,
|
||||
contents: GroupContents,
|
||||
}
|
||||
|
||||
impl TableCtxCell {
|
||||
|
@ -7,7 +7,7 @@ use krilla::tagging::SpanTag;
|
||||
use krilla::text::GlyphId;
|
||||
use typst_library::diag::{SourceResult, bail};
|
||||
use typst_library::layout::Size;
|
||||
use typst_library::text::{Font, Glyph, TextItem};
|
||||
use typst_library::text::{Font, Glyph, Lang, TextItem};
|
||||
use typst_library::visualize::FillRule;
|
||||
use typst_syntax::Span;
|
||||
|
||||
@ -22,11 +22,11 @@ pub(crate) fn handle_text(
|
||||
surface: &mut Surface,
|
||||
gc: &mut GlobalContext,
|
||||
) -> SourceResult<()> {
|
||||
*gc.languages.entry(t.lang).or_insert(0) += t.glyphs.len();
|
||||
|
||||
let lang = gc.tags.try_set_lang(t.lang);
|
||||
let lang = lang.as_ref().map(Lang::as_str);
|
||||
tags::update_bbox(gc, fc, || t.bbox());
|
||||
|
||||
let mut handle = tags::start_span(gc, surface, SpanTag::empty());
|
||||
let mut handle = tags::start_span(gc, surface, SpanTag::empty().with_lang(lang));
|
||||
let surface = handle.surface();
|
||||
|
||||
let font = convert_font(gc, t.font.clone())?;
|
||||
|
@ -3,6 +3,7 @@
|
||||
- Content: page=0 mcid=0
|
||||
- Content: page=0 mcid=1
|
||||
- Tag: BlockQuote
|
||||
/Lang: "ar"
|
||||
/K:
|
||||
- Content: page=0 mcid=2
|
||||
- Content: page=0 mcid=3
|
||||
|
@ -7,6 +7,7 @@
|
||||
- Content: page=0 mcid=2
|
||||
- Content: page=0 mcid=3
|
||||
- Tag: InlineQuote
|
||||
/Lang: "ar"
|
||||
/K:
|
||||
- Tag: P
|
||||
/K:
|
||||
|
Loading…
x
Reference in New Issue
Block a user