summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-06-08 19:31:07 +0200
committerGitHub <noreply@github.com>2022-06-08 19:31:07 +0200
commitcd5a14bc240b21f8f538ff7fc1d116d23611d6c5 (patch)
treef4b8a7e8a941f8653d5061d2f7acc0bfd8d92b13
parentb905048d4bb497252028df6d21b525fefb6b64c3 (diff)
parent72d3f3fffabe6872eb7839585bea925b89aac6a4 (diff)
Merge pull request #75 from typst/semantics
Frame Role and PDF outline
-rw-r--r--src/export/pdf.rs345
-rw-r--r--src/frame.rs100
-rw-r--r--src/library/graphics/hide.rs6
-rw-r--r--src/library/graphics/shape.rs4
-rw-r--r--src/library/layout/flow.rs7
-rw-r--r--src/library/layout/grid.rs16
-rw-r--r--src/library/layout/page.rs26
-rw-r--r--src/library/layout/stack.rs7
-rw-r--r--src/library/math/rex.rs1
-rw-r--r--src/library/structure/heading.rs5
-rw-r--r--src/library/structure/list.rs16
-rw-r--r--src/library/structure/table.rs9
-rw-r--r--src/library/text/par.rs9
-rw-r--r--src/library/text/raw.rs2
-rw-r--r--src/model/content.rs5
-rw-r--r--src/model/layout.rs11
-rw-r--r--src/model/styles.rs21
17 files changed, 460 insertions, 130 deletions
diff --git a/src/export/pdf.rs b/src/export/pdf.rs
index aa7acd41..b8fc1e39 100644
--- a/src/export/pdf.rs
+++ b/src/export/pdf.rs
@@ -16,12 +16,13 @@ use ttf_parser::{name_id, GlyphId, Tag};
use super::subset::subset;
use crate::font::{find_name, FaceId, FontStore};
-use crate::frame::{Destination, Element, Frame, Group, Text};
+use crate::frame::{Destination, Element, Frame, Group, Role, Text};
use crate::geom::{
self, Color, Dir, Em, Geometry, Length, Numeric, Paint, Point, Ratio, Shape, Size,
Stroke, Transform,
};
use crate::image::{Image, ImageId, ImageStore, RasterImage};
+use crate::library::prelude::EcoString;
use crate::library::text::Lang;
use crate::Context;
@@ -42,31 +43,43 @@ const SRGB_GRAY: Name<'static> = Name(b"srgbgray");
/// An exporter for a whole PDF document.
struct PdfExporter<'a> {
+ writer: PdfWriter,
fonts: &'a FontStore,
images: &'a ImageStore,
- writer: PdfWriter,
- alloc: Ref,
pages: Vec<Page>,
- face_map: Remapper<FaceId>,
+ page_heights: Vec<f32>,
+ alloc: Ref,
+ page_tree_ref: Ref,
face_refs: Vec<Ref>,
- glyph_sets: HashMap<FaceId, HashSet<u16>>,
- image_map: Remapper<ImageId>,
image_refs: Vec<Ref>,
+ page_refs: Vec<Ref>,
+ face_map: Remapper<FaceId>,
+ image_map: Remapper<ImageId>,
+ glyph_sets: HashMap<FaceId, HashSet<u16>>,
+ languages: HashMap<Lang, usize>,
+ heading_tree: Vec<HeadingNode>,
}
impl<'a> PdfExporter<'a> {
fn new(ctx: &'a Context) -> Self {
+ let mut alloc = Ref::new(1);
+ let page_tree_ref = alloc.bump();
Self {
+ writer: PdfWriter::new(),
fonts: &ctx.fonts,
images: &ctx.images,
- writer: PdfWriter::new(),
- alloc: Ref::new(1),
pages: vec![],
- face_map: Remapper::new(),
+ page_heights: vec![],
+ alloc,
+ page_tree_ref,
+ page_refs: vec![],
face_refs: vec![],
- glyph_sets: HashMap::new(),
- image_map: Remapper::new(),
image_refs: vec![],
+ face_map: Remapper::new(),
+ image_map: Remapper::new(),
+ glyph_sets: HashMap::new(),
+ languages: HashMap::new(),
+ heading_tree: vec![],
}
}
@@ -74,12 +87,24 @@ impl<'a> PdfExporter<'a> {
self.build_pages(frames);
self.write_fonts();
self.write_images();
- self.write_structure()
+
+ // The root page tree.
+ for page in std::mem::take(&mut self.pages).into_iter() {
+ self.write_page(page);
+ }
+
+ self.write_page_tree();
+ self.write_catalog();
+
+ self.writer.finish()
}
fn build_pages(&mut self, frames: &[Arc<Frame>]) {
for frame in frames {
- let page = PageExporter::new(self).export(frame);
+ let page_id = self.alloc.bump();
+ self.page_refs.push(page_id);
+ let page = PageExporter::new(self, page_id).export(frame);
+ self.page_heights.push(page.size.y.to_f32());
self.pages.push(page);
}
}
@@ -299,70 +324,52 @@ impl<'a> PdfExporter<'a> {
}
}
- fn write_structure(mut self) -> Vec<u8> {
- // The root page tree.
- let page_tree_ref = self.alloc.bump();
-
- // The page objects (non-root nodes in the page tree).
- let mut page_refs = vec![];
- let mut page_heights = vec![];
- for page in &self.pages {
- let page_id = self.alloc.bump();
- page_refs.push(page_id);
- page_heights.push(page.size.y.to_f32());
- }
-
- let mut languages = HashMap::new();
- for (page, page_id) in self.pages.into_iter().zip(page_refs.iter()) {
- let content_id = self.alloc.bump();
-
- let mut page_writer = self.writer.page(*page_id);
- page_writer.parent(page_tree_ref);
-
- let w = page.size.x.to_f32();
- let h = page.size.y.to_f32();
- page_writer.media_box(Rect::new(0.0, 0.0, w, h));
- page_writer.contents(content_id);
-
- let mut annotations = page_writer.annotations();
- for (dest, rect) in page.links {
- let mut link = annotations.push();
- link.subtype(AnnotationType::Link).rect(rect);
- match dest {
- Destination::Url(uri) => {
- link.action()
- .action_type(ActionType::Uri)
- .uri(Str(uri.as_str().as_bytes()));
- }
- Destination::Internal(loc) => {
- let index = loc.page - 1;
- let height = page_heights[index];
- link.action()
- .action_type(ActionType::GoTo)
- .destination_direct()
- .page(page_refs[index])
- .xyz(loc.pos.x.to_f32(), height - loc.pos.y.to_f32(), None);
- }
+ fn write_page(&mut self, page: Page) {
+ let content_id = self.alloc.bump();
+
+ let mut page_writer = self.writer.page(page.id);
+ page_writer.parent(self.page_tree_ref);
+
+ let w = page.size.x.to_f32();
+ let h = page.size.y.to_f32();
+ page_writer.media_box(Rect::new(0.0, 0.0, w, h));
+ page_writer.contents(content_id);
+
+ let mut annotations = page_writer.annotations();
+ for (dest, rect) in page.links {
+ let mut link = annotations.push();
+ link.subtype(AnnotationType::Link).rect(rect);
+ match dest {
+ Destination::Url(uri) => {
+ link.action()
+ .action_type(ActionType::Uri)
+ .uri(Str(uri.as_str().as_bytes()));
+ }
+ Destination::Internal(loc) => {
+ let index = loc.page - 1;
+ let height = self.page_heights[index];
+ link.action()
+ .action_type(ActionType::GoTo)
+ .destination_direct()
+ .page(self.page_refs[index])
+ .xyz(loc.pos.x.to_f32(), height - loc.pos.y.to_f32(), None);
}
}
+ }
- annotations.finish();
- page_writer.finish();
+ annotations.finish();
+ page_writer.finish();
- for (lang, count) in page.languages {
- languages
- .entry(lang)
- .and_modify(|x| *x += count)
- .or_insert_with(|| count);
- }
-
- self.writer
- .stream(content_id, &deflate(&page.content.finish()))
- .filter(Filter::FlateDecode);
- }
+ self.writer
+ .stream(content_id, &deflate(&page.content.finish()))
+ .filter(Filter::FlateDecode);
+ }
- let mut pages = self.writer.pages(page_tree_ref);
- pages.count(page_refs.len() as i32).kids(page_refs);
+ fn write_page_tree(&mut self) {
+ let mut pages = self.writer.pages(self.page_tree_ref);
+ pages
+ .count(self.page_refs.len() as i32)
+ .kids(self.page_refs.iter().copied());
let mut resources = pages.resources();
let mut spaces = resources.color_spaces();
@@ -387,11 +394,36 @@ impl<'a> PdfExporter<'a> {
images.finish();
resources.finish();
pages.finish();
+ }
- let lang = languages
- .into_iter()
- .max_by(|(_, v1), (_, v2)| v1.cmp(v2))
- .map(|(k, _)| k);
+ fn write_catalog(&mut self) {
+ // Build the outline tree.
+ let outline_root_id = (!self.heading_tree.is_empty()).then(|| self.alloc.bump());
+ let outline_start_ref = self.alloc;
+ let len = self.heading_tree.len();
+ let mut prev_ref = None;
+
+ for (i, node) in std::mem::take(&mut self.heading_tree).iter().enumerate() {
+ prev_ref = Some(self.write_outline_item(
+ node,
+ outline_root_id.unwrap(),
+ prev_ref,
+ i + 1 == len,
+ ));
+ }
+
+ if let Some(outline_root_id) = outline_root_id {
+ let mut outline_root = self.writer.outline(outline_root_id);
+ outline_root.first(outline_start_ref);
+ outline_root.last(Ref::new(self.alloc.get() - 1));
+ outline_root.count(self.heading_tree.len() as i32);
+ }
+
+ let lang = self
+ .languages
+ .iter()
+ .max_by_key(|(&lang, &count)| (count, lang))
+ .map(|(&k, _)| k);
let dir = if lang.map(Lang::dir) == Some(Dir::RTL) {
Direction::R2L
@@ -402,38 +434,88 @@ impl<'a> PdfExporter<'a> {
// Write the document information, catalog and wrap it up!
self.writer.document_info(self.alloc.bump()).creator(TextStr("Typst"));
let mut catalog = self.writer.catalog(self.alloc.bump());
- catalog.pages(page_tree_ref);
+ catalog.pages(self.page_tree_ref);
catalog.viewer_preferences().direction(dir);
+ if let Some(outline_root_id) = outline_root_id {
+ catalog.outlines(outline_root_id);
+ }
+
if let Some(lang) = lang {
catalog.lang(TextStr(lang.as_str()));
}
catalog.finish();
- self.writer.finish()
+ }
+
+ fn write_outline_item(
+ &mut self,
+ node: &HeadingNode,
+ parent_ref: Ref,
+ prev_ref: Option<Ref>,
+ is_last: bool,
+ ) -> Ref {
+ let id = self.alloc.bump();
+ let next_ref = Ref::new(id.get() + node.len() as i32);
+
+ let mut outline = self.writer.outline_item(id);
+ outline.parent(parent_ref);
+
+ if !is_last {
+ outline.next(next_ref);
+ }
+
+ if let Some(prev_rev) = prev_ref {
+ outline.prev(prev_rev);
+ }
+
+ if !node.children.is_empty() {
+ let current_child = Ref::new(id.get() + 1);
+ outline.first(current_child);
+ outline.last(Ref::new(next_ref.get() - 1));
+ outline.count(-1 * node.children.len() as i32);
+ }
+
+ outline.title(TextStr(&node.heading.content));
+ outline.dest_direct().page(node.heading.page).xyz(
+ node.heading.position.x.to_f32(),
+ (node.heading.position.y + Length::pt(3.0)).to_f32(),
+ None,
+ );
+
+ outline.finish();
+
+ let mut prev_ref = None;
+ for (i, child) in node.children.iter().enumerate() {
+ prev_ref = Some(self.write_outline_item(
+ child,
+ id,
+ prev_ref,
+ i + 1 == node.children.len(),
+ ));
+ }
+
+ id
}
}
/// An exporter for the contents of a single PDF page.
-struct PageExporter<'a> {
- fonts: &'a FontStore,
- font_map: &'a mut Remapper<FaceId>,
- image_map: &'a mut Remapper<ImageId>,
- glyphs: &'a mut HashMap<FaceId, HashSet<u16>>,
- languages: HashMap<Lang, usize>,
- bottom: f32,
+struct PageExporter<'a, 'b> {
+ exporter: &'a mut PdfExporter<'b>,
+ page_ref: Ref,
content: Content,
- links: Vec<(Destination, Rect)>,
state: State,
saves: Vec<State>,
+ bottom: f32,
+ links: Vec<(Destination, Rect)>,
}
/// Data for an exported page.
struct Page {
+ id: Ref,
size: Size,
content: Content,
links: Vec<(Destination, Rect)>,
- languages: HashMap<Lang, usize>,
}
/// A simulated graphics state used to deduplicate graphics state changes and
@@ -448,19 +530,56 @@ struct State {
stroke_space: Option<Name<'static>>,
}
-impl<'a> PageExporter<'a> {
- fn new(exporter: &'a mut PdfExporter) -> Self {
+/// A heading that can later be linked in the outline panel.
+#[derive(Debug, Clone)]
+struct Heading {
+ content: EcoString,
+ level: usize,
+ position: Point,
+ page: Ref,
+}
+
+#[derive(Debug, Clone)]
+struct HeadingNode {
+ heading: Heading,
+ children: Vec<HeadingNode>,
+}
+
+impl HeadingNode {
+ fn leaf(heading: Heading) -> Self {
+ HeadingNode { heading, children: Vec::new() }
+ }
+
+ fn len(&self) -> usize {
+ 1 + self.children.iter().map(Self::len).sum::<usize>()
+ }
+
+ fn insert(&mut self, other: Heading, level: usize) -> bool {
+ if level >= other.level {
+ return false;
+ }
+
+ if let Some(child) = self.children.last_mut() {
+ if child.insert(other.clone(), level + 1) {
+ return true;
+ }
+ }
+
+ self.children.push(Self::leaf(other));
+ true
+ }
+}
+
+impl<'a, 'b> PageExporter<'a, 'b> {
+ fn new(exporter: &'a mut PdfExporter<'b>, page_ref: Ref) -> Self {
Self {
- fonts: exporter.fonts,
- font_map: &mut exporter.face_map,
- image_map: &mut exporter.image_map,
- glyphs: &mut exporter.glyph_sets,
- languages: HashMap::new(),
- bottom: 0.0,
+ exporter,
+ page_ref,
content: Content::new(),
- links: vec![],
state: State::default(),
saves: vec![],
+ bottom: 0.0,
+ links: vec![],
}
}
@@ -479,12 +598,29 @@ impl<'a> PageExporter<'a> {
Page {
size: frame.size,
content: self.content,
+ id: self.page_ref,
links: self.links,
- languages: self.languages,
}
}
fn write_frame(&mut self, frame: &Frame) {
+ if let Some(Role::Heading(level)) = frame.role() {
+ let heading = Heading {
+ position: Point::new(self.state.transform.tx, self.state.transform.ty),
+ content: frame.text(),
+ page: self.page_ref,
+ level,
+ };
+
+ if let Some(last) = self.exporter.heading_tree.last_mut() {
+ if !last.insert(heading.clone(), 1) {
+ self.exporter.heading_tree.push(HeadingNode::leaf(heading))
+ }
+ } else {
+ self.exporter.heading_tree.push(HeadingNode::leaf(heading))
+ }
+ }
+
for &(pos, ref element) in &frame.elements {
let x = pos.x.to_f32();
let y = pos.y.to_f32();
@@ -521,13 +657,14 @@ impl<'a> PageExporter<'a> {
}
fn write_text(&mut self, x: f32, y: f32, text: &Text) {
- *self.languages.entry(text.lang).or_insert(0) += text.glyphs.len();
- self.glyphs
+ *self.exporter.languages.entry(text.lang).or_insert(0) += text.glyphs.len();
+ self.exporter
+ .glyph_sets
.entry(text.face_id)
.or_default()
.extend(text.glyphs.iter().map(|g| g.id));
- let face = self.fonts.get(text.face_id);
+ let face = self.exporter.fonts.get(text.face_id);
self.set_fill(text.fill);
self.set_font(text.face_id, text.size);
@@ -641,8 +778,8 @@ impl<'a> PageExporter<'a> {
}
fn write_image(&mut self, x: f32, y: f32, id: ImageId, size: Size) {
- self.image_map.insert(id);
- let name = format_eco!("Im{}", self.image_map.map(id));
+ self.exporter.image_map.insert(id);
+ let name = format_eco!("Im{}", self.exporter.image_map.map(id));
let w = size.x.to_f32();
let h = size.y.to_f32();
self.content.save_state();
@@ -705,8 +842,8 @@ impl<'a> PageExporter<'a> {
fn set_font(&mut self, face_id: FaceId, size: Length) {
if self.state.font != Some((face_id, size)) {
- self.font_map.insert(face_id);
- let name = format_eco!("F{}", self.font_map.map(face_id));
+ self.exporter.face_map.insert(face_id);
+ let name = format_eco!("F{}", self.exporter.face_map.map(face_id));
self.content.set_font(Name(name.as_bytes()), size.to_f32());
self.state.font = Some((face_id, size));
}
diff --git a/src/frame.rs b/src/frame.rs
index 1bd1f454..7972b069 100644
--- a/src/frame.rs
+++ b/src/frame.rs
@@ -22,6 +22,8 @@ pub struct Frame {
pub baseline: Option<Length>,
/// The elements composing this layout.
pub elements: Vec<(Point, Element)>,
+ /// The semantic role of the frame.
+ role: Option<Role>,
}
impl Frame {
@@ -29,7 +31,12 @@ impl Frame {
#[track_caller]
pub fn new(size: Size) -> Self {
assert!(size.is_finite());
- Self { size, baseline: None, elements: vec![] }
+ Self {
+ size,
+ baseline: None,
+ elements: vec![],
+ role: None,
+ }
}
/// The baseline of the frame.
@@ -43,6 +50,11 @@ impl Frame {
self.elements.len()
}
+ /// The role of the frame.
+ pub fn role(&self) -> Option<Role> {
+ self.role
+ }
+
/// Whether the frame has comparatively few elements.
pub fn is_light(&self) -> bool {
self.elements.len() <= 5
@@ -58,7 +70,9 @@ impl Frame {
/// Automatically decides whether to inline the frame or to include it as a
/// group based on the number of elements in the frame.
pub fn push_frame(&mut self, pos: Point, frame: impl FrameRepr) {
- if self.elements.is_empty() || frame.as_ref().is_light() {
+ if (self.elements.is_empty() || frame.as_ref().is_light())
+ && frame.as_ref().role().is_none()
+ {
frame.inline(self, self.layer(), pos);
} else {
self.elements.push((pos, Element::Group(Group::new(frame.share()))));
@@ -80,7 +94,9 @@ impl Frame {
/// Add a frame at a position in the background.
pub fn prepend_frame(&mut self, pos: Point, frame: impl FrameRepr) {
- if self.elements.is_empty() || frame.as_ref().is_light() {
+ if (self.elements.is_empty() || frame.as_ref().is_light())
+ && frame.as_ref().role().is_none()
+ {
frame.inline(self, 0, pos);
} else {
self.elements
@@ -125,6 +141,13 @@ impl Frame {
self.group(|g| g.transform = transform);
}
+ /// Apply the given role to the frame if it doesn't already have one.
+ pub fn apply_role(&mut self, role: Role) {
+ if self.role.map_or(true, Role::is_weak) {
+ self.role = Some(role);
+ }
+ }
+
/// Clip the contents of a frame to its size.
pub fn clip(&mut self) {
self.group(|g| g.clips = true);
@@ -146,10 +169,31 @@ impl Frame {
pub fn link(&mut self, dest: Destination) {
self.push(Point::zero(), Element::Link(dest, self.size));
}
+
+ /// Recover the text inside of the frame and its children.
+ pub fn text(&self) -> EcoString {
+ let mut text = EcoString::new();
+ for (_, element) in &self.elements {
+ match element {
+ Element::Text(content) => {
+ for glyph in &content.glyphs {
+ text.push(glyph.c);
+ }
+ }
+ Element::Group(group) => text.push_str(&group.frame.text()),
+ _ => {}
+ }
+ }
+ text
+ }
}
impl Debug for Frame {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ if let Some(role) = self.role {
+ write!(f, "{role:?} ")?;
+ }
+
f.debug_list()
.entries(self.elements.iter().map(|(_, element)| element))
.finish()
@@ -362,3 +406,53 @@ impl Location {
}
}
}
+
+/// A semantic role of a frame.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum Role {
+ /// A paragraph.
+ Paragraph,
+ /// A heading with some level.
+ Heading(usize),
+ /// A generic block-level subdivision.
+ GenericBlock,
+ /// A generic inline subdivision.
+ GenericInline,
+ /// A list. The boolean indicates whether it is ordered.
+ List { ordered: bool },
+ /// A list item. Must have a list parent.
+ ListItem,
+ /// The label of a list item.
+ ListLabel,
+ /// The body of a list item.
+ ListItemBody,
+ /// A mathematical formula.
+ Formula,
+ /// A table.
+ Table,
+ /// A table row.
+ TableRow,
+ /// A table cell.
+ TableCell,
+ /// A code fragment.
+ Code,
+ /// A page header.
+ Header,
+ /// A page footer.
+ Footer,
+ /// A page background.
+ Background,
+ /// A page foreground.
+ Foreground,
+}
+
+impl Role {
+ /// Whether the role describes a generic element and is not very
+ /// descriptive.
+ pub fn is_weak(self) -> bool {
+ match self {
+ Self::Paragraph | Self::GenericBlock | Self::GenericInline => true,
+ _ => false,
+ }
+ }
+}
diff --git a/src/library/graphics/hide.rs b/src/library/graphics/hide.rs
index 28afe320..4ba5e023 100644
--- a/src/library/graphics/hide.rs
+++ b/src/library/graphics/hide.rs
@@ -22,7 +22,11 @@ impl Layout for HideNode {
// Clear the frames.
for frame in &mut frames {
- *frame = Arc::new(Frame { elements: vec![], ..**frame });
+ *frame = Arc::new({
+ let mut empty = Frame::new(frame.size);
+ empty.baseline = frame.baseline;
+ empty
+ });
}
Ok(frames)
diff --git a/src/library/graphics/shape.rs b/src/library/graphics/shape.rs
index 9da8d8df..82eb2d9d 100644
--- a/src/library/graphics/shape.rs
+++ b/src/library/graphics/shape.rs
@@ -93,6 +93,10 @@ impl<const S: ShapeKind> Layout for ShapeNode<S> {
let mut pod = Regions::one(regions.first, regions.base, regions.expand);
frames = child.layout(ctx, &pod, styles)?;
+ for frame in frames.iter_mut() {
+ Arc::make_mut(frame).apply_role(Role::GenericBlock);
+ }
+
// Relayout with full expansion into square region to make sure
// the result is really a square or circle.
if is_quadratic(S) {
diff --git a/src/library/layout/flow.rs b/src/library/layout/flow.rs
index 6193a68f..f779c8b1 100644
--- a/src/library/layout/flow.rs
+++ b/src/library/layout/flow.rs
@@ -182,7 +182,12 @@ impl FlowLayouter {
let frames = node.layout(ctx, &self.regions, styles)?;
let len = frames.len();
- for (i, frame) in frames.into_iter().enumerate() {
+ for (i, mut frame) in frames.into_iter().enumerate() {
+ // Set the generic block role.
+ if frame.role().map_or(true, Role::is_weak) {
+ Arc::make_mut(&mut frame).apply_role(Role::GenericBlock);
+ }
+
// Grow our size, shrink the region and save the frame for later.
let size = frame.size;
self.used.y += size.y;
diff --git a/src/library/layout/grid.rs b/src/library/layout/grid.rs
index 4cad9de6..2d6eb259 100644
--- a/src/library/layout/grid.rs
+++ b/src/library/layout/grid.rs
@@ -450,6 +450,7 @@ impl<'a> GridLayouter<'a> {
/// Layout a row with fixed height and return its frame.
fn layout_single_row(&mut self, height: Length, y: usize) -> TypResult<Frame> {
let mut output = Frame::new(Size::new(self.used.x, height));
+
let mut pos = Point::zero();
for (x, &rcol) in self.rcols.iter().enumerate() {
@@ -464,6 +465,14 @@ impl<'a> GridLayouter<'a> {
let pod = Regions::one(size, base, Spec::splat(true));
let frame = node.layout(self.ctx, &pod, self.styles)?.remove(0);
+ match frame.role() {
+ Some(Role::ListLabel | Role::ListItemBody) => {
+ output.apply_role(Role::ListItem)
+ }
+ Some(Role::TableCell) => output.apply_role(Role::TableRow),
+ _ => {}
+ }
+
output.push_frame(pos, frame);
}
@@ -505,6 +514,13 @@ impl<'a> GridLayouter<'a> {
// Push the layouted frames into the individual output frames.
let frames = node.layout(self.ctx, &pod, self.styles)?;
for (output, frame) in outputs.iter_mut().zip(frames) {
+ match frame.role() {
+ Some(Role::ListLabel | Role::ListItemBody) => {
+ output.apply_role(Role::ListItem)
+ }
+ Some(Role::TableCell) => output.apply_role(Role::TableRow),
+ _ => {}
+ }
output.push_frame(pos, frame);
}
}
diff --git a/src/library/layout/page.rs b/src/library/layout/page.rs
index 8435e510..115a1923 100644
--- a/src/library/layout/page.rs
+++ b/src/library/layout/page.rs
@@ -110,16 +110,28 @@ impl PageNode {
let pad = padding.resolve(styles).relative_to(size);
let pw = size.x - pad.left - pad.right;
let py = size.y - pad.bottom;
- for (marginal, pos, area) in [
- (header, Point::with_x(pad.left), Size::new(pw, pad.top)),
- (footer, Point::new(pad.left, py), Size::new(pw, pad.bottom)),
- (foreground, Point::zero(), size),
- (background, Point::zero(), size),
+ for (role, marginal, pos, area) in [
+ (
+ Role::Header,
+ header,
+ Point::with_x(pad.left),
+ Size::new(pw, pad.top),
+ ),
+ (
+ Role::Footer,
+ footer,
+ Point::new(pad.left, py),
+ Size::new(pw, pad.bottom),
+ ),
+ (Role::Foreground, foreground, Point::zero(), size),
+ (Role::Background, background, Point::zero(), size),
] {
if let Some(content) = marginal.resolve(ctx, page)? {
let pod = Regions::one(area, area, Spec::splat(true));
- let sub = content.layout(ctx, &pod, styles)?.remove(0);
- if std::ptr::eq(marginal, background) {
+ let mut sub = content.layout(ctx, &pod, styles)?.remove(0);
+ Arc::make_mut(&mut sub).apply_role(role);
+
+ if role == Role::Background {
Arc::make_mut(frame).prepend_frame(pos, sub);
} else {
Arc::make_mut(frame).push_frame(pos, sub);
diff --git a/src/library/layout/stack.rs b/src/library/layout/stack.rs
index 828ff8e3..9c2cbccd 100644
--- a/src/library/layout/stack.rs
+++ b/src/library/layout/stack.rs
@@ -194,7 +194,12 @@ impl<'a> StackLayouter<'a> {
let frames = node.layout(ctx, &self.regions, styles)?;
let len = frames.len();
- for (i, frame) in frames.into_iter().enumerate() {
+ for (i, mut frame) in frames.into_iter().enumerate() {
+ // Set the generic block role.
+ if frame.role().map_or(true, Role::is_weak) {
+ Arc::make_mut(&mut frame).apply_role(Role::GenericBlock);
+ }
+
// Grow our size, shrink the region and save the frame for later.
let size = frame.size.to_gen(self.axis);
self.used.main += size.main;
diff --git a/src/library/math/rex.rs b/src/library/math/rex.rs
index 0268fb9c..f839a9e8 100644
--- a/src/library/math/rex.rs
+++ b/src/library/math/rex.rs
@@ -66,6 +66,7 @@ impl Layout for RexNode {
let mut backend = FrameBackend {
frame: {
let mut frame = Frame::new(size);
+ frame.apply_role(Role::Formula);
frame.baseline = Some(baseline);
frame
},
diff --git a/src/library/structure/heading.rs b/src/library/structure/heading.rs
index a0973b90..af2b3626 100644
--- a/src/library/structure/heading.rs
+++ b/src/library/structure/heading.rs
@@ -65,7 +65,8 @@ impl HeadingNode {
impl Show for HeadingNode {
fn unguard(&self, sel: Selector) -> ShowNode {
- Self { body: self.body.unguard(sel), ..*self }.pack()
+ let body = self.body.unguard(sel).role(Role::Heading(self.level.get()));
+ Self { body, ..*self }.pack()
}
fn encode(&self, _: StyleChain) -> Dict {
@@ -114,7 +115,7 @@ impl Show for HeadingNode {
realized = realized.underlined();
}
- realized = realized.styled_with_map(map);
+ realized = realized.styled_with_map(map).role(Role::Heading(self.level.get()));
realized = realized.spaced(
resolve!(Self::ABOVE).resolve(styles),
resolve!(Self::BELOW).resolve(styles),
diff --git a/src/library/structure/list.rs b/src/library/structure/list.rs
index 84603eb3..015ef520 100644
--- a/src/library/structure/list.rs
+++ b/src/library/structure/list.rs
@@ -78,7 +78,7 @@ impl<const L: ListKind> Show for ListNode<L> {
fn unguard(&self, sel: Selector) -> ShowNode {
Self {
items: self.items.map(|item| ListItem {
- body: Box::new(item.body.unguard(sel)),
+ body: Box::new(item.body.unguard(sel).role(Role::ListItemBody)),
..*item
}),
..*self
@@ -108,9 +108,15 @@ impl<const L: ListKind> Show for ListNode<L> {
for (item, map) in self.items.iter() {
number = item.number.unwrap_or(number);
+
cells.push(LayoutNode::default());
- cells
- .push(label.resolve(ctx, L, number)?.styled_with_map(map.clone()).pack());
+ cells.push(
+ label
+ .resolve(ctx, L, number)?
+ .styled_with_map(map.clone())
+ .role(Role::ListLabel)
+ .pack(),
+ );
cells.push(LayoutNode::default());
cells.push((*item.body).clone().styled_with_map(map.clone()).pack());
number += 1;
@@ -155,7 +161,9 @@ impl<const L: ListKind> Show for ListNode<L> {
}
}
- Ok(realized.spaced(above, below))
+ Ok(realized
+ .role(Role::List { ordered: L == ORDERED })
+ .spaced(above, below))
}
}
diff --git a/src/library/structure/table.rs b/src/library/structure/table.rs
index cd70db30..0f74fc96 100644
--- a/src/library/structure/table.rs
+++ b/src/library/structure/table.rs
@@ -52,7 +52,11 @@ impl Show for TableNode {
Self {
tracks: self.tracks.clone(),
gutter: self.gutter.clone(),
- cells: self.cells.iter().map(|cell| cell.unguard(sel)).collect(),
+ cells: self
+ .cells
+ .iter()
+ .map(|cell| cell.unguard(sel).role(Role::TableCell))
+ .collect(),
}
.pack()
}
@@ -100,7 +104,8 @@ impl Show for TableNode {
tracks: self.tracks.clone(),
gutter: self.gutter.clone(),
cells,
- }))
+ })
+ .role(Role::Table))
}
fn finalize(
diff --git a/src/library/text/par.rs b/src/library/text/par.rs
index 695d8066..41246b00 100644
--- a/src/library/text/par.rs
+++ b/src/library/text/par.rs
@@ -551,11 +551,14 @@ fn prepare<'a>(
} else {
let size = Size::new(regions.first.x, regions.base.y);
let pod = Regions::one(size, regions.base, Spec::splat(false));
+
let mut frame = node.layout(ctx, &pod, styles)?.remove(0);
let shift = styles.get(TextNode::BASELINE);
- if !shift.is_zero() {
- Arc::make_mut(&mut frame).translate(Point::with_y(shift));
+ if !shift.is_zero() || frame.role().map_or(true, Role::is_weak) {
+ let frame = Arc::make_mut(&mut frame);
+ frame.translate(Point::with_y(shift));
+ frame.apply_role(Role::GenericInline);
}
items.push(Item::Frame(frame));
@@ -1063,6 +1066,7 @@ fn stack(
let mut finished = vec![];
let mut first = true;
let mut output = Frame::new(Size::with_x(width));
+ output.apply_role(Role::Paragraph);
// Stack the lines into one frame per region.
for line in lines {
@@ -1072,6 +1076,7 @@ fn stack(
while !regions.first.y.fits(height) && !regions.in_last() {
finished.push(Arc::new(output));
output = Frame::new(Size::with_x(width));
+ output.apply_role(Role::Paragraph);
regions.next();
first = true;
}
diff --git a/src/library/text/raw.rs b/src/library/text/raw.rs
index a24d2170..31db811a 100644
--- a/src/library/text/raw.rs
+++ b/src/library/text/raw.rs
@@ -123,7 +123,7 @@ impl Show for RawNode {
realized = realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW));
}
- Ok(realized.styled_with_map(map))
+ Ok(realized.styled_with_map(map).role(Role::Code))
}
}
diff --git a/src/model/content.rs b/src/model/content.rs
index 21bf8369..3e27c02f 100644
--- a/src/model/content.rs
+++ b/src/model/content.rs
@@ -204,6 +204,11 @@ impl Content {
Self::Styled(Arc::new((self, styles)))
}
+ /// Assign a role to this content by adding a style map.
+ pub fn role(self, role: Role) -> Self {
+ self.styled_with_entry(StyleEntry::Role(role))
+ }
+
/// Reenable the show rule identified by the selector.
pub fn unguard(&self, sel: Selector) -> Self {
self.clone().styled_with_entry(StyleEntry::Unguard(sel))
diff --git a/src/model/layout.rs b/src/model/layout.rs
index b0247258..b4151c04 100644
--- a/src/model/layout.rs
+++ b/src/model/layout.rs
@@ -232,7 +232,16 @@ impl Layout for LayoutNode {
let at = ctx.pins.cursor();
let entry = StyleEntry::Barrier(Barrier::new(node.id()));
- let result = node.0.layout(ctx, regions, entry.chain(&styles));
+ let mut result = node.0.layout(ctx, regions, entry.chain(&styles));
+
+ if let Some(role) = styles.role() {
+ result = result.map(|mut frames| {
+ for frame in frames.iter_mut() {
+ Arc::make_mut(frame).apply_role(role);
+ }
+ frames
+ });
+ }
let fresh = ctx.pins.from(at);
let dirty = ctx.pins.dirty.get();
diff --git a/src/model/styles.rs b/src/model/styles.rs
index 9e723171..7d16f4ba 100644
--- a/src/model/styles.rs
+++ b/src/model/styles.rs
@@ -5,6 +5,7 @@ use std::marker::PhantomData;
use super::{Barrier, Content, Key, Property, Recipe, Selector, Show, Target};
use crate::diag::TypResult;
+use crate::frame::Role;
use crate::library::text::{FontFamily, TextNode};
use crate::util::ReadableTypeId;
use crate::Context;
@@ -170,6 +171,8 @@ pub enum StyleEntry {
Property(Property),
/// A show rule recipe.
Recipe(Recipe),
+ /// A semantic role.
+ Role(Role),
/// A barrier for scoped styles.
Barrier(Barrier),
/// Guards against recursive show rules.
@@ -229,6 +232,7 @@ impl Debug for StyleEntry {
match self {
Self::Property(property) => property.fmt(f)?,
Self::Recipe(recipe) => recipe.fmt(f)?,
+ Self::Role(role) => role.fmt(f)?,
Self::Barrier(barrier) => barrier.fmt(f)?,
Self::Guard(sel) => write!(f, "Guard against {sel:?}")?,
Self::Unguard(sel) => write!(f, "Unguard against {sel:?}")?,
@@ -324,8 +328,23 @@ impl<'a> StyleChain<'a> {
Ok(realized)
}
+ /// Retrieve the current role
+ pub fn role(self) -> Option<Role> {
+ let mut depth = 0;
+
+ for entry in self.entries() {
+ match *entry {
+ StyleEntry::Role(role) => return Some(role),
+ StyleEntry::Barrier(_) if depth == 1 => return None,
+ StyleEntry::Barrier(_) => depth += 1,
+ _ => {}
+ }
+ }
+ None
+ }
+
/// Whether the recipe identified by the selector is guarded.
- fn guarded(&self, sel: Selector) -> bool {
+ fn guarded(self, sel: Selector) -> bool {
for entry in self.entries() {
match *entry {
StyleEntry::Guard(s) if s == sel => return true,