summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Haug <mhaug@live.de>2022-06-04 12:57:45 +0200
committerMartin Haug <mhaug@live.de>2022-06-04 12:57:45 +0200
commit4640585fbdf72df993dbed46799844aa78996cce (patch)
tree38a09389885a61068970441d6d27178a2ae4f115
parenta937462491a63f5cff3551b5bb8bc45fb350f0b6 (diff)
First iteration of outline items
-rw-r--r--src/export/pdf.rs190
-rw-r--r--src/frame.rs99
-rw-r--r--src/library/graphics/hide.rs6
-rw-r--r--src/library/graphics/shape.rs3
-rw-r--r--src/library/layout/flow.rs7
-rw-r--r--src/library/layout/grid.rs44
-rw-r--r--src/library/layout/page.rs23
-rw-r--r--src/library/layout/stack.rs3
-rw-r--r--src/library/math/rex.rs1
-rw-r--r--src/library/structure/heading.rs12
-rw-r--r--src/library/structure/list.rs20
-rw-r--r--src/library/structure/table.rs16
-rw-r--r--src/library/text/par.rs5
-rw-r--r--src/library/text/raw.rs2
-rw-r--r--src/model/layout.rs11
-rw-r--r--src/model/styles.rs28
16 files changed, 445 insertions, 25 deletions
diff --git a/src/export/pdf.rs b/src/export/pdf.rs
index aa7acd41..843e6f37 100644
--- a/src/export/pdf.rs
+++ b/src/export/pdf.rs
@@ -16,7 +16,7 @@ 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,
@@ -313,6 +313,8 @@ impl<'a> PdfExporter<'a> {
}
let mut languages = HashMap::new();
+ let mut heading_tree: Vec<HeadingNode> = vec![];
+
for (page, page_id) in self.pages.into_iter().zip(page_refs.iter()) {
let content_id = self.alloc.bump();
@@ -356,6 +358,20 @@ impl<'a> PdfExporter<'a> {
.or_insert_with(|| count);
}
+ for heading in page.headings.into_iter() {
+ if let Some(last) = heading_tree.pop() {
+ let new = last.clone().insert(heading.clone(), *page_id, 1);
+ if let Some(new) = new {
+ heading_tree.push(new);
+ } else {
+ heading_tree.push(last);
+ heading_tree.push(HeadingNode::Leaf(heading, *page_id))
+ }
+ } else {
+ heading_tree.push(HeadingNode::Leaf(heading, *page_id))
+ }
+ }
+
self.writer
.stream(content_id, &deflate(&page.content.finish()))
.filter(Filter::FlateDecode);
@@ -388,6 +404,39 @@ impl<'a> PdfExporter<'a> {
resources.finish();
pages.finish();
+ // Build the heading tree.
+ let outline_root_id = self.alloc.bump();
+
+ let start_ref = self.alloc.bump();
+ let mut current_ref = start_ref;
+ let mut prev_ref = None;
+
+ for (i, node) in heading_tree.iter().enumerate() {
+ let next = write_outline_item(
+ &mut self.writer,
+ node,
+ current_ref,
+ prev_ref,
+ i == heading_tree.len() - 1,
+ outline_root_id,
+ );
+ prev_ref = Some(current_ref);
+ current_ref = next;
+ }
+
+
+ self.alloc = Ref::new(
+ start_ref.get()
+ + heading_tree.iter().map(HeadingNode::len).sum::<usize>() as i32,
+ );
+
+ if let Some(prev_ref) = prev_ref {
+ let mut outline_root = self.writer.outline(outline_root_id);
+ outline_root.first(start_ref);
+ outline_root.last(prev_ref);
+ outline_root.count(heading_tree.len() as i32);
+ }
+
let lang = languages
.into_iter()
.max_by(|(_, v1), (_, v2)| v1.cmp(v2))
@@ -405,6 +454,11 @@ impl<'a> PdfExporter<'a> {
catalog.pages(page_tree_ref);
catalog.viewer_preferences().direction(dir);
+
+ if !heading_tree.is_empty() {
+ catalog.outlines(outline_root_id);
+ }
+
if let Some(lang) = lang {
catalog.lang(TextStr(lang.as_str()));
}
@@ -426,6 +480,7 @@ struct PageExporter<'a> {
links: Vec<(Destination, Rect)>,
state: State,
saves: Vec<State>,
+ headings: Vec<Heading>,
}
/// Data for an exported page.
@@ -434,6 +489,7 @@ struct Page {
content: Content,
links: Vec<(Destination, Rect)>,
languages: HashMap<Lang, usize>,
+ headings: Vec<Heading>,
}
/// A simulated graphics state used to deduplicate graphics state changes and
@@ -448,6 +504,64 @@ struct State {
stroke_space: Option<Name<'static>>,
}
+/// A heading that can later be linked in the outline panel.
+#[derive(Debug, Clone)]
+struct Heading {
+ content: String,
+ level: usize,
+ position: Point,
+}
+
+#[derive(Debug, Clone)]
+enum HeadingNode {
+ Leaf(Heading, Ref),
+ Branch(Heading, Ref, Vec<HeadingNode>),
+}
+
+impl HeadingNode {
+ fn heading(&self) -> &Heading {
+ match self {
+ HeadingNode::Leaf(h, _) => h,
+ HeadingNode::Branch(h, _, _) => h,
+ }
+ }
+
+ fn reference(&self) -> Ref {
+ match self {
+ HeadingNode::Leaf(_, r) => *r,
+ HeadingNode::Branch(_, r, _) => *r,
+ }
+ }
+
+ fn len(&self) -> usize {
+ match self {
+ HeadingNode::Leaf(_, _) => 1,
+ HeadingNode::Branch(_, _, children) => {
+ 1 + children.iter().map(|c| c.len()).sum::<usize>()
+ }
+ }
+ }
+
+ fn insert(self, other: Heading, page: Ref, level: usize) -> Option<Self> {
+ if level >= other.level {
+ return None;
+ }
+
+ let mut node = match self {
+ HeadingNode::Leaf(h, r) => (h, r, vec![]),
+ HeadingNode::Branch(h, r, v) if level + 1 == other.level => (h, r, v),
+ HeadingNode::Branch(h, r, mut v) => {
+ let new = v.pop().unwrap().insert(other, page, level + 1).unwrap();
+ v.push(new);
+ return Some(HeadingNode::Branch(h, r, v));
+ }
+ };
+
+ node.2.push(HeadingNode::Leaf(other, page));
+ Some(HeadingNode::Branch(node.0, node.1, node.2))
+ }
+}
+
impl<'a> PageExporter<'a> {
fn new(exporter: &'a mut PdfExporter) -> Self {
Self {
@@ -461,6 +575,7 @@ impl<'a> PageExporter<'a> {
links: vec![],
state: State::default(),
saves: vec![],
+ headings: vec![],
}
}
@@ -481,10 +596,22 @@ impl<'a> PageExporter<'a> {
content: self.content,
links: self.links,
languages: self.languages,
+ headings: self.headings,
}
}
fn write_frame(&mut self, frame: &Frame) {
+ if let Some(Role::Heading(level)) = frame.role() {
+ self.headings.push(Heading {
+ position: Point::new(
+ self.state.transform.tx,
+ self.state.transform.ty + Length::pt(3.0),
+ ),
+ content: frame.inner_text().to_string(),
+ level,
+ })
+ }
+
for &(pos, ref element) in &frame.elements {
let x = pos.x.to_f32();
let y = pos.y.to_f32();
@@ -815,6 +942,67 @@ fn encode_image(img: &RasterImage) -> ImageResult<(Vec<u8>, Filter, bool)> {
})
}
+fn write_outline_item(
+ writer: &mut PdfWriter,
+ node: &HeadingNode,
+ current_ref: Ref,
+ prev_ref: Option<Ref>,
+ is_last: bool,
+ parent_ref: Ref,
+) -> Ref {
+ let mut outline = writer.outline_item(current_ref);
+ let next = Ref::new(current_ref.get() + node.len() as i32);
+ outline.parent(parent_ref);
+
+ if !is_last {
+ outline.next(next);
+ }
+
+ if let Some(prev_ref) = prev_ref {
+ outline.prev(prev_ref);
+ }
+
+ if let HeadingNode::Branch(_, _, children) = node {
+ let current_child = Ref::new(current_ref.get() + 1);
+ if children.len() > 0 {
+ outline.first(current_child);
+ outline.last(Ref::new(next.get() - 1));
+ }
+
+ outline.count(-1 * children.len() as i32);
+ }
+
+ let heading = node.heading();
+ outline.title(TextStr(&heading.content));
+ outline.dest_direct().page(node.reference()).xyz(
+ heading.position.x.to_f32(),
+ heading.position.y.to_f32(),
+ None,
+ );
+
+ outline.finish();
+
+ if let HeadingNode::Branch(_, _, children) = node {
+ let mut current_child = Ref::new(current_ref.get() + 1);
+ let mut prev_ref = None;
+
+ for (i, child) in children.iter().enumerate() {
+ write_outline_item(
+ writer,
+ child,
+ current_child,
+ prev_ref,
+ i == children.len() - 1,
+ current_ref,
+ );
+ prev_ref = Some(current_child);
+ current_child = Ref::new(current_child.get() + 1);
+ }
+ }
+
+ next
+}
+
/// Encode an image's alpha channel if present.
fn encode_alpha(img: &RasterImage) -> (Vec<u8>, Filter) {
let pixels: Vec<_> = img.buf.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect();
diff --git a/src/frame.rs b/src/frame.rs
index 1bd1f454..8b14b2b1 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,12 @@ 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() || self.role.is_none())
+ {
+ if self.role.is_none() {
+ self.role = frame.as_ref().role()
+ }
frame.inline(self, self.layer(), pos);
} else {
self.elements.push((pos, Element::Group(Group::new(frame.share()))));
@@ -80,7 +97,12 @@ 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() || self.role.is_none())
+ {
+ if self.role.is_none() {
+ self.role = frame.as_ref().role()
+ }
frame.inline(self, 0, pos);
} else {
self.elements
@@ -125,6 +147,15 @@ 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) {
+ match self.role {
+ None => self.role = Some(role),
+ Some(old) if old.is_weak() => self.role = Some(role),
+ Some(_) => {}
+ }
+ }
+
/// Clip the contents of a frame to its size.
pub fn clip(&mut self) {
self.group(|g| g.clips = true);
@@ -146,10 +177,26 @@ 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 inner_text(&self) -> EcoString {
+ let mut res = EcoString::new();
+ for (_, element) in &self.elements {
+ match element {
+ Element::Text(text) => res.push_str(
+ &text.glyphs.iter().map(|glyph| glyph.c).collect::<EcoString>(),
+ ),
+ Element::Group(group) => res.push_str(&group.frame.inner_text()),
+ _ => {}
+ }
+ }
+ res
+ }
}
impl Debug for Frame {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+ self.role.fmt(f)?;
f.debug_list()
.entries(self.elements.iter().map(|(_, element)| element))
.finish()
@@ -362,3 +409,49 @@ 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(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,
+}
+
+impl Role {
+ 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..8070231e 100644
--- a/src/library/graphics/shape.rs
+++ b/src/library/graphics/shape.rs
@@ -91,6 +91,9 @@ impl<const S: ShapeKind> Layout for ShapeNode<S> {
let child = child.clone().padded(inset.map(|side| side.map(RawLength::from)));
let mut pod = Regions::one(regions.first, regions.base, regions.expand);
+ let role_map = StyleMap::with_role(Role::GenericBlock);
+ let styles = role_map.chain(&styles);
+
frames = child.layout(ctx, &pod, styles)?;
// Relayout with full expansion into square region to make sure
diff --git a/src/library/layout/flow.rs b/src/library/layout/flow.rs
index 6193a68f..0ba84b09 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().is_none() {
+ 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..2517d193 100644
--- a/src/library/layout/grid.rs
+++ b/src/library/layout/grid.rs
@@ -9,6 +9,8 @@ pub struct GridNode {
pub gutter: Spec<Vec<TrackSizing>>,
/// The nodes to be arranged in a grid.
pub cells: Vec<LayoutNode>,
+ /// The role of the grid in the semantic tree.
+ pub semantic: GridSemantics,
}
#[node]
@@ -26,6 +28,7 @@ impl GridNode {
row_gutter.unwrap_or(base_gutter),
),
cells: args.all()?,
+ semantic: GridSemantics::None,
}))
}
}
@@ -45,6 +48,7 @@ impl Layout for GridNode {
&self.cells,
regions,
styles,
+ self.semantic,
);
// Measure the columns and layout the grid row-by-row.
@@ -65,6 +69,28 @@ pub enum TrackSizing {
Fractional(Fraction),
}
+/// Defines what kind of semantics a grid should represent.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum GridSemantics {
+ /// The grid is transparent to the semantic tree.
+ None,
+ /// The grid is a list, its rows are list items. The bool indicates whether
+ /// the list is ordered.
+ List,
+ /// The grid is a table.
+ Table,
+}
+
+impl GridSemantics {
+ fn row(self) -> Option<Role> {
+ match self {
+ Self::None => None,
+ Self::List => Some(Role::ListItem),
+ Self::Table => Some(Role::TableRow),
+ }
+ }
+}
+
castable! {
Vec<TrackSizing>,
Expected: "integer, auto, relative length, fraction, or array of the latter three)",
@@ -104,6 +130,8 @@ pub struct GridLayouter<'a> {
regions: Regions,
/// The inherited styles.
styles: StyleChain<'a>,
+ /// The role of the grid in the semantic tree.
+ semantic: GridSemantics,
/// Resolved column sizes.
rcols: Vec<Length>,
/// Rows in the current region.
@@ -139,6 +167,7 @@ impl<'a> GridLayouter<'a> {
cells: &'a [LayoutNode],
regions: &Regions,
styles: StyleChain<'a>,
+ semantic: GridSemantics,
) -> Self {
let mut cols = vec![];
let mut rows = vec![];
@@ -193,6 +222,7 @@ impl<'a> GridLayouter<'a> {
rows,
regions,
styles,
+ semantic,
rcols,
lrows,
full,
@@ -450,6 +480,10 @@ 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));
+ if let Some(role) = self.semantic.row() {
+ output.apply_role(role);
+ }
+
let mut pos = Point::zero();
for (x, &rcol) in self.rcols.iter().enumerate() {
@@ -464,6 +498,7 @@ impl<'a> GridLayouter<'a> {
let pod = Regions::one(size, base, Spec::splat(true));
let frame = node.layout(self.ctx, &pod, self.styles)?.remove(0);
+
output.push_frame(pos, frame);
}
@@ -482,7 +517,14 @@ impl<'a> GridLayouter<'a> {
// Prepare frames.
let mut outputs: Vec<_> = heights
.iter()
- .map(|&h| Frame::new(Size::new(self.used.x, h)))
+ .map(|&h| {
+ let mut f = Frame::new(Size::new(self.used.x, h));
+ if let Some(role) = self.semantic.row() {
+ f.apply_role(role);
+ }
+
+ f
+ })
.collect();
// Prepare regions.
diff --git a/src/library/layout/page.rs b/src/library/layout/page.rs
index 8435e510..d524839b 100644
--- a/src/library/layout/page.rs
+++ b/src/library/layout/page.rs
@@ -110,15 +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 (marginal, pos, area, role) in [
+ (
+ header,
+ Point::with_x(pad.left),
+ Size::new(pw, pad.top),
+ Role::Header,
+ ),
+ (
+ footer,
+ Point::new(pad.left, py),
+ Size::new(pw, pad.bottom),
+ Role::Footer,
+ ),
+ (foreground, Point::zero(), size, Role::Background),
+ (background, Point::zero(), size, Role::Background),
] {
if let Some(content) = marginal.resolve(ctx, page)? {
let pod = Regions::one(area, area, Spec::splat(true));
+ let role_map = StyleMap::with_role(role);
+ let styles = role_map.chain(&styles);
let sub = content.layout(ctx, &pod, styles)?.remove(0);
+
if std::ptr::eq(marginal, background) {
Arc::make_mut(frame).prepend_frame(pos, sub);
} else {
diff --git a/src/library/layout/stack.rs b/src/library/layout/stack.rs
index 828ff8e3..7bad01d9 100644
--- a/src/library/layout/stack.rs
+++ b/src/library/layout/stack.rs
@@ -192,6 +192,9 @@ impl<'a> StackLayouter<'a> {
self.dir.start().into()
});
+ let role_map = StyleMap::with_role(Role::GenericBlock);
+ let styles = role_map.chain(&styles);
+
let frames = node.layout(ctx, &self.regions, styles)?;
let len = frames.len();
for (i, frame) in frames.into_iter().enumerate() {
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..285793dd 100644
--- a/src/library/structure/heading.rs
+++ b/src/library/structure/heading.rs
@@ -1,6 +1,7 @@
use crate::library::layout::BlockSpacing;
use crate::library::prelude::*;
use crate::library::text::{FontFamily, TextNode, TextSize};
+use crate::model::StyleEntry;
/// A section heading.
#[derive(Debug, Hash)]
@@ -65,7 +66,13 @@ impl HeadingNode {
impl Show for HeadingNode {
fn unguard(&self, sel: Selector) -> ShowNode {
- Self { body: self.body.unguard(sel), ..*self }.pack()
+ let mut map = StyleMap::with_role(Role::Heading(self.level.get()));
+ map.push(StyleEntry::Unguard(sel));
+ Self {
+ body: self.body.clone().styled_with_map(map),
+ ..*self
+ }
+ .pack()
}
fn encode(&self, _: StyleChain) -> Dict {
@@ -91,7 +98,8 @@ impl Show for HeadingNode {
};
}
- let mut map = StyleMap::new();
+ let mut map = StyleMap::with_role(Role::Heading(self.level.get()));
+
map.set(TextNode::SIZE, resolve!(Self::SIZE));
if let Smart::Custom(family) = resolve!(Self::FAMILY) {
diff --git a/src/library/structure/list.rs b/src/library/structure/list.rs
index 84603eb3..563426b4 100644
--- a/src/library/structure/list.rs
+++ b/src/library/structure/list.rs
@@ -2,10 +2,11 @@ use std::fmt::Write;
use unscanny::Scanner;
-use crate::library::layout::{BlockSpacing, GridNode, TrackSizing};
+use crate::library::layout::{BlockSpacing, GridNode, GridSemantics, TrackSizing};
use crate::library::prelude::*;
use crate::library::text::ParNode;
use crate::library::utility::Numbering;
+use crate::model::StyleEntry;
/// An unordered (bulleted) or ordered (numbered) list.
#[derive(Debug, Hash)]
@@ -76,9 +77,12 @@ impl<const L: ListKind> ListNode<L> {
impl<const L: ListKind> Show for ListNode<L> {
fn unguard(&self, sel: Selector) -> ShowNode {
+ let mut map = StyleMap::with_role(Role::ListItemBody);
+ map.push(StyleEntry::Unguard(sel));
+
Self {
items: self.items.map(|item| ListItem {
- body: Box::new(item.body.unguard(sel)),
+ body: Box::new(item.body.clone().styled_with_map(map.clone())),
..*item
}),
..*self
@@ -108,9 +112,12 @@ impl<const L: ListKind> Show for ListNode<L> {
for (item, map) in self.items.iter() {
number = item.number.unwrap_or(number);
+
+ let mut label_map = map.clone();
+ label_map.push(StyleEntry::Role(Role::ListLabel));
+
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(label_map).pack());
cells.push(LayoutNode::default());
cells.push((*item.body).clone().styled_with_map(map.clone()).pack());
number += 1;
@@ -134,6 +141,7 @@ impl<const L: ListKind> Show for ListNode<L> {
]),
gutter: Spec::with_y(vec![TrackSizing::Relative(gutter.into())]),
cells,
+ semantic: GridSemantics::List,
}))
}
@@ -155,7 +163,9 @@ impl<const L: ListKind> Show for ListNode<L> {
}
}
- Ok(realized.spaced(above, below))
+ Ok(realized
+ .styled_with_map(StyleMap::with_role(Role::List(L == ORDERED)))
+ .spaced(above, below))
}
}
diff --git a/src/library/structure/table.rs b/src/library/structure/table.rs
index cd70db30..60115612 100644
--- a/src/library/structure/table.rs
+++ b/src/library/structure/table.rs
@@ -1,5 +1,6 @@
-use crate::library::layout::{BlockSpacing, GridNode, TrackSizing};
+use crate::library::layout::{BlockSpacing, GridNode, GridSemantics, TrackSizing};
use crate::library::prelude::*;
+use crate::model::StyleEntry;
/// A table of items.
#[derive(Debug, Hash)]
@@ -49,10 +50,17 @@ impl TableNode {
impl Show for TableNode {
fn unguard(&self, sel: Selector) -> ShowNode {
+ let mut map = StyleMap::with_role(Role::TableCell);
+ map.push(StyleEntry::Unguard(sel));
+
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.clone().styled_with_map(map.clone()))
+ .collect(),
}
.pack()
}
@@ -100,7 +108,9 @@ impl Show for TableNode {
tracks: self.tracks.clone(),
gutter: self.gutter.clone(),
cells,
- }))
+ semantic: GridSemantics::Table,
+ })
+ .styled_with_map(StyleMap::with_role(Role::Table)))
}
fn finalize(
diff --git a/src/library/text/par.rs b/src/library/text/par.rs
index 695d8066..53bb798f 100644
--- a/src/library/text/par.rs
+++ b/src/library/text/par.rs
@@ -551,6 +551,9 @@ 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 role_map = StyleMap::with_role(Role::GenericInline);
+ let styles = role_map.chain(&styles);
+
let mut frame = node.layout(ctx, &pod, styles)?.remove(0);
let shift = styles.get(TextNode::BASELINE);
@@ -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..4d73b11b 100644
--- a/src/library/text/raw.rs
+++ b/src/library/text/raw.rs
@@ -113,7 +113,7 @@ impl Show for RawNode {
styles: StyleChain,
mut realized: Content,
) -> TypResult<Content> {
- let mut map = StyleMap::new();
+ let mut map = StyleMap::with_role(Role::Code);
map.set_family(styles.get(Self::FAMILY).clone(), styles);
map.set(TextNode::OVERHANG, false);
map.set(TextNode::HYPHENATE, Smart::Custom(Hyphenate(false)));
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..7db2df42 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;
@@ -36,6 +37,13 @@ impl StyleMap {
styles
}
+ /// Create a style map from a single role.
+ pub fn with_role(role: Role) -> Self {
+ let mut styles = Self::new();
+ styles.push(StyleEntry::Role(role));
+ styles
+ }
+
/// Set an inner value for a style property.
///
/// If the property needs folding and the value is already contained in the
@@ -170,6 +178,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 +239,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 +335,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,