Merge pull request #75 from typst/semantics

Frame Role and PDF outline
This commit is contained in:
Laurenz 2022-06-08 19:31:07 +02:00 committed by GitHub
commit cd5a14bc24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 458 additions and 128 deletions

View File

@ -16,12 +16,13 @@ use ttf_parser::{name_id, GlyphId, Tag};
use super::subset::subset; use super::subset::subset;
use crate::font::{find_name, FaceId, FontStore}; 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::{ use crate::geom::{
self, Color, Dir, Em, Geometry, Length, Numeric, Paint, Point, Ratio, Shape, Size, self, Color, Dir, Em, Geometry, Length, Numeric, Paint, Point, Ratio, Shape, Size,
Stroke, Transform, Stroke, Transform,
}; };
use crate::image::{Image, ImageId, ImageStore, RasterImage}; use crate::image::{Image, ImageId, ImageStore, RasterImage};
use crate::library::prelude::EcoString;
use crate::library::text::Lang; use crate::library::text::Lang;
use crate::Context; use crate::Context;
@ -42,31 +43,43 @@ const SRGB_GRAY: Name<'static> = Name(b"srgbgray");
/// An exporter for a whole PDF document. /// An exporter for a whole PDF document.
struct PdfExporter<'a> { struct PdfExporter<'a> {
writer: PdfWriter,
fonts: &'a FontStore, fonts: &'a FontStore,
images: &'a ImageStore, images: &'a ImageStore,
writer: PdfWriter,
alloc: Ref,
pages: Vec<Page>, pages: Vec<Page>,
face_map: Remapper<FaceId>, page_heights: Vec<f32>,
alloc: Ref,
page_tree_ref: Ref,
face_refs: Vec<Ref>, face_refs: Vec<Ref>,
glyph_sets: HashMap<FaceId, HashSet<u16>>,
image_map: Remapper<ImageId>,
image_refs: Vec<Ref>, 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> { impl<'a> PdfExporter<'a> {
fn new(ctx: &'a Context) -> Self { fn new(ctx: &'a Context) -> Self {
let mut alloc = Ref::new(1);
let page_tree_ref = alloc.bump();
Self { Self {
writer: PdfWriter::new(),
fonts: &ctx.fonts, fonts: &ctx.fonts,
images: &ctx.images, images: &ctx.images,
writer: PdfWriter::new(),
alloc: Ref::new(1),
pages: vec![], pages: vec![],
face_map: Remapper::new(), page_heights: vec![],
alloc,
page_tree_ref,
page_refs: vec![],
face_refs: vec![], face_refs: vec![],
glyph_sets: HashMap::new(),
image_map: Remapper::new(),
image_refs: vec![], 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.build_pages(frames);
self.write_fonts(); self.write_fonts();
self.write_images(); 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>]) { fn build_pages(&mut self, frames: &[Arc<Frame>]) {
for frame in frames { 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); self.pages.push(page);
} }
} }
@ -299,70 +324,52 @@ impl<'a> PdfExporter<'a> {
} }
} }
fn write_structure(mut self) -> Vec<u8> { fn write_page(&mut self, page: Page) {
// The root page tree. let content_id = self.alloc.bump();
let page_tree_ref = self.alloc.bump();
// The page objects (non-root nodes in the page tree). let mut page_writer = self.writer.page(page.id);
let mut page_refs = vec![]; page_writer.parent(self.page_tree_ref);
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(); let w = page.size.x.to_f32();
for (page, page_id) in self.pages.into_iter().zip(page_refs.iter()) { let h = page.size.y.to_f32();
let content_id = self.alloc.bump(); page_writer.media_box(Rect::new(0.0, 0.0, w, h));
page_writer.contents(content_id);
let mut page_writer = self.writer.page(*page_id); let mut annotations = page_writer.annotations();
page_writer.parent(page_tree_ref); for (dest, rect) in page.links {
let mut link = annotations.push();
let w = page.size.x.to_f32(); link.subtype(AnnotationType::Link).rect(rect);
let h = page.size.y.to_f32(); match dest {
page_writer.media_box(Rect::new(0.0, 0.0, w, h)); Destination::Url(uri) => {
page_writer.contents(content_id); link.action()
.action_type(ActionType::Uri)
let mut annotations = page_writer.annotations(); .uri(Str(uri.as_str().as_bytes()));
for (dest, rect) in page.links { }
let mut link = annotations.push(); Destination::Internal(loc) => {
link.subtype(AnnotationType::Link).rect(rect); let index = loc.page - 1;
match dest { let height = self.page_heights[index];
Destination::Url(uri) => { link.action()
link.action() .action_type(ActionType::GoTo)
.action_type(ActionType::Uri) .destination_direct()
.uri(Str(uri.as_str().as_bytes())); .page(self.page_refs[index])
} .xyz(loc.pos.x.to_f32(), height - loc.pos.y.to_f32(), None);
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);
}
} }
} }
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);
} }
let mut pages = self.writer.pages(page_tree_ref); annotations.finish();
pages.count(page_refs.len() as i32).kids(page_refs); page_writer.finish();
self.writer
.stream(content_id, &deflate(&page.content.finish()))
.filter(Filter::FlateDecode);
}
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 resources = pages.resources();
let mut spaces = resources.color_spaces(); let mut spaces = resources.color_spaces();
@ -387,11 +394,36 @@ impl<'a> PdfExporter<'a> {
images.finish(); images.finish();
resources.finish(); resources.finish();
pages.finish(); pages.finish();
}
let lang = languages fn write_catalog(&mut self) {
.into_iter() // Build the outline tree.
.max_by(|(_, v1), (_, v2)| v1.cmp(v2)) let outline_root_id = (!self.heading_tree.is_empty()).then(|| self.alloc.bump());
.map(|(k, _)| k); 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) { let dir = if lang.map(Lang::dir) == Some(Dir::RTL) {
Direction::R2L Direction::R2L
@ -402,38 +434,88 @@ impl<'a> PdfExporter<'a> {
// Write the document information, catalog and wrap it up! // Write the document information, catalog and wrap it up!
self.writer.document_info(self.alloc.bump()).creator(TextStr("Typst")); self.writer.document_info(self.alloc.bump()).creator(TextStr("Typst"));
let mut catalog = self.writer.catalog(self.alloc.bump()); 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); catalog.viewer_preferences().direction(dir);
if let Some(outline_root_id) = outline_root_id {
catalog.outlines(outline_root_id);
}
if let Some(lang) = lang { if let Some(lang) = lang {
catalog.lang(TextStr(lang.as_str())); catalog.lang(TextStr(lang.as_str()));
} }
catalog.finish(); 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. /// An exporter for the contents of a single PDF page.
struct PageExporter<'a> { struct PageExporter<'a, 'b> {
fonts: &'a FontStore, exporter: &'a mut PdfExporter<'b>,
font_map: &'a mut Remapper<FaceId>, page_ref: Ref,
image_map: &'a mut Remapper<ImageId>,
glyphs: &'a mut HashMap<FaceId, HashSet<u16>>,
languages: HashMap<Lang, usize>,
bottom: f32,
content: Content, content: Content,
links: Vec<(Destination, Rect)>,
state: State, state: State,
saves: Vec<State>, saves: Vec<State>,
bottom: f32,
links: Vec<(Destination, Rect)>,
} }
/// Data for an exported page. /// Data for an exported page.
struct Page { struct Page {
id: Ref,
size: Size, size: Size,
content: Content, content: Content,
links: Vec<(Destination, Rect)>, links: Vec<(Destination, Rect)>,
languages: HashMap<Lang, usize>,
} }
/// A simulated graphics state used to deduplicate graphics state changes and /// A simulated graphics state used to deduplicate graphics state changes and
@ -448,19 +530,56 @@ struct State {
stroke_space: Option<Name<'static>>, stroke_space: Option<Name<'static>>,
} }
impl<'a> PageExporter<'a> { /// A heading that can later be linked in the outline panel.
fn new(exporter: &'a mut PdfExporter) -> Self { #[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 { Self {
fonts: exporter.fonts, exporter,
font_map: &mut exporter.face_map, page_ref,
image_map: &mut exporter.image_map,
glyphs: &mut exporter.glyph_sets,
languages: HashMap::new(),
bottom: 0.0,
content: Content::new(), content: Content::new(),
links: vec![],
state: State::default(), state: State::default(),
saves: vec![], saves: vec![],
bottom: 0.0,
links: vec![],
} }
} }
@ -479,12 +598,29 @@ impl<'a> PageExporter<'a> {
Page { Page {
size: frame.size, size: frame.size,
content: self.content, content: self.content,
id: self.page_ref,
links: self.links, links: self.links,
languages: self.languages,
} }
} }
fn write_frame(&mut self, frame: &Frame) { 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 { for &(pos, ref element) in &frame.elements {
let x = pos.x.to_f32(); let x = pos.x.to_f32();
let y = pos.y.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) { fn write_text(&mut self, x: f32, y: f32, text: &Text) {
*self.languages.entry(text.lang).or_insert(0) += text.glyphs.len(); *self.exporter.languages.entry(text.lang).or_insert(0) += text.glyphs.len();
self.glyphs self.exporter
.glyph_sets
.entry(text.face_id) .entry(text.face_id)
.or_default() .or_default()
.extend(text.glyphs.iter().map(|g| g.id)); .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_fill(text.fill);
self.set_font(text.face_id, text.size); 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) { fn write_image(&mut self, x: f32, y: f32, id: ImageId, size: Size) {
self.image_map.insert(id); self.exporter.image_map.insert(id);
let name = format_eco!("Im{}", self.image_map.map(id)); let name = format_eco!("Im{}", self.exporter.image_map.map(id));
let w = size.x.to_f32(); let w = size.x.to_f32();
let h = size.y.to_f32(); let h = size.y.to_f32();
self.content.save_state(); self.content.save_state();
@ -705,8 +842,8 @@ impl<'a> PageExporter<'a> {
fn set_font(&mut self, face_id: FaceId, size: Length) { fn set_font(&mut self, face_id: FaceId, size: Length) {
if self.state.font != Some((face_id, size)) { if self.state.font != Some((face_id, size)) {
self.font_map.insert(face_id); self.exporter.face_map.insert(face_id);
let name = format_eco!("F{}", self.font_map.map(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.content.set_font(Name(name.as_bytes()), size.to_f32());
self.state.font = Some((face_id, size)); self.state.font = Some((face_id, size));
} }

View File

@ -22,6 +22,8 @@ pub struct Frame {
pub baseline: Option<Length>, pub baseline: Option<Length>,
/// The elements composing this layout. /// The elements composing this layout.
pub elements: Vec<(Point, Element)>, pub elements: Vec<(Point, Element)>,
/// The semantic role of the frame.
role: Option<Role>,
} }
impl Frame { impl Frame {
@ -29,7 +31,12 @@ impl Frame {
#[track_caller] #[track_caller]
pub fn new(size: Size) -> Self { pub fn new(size: Size) -> Self {
assert!(size.is_finite()); assert!(size.is_finite());
Self { size, baseline: None, elements: vec![] } Self {
size,
baseline: None,
elements: vec![],
role: None,
}
} }
/// The baseline of the frame. /// The baseline of the frame.
@ -43,6 +50,11 @@ impl Frame {
self.elements.len() self.elements.len()
} }
/// The role of the frame.
pub fn role(&self) -> Option<Role> {
self.role
}
/// Whether the frame has comparatively few elements. /// Whether the frame has comparatively few elements.
pub fn is_light(&self) -> bool { pub fn is_light(&self) -> bool {
self.elements.len() <= 5 self.elements.len() <= 5
@ -58,7 +70,9 @@ impl Frame {
/// Automatically decides whether to inline the frame or to include it as a /// Automatically decides whether to inline the frame or to include it as a
/// group based on the number of elements in the frame. /// group based on the number of elements in the frame.
pub fn push_frame(&mut self, pos: Point, frame: impl FrameRepr) { 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); frame.inline(self, self.layer(), pos);
} else { } else {
self.elements.push((pos, Element::Group(Group::new(frame.share())))); 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. /// Add a frame at a position in the background.
pub fn prepend_frame(&mut self, pos: Point, frame: impl FrameRepr) { 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); frame.inline(self, 0, pos);
} else { } else {
self.elements self.elements
@ -125,6 +141,13 @@ impl Frame {
self.group(|g| g.transform = transform); 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. /// Clip the contents of a frame to its size.
pub fn clip(&mut self) { pub fn clip(&mut self) {
self.group(|g| g.clips = true); self.group(|g| g.clips = true);
@ -146,10 +169,31 @@ impl Frame {
pub fn link(&mut self, dest: Destination) { pub fn link(&mut self, dest: Destination) {
self.push(Point::zero(), Element::Link(dest, self.size)); 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 { impl Debug for Frame {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
if let Some(role) = self.role {
write!(f, "{role:?} ")?;
}
f.debug_list() f.debug_list()
.entries(self.elements.iter().map(|(_, element)| element)) .entries(self.elements.iter().map(|(_, element)| element))
.finish() .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,
}
}
}

View File

@ -22,7 +22,11 @@ impl Layout for HideNode {
// Clear the frames. // Clear the frames.
for frame in &mut 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) Ok(frames)

View File

@ -93,6 +93,10 @@ impl<const S: ShapeKind> Layout for ShapeNode<S> {
let mut pod = Regions::one(regions.first, regions.base, regions.expand); let mut pod = Regions::one(regions.first, regions.base, regions.expand);
frames = child.layout(ctx, &pod, styles)?; 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 // Relayout with full expansion into square region to make sure
// the result is really a square or circle. // the result is really a square or circle.
if is_quadratic(S) { if is_quadratic(S) {

View File

@ -182,7 +182,12 @@ impl FlowLayouter {
let frames = node.layout(ctx, &self.regions, styles)?; let frames = node.layout(ctx, &self.regions, styles)?;
let len = frames.len(); 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. // Grow our size, shrink the region and save the frame for later.
let size = frame.size; let size = frame.size;
self.used.y += size.y; self.used.y += size.y;

View File

@ -450,6 +450,7 @@ impl<'a> GridLayouter<'a> {
/// Layout a row with fixed height and return its frame. /// Layout a row with fixed height and return its frame.
fn layout_single_row(&mut self, height: Length, y: usize) -> TypResult<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 output = Frame::new(Size::new(self.used.x, height));
let mut pos = Point::zero(); let mut pos = Point::zero();
for (x, &rcol) in self.rcols.iter().enumerate() { 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 pod = Regions::one(size, base, Spec::splat(true));
let frame = node.layout(self.ctx, &pod, self.styles)?.remove(0); 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); output.push_frame(pos, frame);
} }
@ -505,6 +514,13 @@ impl<'a> GridLayouter<'a> {
// Push the layouted frames into the individual output frames. // Push the layouted frames into the individual output frames.
let frames = node.layout(self.ctx, &pod, self.styles)?; let frames = node.layout(self.ctx, &pod, self.styles)?;
for (output, frame) in outputs.iter_mut().zip(frames) { 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); output.push_frame(pos, frame);
} }
} }

View File

@ -110,16 +110,28 @@ impl PageNode {
let pad = padding.resolve(styles).relative_to(size); let pad = padding.resolve(styles).relative_to(size);
let pw = size.x - pad.left - pad.right; let pw = size.x - pad.left - pad.right;
let py = size.y - pad.bottom; let py = size.y - pad.bottom;
for (marginal, pos, area) in [ for (role, 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)), Role::Header,
(foreground, Point::zero(), size), header,
(background, Point::zero(), size), 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)? { if let Some(content) = marginal.resolve(ctx, page)? {
let pod = Regions::one(area, area, Spec::splat(true)); let pod = Regions::one(area, area, Spec::splat(true));
let sub = content.layout(ctx, &pod, styles)?.remove(0); let mut sub = content.layout(ctx, &pod, styles)?.remove(0);
if std::ptr::eq(marginal, background) { Arc::make_mut(&mut sub).apply_role(role);
if role == Role::Background {
Arc::make_mut(frame).prepend_frame(pos, sub); Arc::make_mut(frame).prepend_frame(pos, sub);
} else { } else {
Arc::make_mut(frame).push_frame(pos, sub); Arc::make_mut(frame).push_frame(pos, sub);

View File

@ -194,7 +194,12 @@ impl<'a> StackLayouter<'a> {
let frames = node.layout(ctx, &self.regions, styles)?; let frames = node.layout(ctx, &self.regions, styles)?;
let len = frames.len(); 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. // Grow our size, shrink the region and save the frame for later.
let size = frame.size.to_gen(self.axis); let size = frame.size.to_gen(self.axis);
self.used.main += size.main; self.used.main += size.main;

View File

@ -66,6 +66,7 @@ impl Layout for RexNode {
let mut backend = FrameBackend { let mut backend = FrameBackend {
frame: { frame: {
let mut frame = Frame::new(size); let mut frame = Frame::new(size);
frame.apply_role(Role::Formula);
frame.baseline = Some(baseline); frame.baseline = Some(baseline);
frame frame
}, },

View File

@ -65,7 +65,8 @@ impl HeadingNode {
impl Show for HeadingNode { impl Show for HeadingNode {
fn unguard(&self, sel: Selector) -> ShowNode { 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 { fn encode(&self, _: StyleChain) -> Dict {
@ -114,7 +115,7 @@ impl Show for HeadingNode {
realized = realized.underlined(); realized = realized.underlined();
} }
realized = realized.styled_with_map(map); realized = realized.styled_with_map(map).role(Role::Heading(self.level.get()));
realized = realized.spaced( realized = realized.spaced(
resolve!(Self::ABOVE).resolve(styles), resolve!(Self::ABOVE).resolve(styles),
resolve!(Self::BELOW).resolve(styles), resolve!(Self::BELOW).resolve(styles),

View File

@ -78,7 +78,7 @@ impl<const L: ListKind> Show for ListNode<L> {
fn unguard(&self, sel: Selector) -> ShowNode { fn unguard(&self, sel: Selector) -> ShowNode {
Self { Self {
items: self.items.map(|item| ListItem { items: self.items.map(|item| ListItem {
body: Box::new(item.body.unguard(sel)), body: Box::new(item.body.unguard(sel).role(Role::ListItemBody)),
..*item ..*item
}), }),
..*self ..*self
@ -108,9 +108,15 @@ impl<const L: ListKind> Show for ListNode<L> {
for (item, map) in self.items.iter() { for (item, map) in self.items.iter() {
number = item.number.unwrap_or(number); number = item.number.unwrap_or(number);
cells.push(LayoutNode::default()); cells.push(LayoutNode::default());
cells cells.push(
.push(label.resolve(ctx, L, number)?.styled_with_map(map.clone()).pack()); label
.resolve(ctx, L, number)?
.styled_with_map(map.clone())
.role(Role::ListLabel)
.pack(),
);
cells.push(LayoutNode::default()); cells.push(LayoutNode::default());
cells.push((*item.body).clone().styled_with_map(map.clone()).pack()); cells.push((*item.body).clone().styled_with_map(map.clone()).pack());
number += 1; 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))
} }
} }

View File

@ -52,7 +52,11 @@ impl Show for TableNode {
Self { Self {
tracks: self.tracks.clone(), tracks: self.tracks.clone(),
gutter: self.gutter.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() .pack()
} }
@ -100,7 +104,8 @@ impl Show for TableNode {
tracks: self.tracks.clone(), tracks: self.tracks.clone(),
gutter: self.gutter.clone(), gutter: self.gutter.clone(),
cells, cells,
})) })
.role(Role::Table))
} }
fn finalize( fn finalize(

View File

@ -551,11 +551,14 @@ fn prepare<'a>(
} else { } else {
let size = Size::new(regions.first.x, regions.base.y); let size = Size::new(regions.first.x, regions.base.y);
let pod = Regions::one(size, regions.base, Spec::splat(false)); let pod = Regions::one(size, regions.base, Spec::splat(false));
let mut frame = node.layout(ctx, &pod, styles)?.remove(0); let mut frame = node.layout(ctx, &pod, styles)?.remove(0);
let shift = styles.get(TextNode::BASELINE); let shift = styles.get(TextNode::BASELINE);
if !shift.is_zero() { if !shift.is_zero() || frame.role().map_or(true, Role::is_weak) {
Arc::make_mut(&mut frame).translate(Point::with_y(shift)); let frame = Arc::make_mut(&mut frame);
frame.translate(Point::with_y(shift));
frame.apply_role(Role::GenericInline);
} }
items.push(Item::Frame(frame)); items.push(Item::Frame(frame));
@ -1063,6 +1066,7 @@ fn stack(
let mut finished = vec![]; let mut finished = vec![];
let mut first = true; let mut first = true;
let mut output = Frame::new(Size::with_x(width)); let mut output = Frame::new(Size::with_x(width));
output.apply_role(Role::Paragraph);
// Stack the lines into one frame per region. // Stack the lines into one frame per region.
for line in lines { for line in lines {
@ -1072,6 +1076,7 @@ fn stack(
while !regions.first.y.fits(height) && !regions.in_last() { while !regions.first.y.fits(height) && !regions.in_last() {
finished.push(Arc::new(output)); finished.push(Arc::new(output));
output = Frame::new(Size::with_x(width)); output = Frame::new(Size::with_x(width));
output.apply_role(Role::Paragraph);
regions.next(); regions.next();
first = true; first = true;
} }

View File

@ -123,7 +123,7 @@ impl Show for RawNode {
realized = realized.spaced(styles.get(Self::ABOVE), styles.get(Self::BELOW)); 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))
} }
} }

View File

@ -204,6 +204,11 @@ impl Content {
Self::Styled(Arc::new((self, styles))) 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. /// Reenable the show rule identified by the selector.
pub fn unguard(&self, sel: Selector) -> Self { pub fn unguard(&self, sel: Selector) -> Self {
self.clone().styled_with_entry(StyleEntry::Unguard(sel)) self.clone().styled_with_entry(StyleEntry::Unguard(sel))

View File

@ -232,7 +232,16 @@ impl Layout for LayoutNode {
let at = ctx.pins.cursor(); let at = ctx.pins.cursor();
let entry = StyleEntry::Barrier(Barrier::new(node.id())); 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 fresh = ctx.pins.from(at);
let dirty = ctx.pins.dirty.get(); let dirty = ctx.pins.dirty.get();

View File

@ -5,6 +5,7 @@ use std::marker::PhantomData;
use super::{Barrier, Content, Key, Property, Recipe, Selector, Show, Target}; use super::{Barrier, Content, Key, Property, Recipe, Selector, Show, Target};
use crate::diag::TypResult; use crate::diag::TypResult;
use crate::frame::Role;
use crate::library::text::{FontFamily, TextNode}; use crate::library::text::{FontFamily, TextNode};
use crate::util::ReadableTypeId; use crate::util::ReadableTypeId;
use crate::Context; use crate::Context;
@ -170,6 +171,8 @@ pub enum StyleEntry {
Property(Property), Property(Property),
/// A show rule recipe. /// A show rule recipe.
Recipe(Recipe), Recipe(Recipe),
/// A semantic role.
Role(Role),
/// A barrier for scoped styles. /// A barrier for scoped styles.
Barrier(Barrier), Barrier(Barrier),
/// Guards against recursive show rules. /// Guards against recursive show rules.
@ -229,6 +232,7 @@ impl Debug for StyleEntry {
match self { match self {
Self::Property(property) => property.fmt(f)?, Self::Property(property) => property.fmt(f)?,
Self::Recipe(recipe) => recipe.fmt(f)?, Self::Recipe(recipe) => recipe.fmt(f)?,
Self::Role(role) => role.fmt(f)?,
Self::Barrier(barrier) => barrier.fmt(f)?, Self::Barrier(barrier) => barrier.fmt(f)?,
Self::Guard(sel) => write!(f, "Guard against {sel:?}")?, Self::Guard(sel) => write!(f, "Guard against {sel:?}")?,
Self::Unguard(sel) => write!(f, "Unguard against {sel:?}")?, Self::Unguard(sel) => write!(f, "Unguard against {sel:?}")?,
@ -324,8 +328,23 @@ impl<'a> StyleChain<'a> {
Ok(realized) 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. /// 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() { for entry in self.entries() {
match *entry { match *entry {
StyleEntry::Guard(s) if s == sel => return true, StyleEntry::Guard(s) if s == sel => return true,