mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
commit
cd5a14bc24
@ -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));
|
||||
}
|
||||
|
100
src/frame.rs
100
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
},
|
||||
|
@ -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),
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user