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 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();
fn write_page(&mut self, page: Page) {
let content_id = 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 page_writer = self.writer.page(page.id);
page_writer.parent(self.page_tree_ref);
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 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 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);
}
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();
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);
pages.count(page_refs.len() as i32).kids(page_refs);
annotations.finish();
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 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));
}

View File

@ -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,
}
}
}

View File

@ -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)

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);
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) {

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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
},

View File

@ -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),

View File

@ -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))
}
}

View File

@ -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(

View File

@ -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;
}

View File

@ -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))
}
}

View File

@ -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))

View File

@ -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();

View File

@ -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,