Write PDF outline
This commit is contained in:
parent
428c55b6ee
commit
9bdc4a7de0
@ -211,6 +211,7 @@ fn items() -> LangItems {
|
||||
},
|
||||
bibliography_keys: meta::BibliographyElem::keys,
|
||||
heading: |level, title| meta::HeadingElem::new(title).with_level(level).pack(),
|
||||
heading_func: meta::HeadingElem::func(),
|
||||
list_item: |body| layout::ListItem::new(body).pack(),
|
||||
enum_item: |number, body| {
|
||||
let mut elem = layout::EnumItem::new(body);
|
||||
|
@ -173,14 +173,14 @@ impl Synthesize for FigureElem {
|
||||
// Determine the figure's kind.
|
||||
let kind = match self.kind(styles) {
|
||||
Smart::Auto => self
|
||||
.find_figurable(vt, styles)
|
||||
.find_figurable(styles)
|
||||
.map(|elem| FigureKind::Elem(elem.func()))
|
||||
.unwrap_or_else(|| FigureKind::Elem(ImageElem::func())),
|
||||
Smart::Custom(kind) => kind,
|
||||
};
|
||||
|
||||
let content = match &kind {
|
||||
FigureKind::Elem(func) => self.find_of_elem(vt, *func),
|
||||
FigureKind::Elem(func) => self.find_of_elem(*func),
|
||||
FigureKind::Name(_) => None,
|
||||
}
|
||||
.unwrap_or_else(|| self.body());
|
||||
@ -303,9 +303,9 @@ impl Refable for FigureElem {
|
||||
impl FigureElem {
|
||||
/// Determines the type of the figure by looking at the content, finding all
|
||||
/// [`Figurable`] elements and sorting them by priority then returning the highest.
|
||||
pub fn find_figurable(&self, vt: &Vt, styles: StyleChain) -> Option<Content> {
|
||||
pub fn find_figurable(&self, styles: StyleChain) -> Option<Content> {
|
||||
self.body()
|
||||
.query(vt.introspector, Selector::can::<dyn Figurable>())
|
||||
.query(Selector::can::<dyn Figurable>())
|
||||
.into_iter()
|
||||
.max_by_key(|elem| elem.with::<dyn Figurable>().unwrap().priority(styles))
|
||||
.cloned()
|
||||
@ -313,9 +313,9 @@ impl FigureElem {
|
||||
|
||||
/// Finds the element with the given function in the figure's content.
|
||||
/// Returns `None` if no element with the given function is found.
|
||||
pub fn find_of_elem(&self, vt: &Vt, func: ElemFunc) -> Option<Content> {
|
||||
pub fn find_of_elem(&self, func: ElemFunc) -> Option<Content> {
|
||||
self.body()
|
||||
.query(vt.introspector, Selector::Elem(func, None))
|
||||
.query(Selector::Elem(func, None))
|
||||
.into_iter()
|
||||
.next()
|
||||
.cloned()
|
||||
|
@ -23,9 +23,9 @@ pub use typst::geom::*;
|
||||
#[doc(no_inline)]
|
||||
pub use typst::model::{
|
||||
element, Behave, Behaviour, Construct, Content, ElemFunc, Element, Finalize, Fold,
|
||||
Introspector, Label, Locatable, LocatableSelector, Location, MetaElem, Resolve,
|
||||
Selector, Set, Show, StabilityProvider, StyleChain, StyleVec, Styles, Synthesize,
|
||||
Unlabellable, Vt,
|
||||
Introspector, Label, Locatable, LocatableSelector, Location, MetaElem, PlainText,
|
||||
Resolve, Selector, Set, Show, StabilityProvider, StyleChain, StyleVec, Styles,
|
||||
Synthesize, Unlabellable, Vt,
|
||||
};
|
||||
#[doc(no_inline)]
|
||||
pub use typst::syntax::{Span, Spanned};
|
||||
|
@ -5,7 +5,7 @@ use crate::prelude::*;
|
||||
///
|
||||
/// Display: Space
|
||||
/// Category: text
|
||||
#[element(Unlabellable, Behave)]
|
||||
#[element(Behave, Unlabellable, PlainText)]
|
||||
pub struct SpaceElem {}
|
||||
|
||||
impl Behave for SpaceElem {
|
||||
@ -16,6 +16,12 @@ impl Behave for SpaceElem {
|
||||
|
||||
impl Unlabellable for SpaceElem {}
|
||||
|
||||
impl PlainText for SpaceElem {
|
||||
fn plain_text(&self, text: &mut EcoString) {
|
||||
text.push(' ');
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts a line break.
|
||||
///
|
||||
/// Advances the paragraph to the next line. A single trailing line break at the
|
||||
|
@ -40,7 +40,7 @@ use crate::prelude::*;
|
||||
///
|
||||
/// Display: Text
|
||||
/// Category: text
|
||||
#[element(Construct)]
|
||||
#[element(Construct, PlainText)]
|
||||
pub struct TextElem {
|
||||
/// A prioritized sequence of font families.
|
||||
///
|
||||
@ -497,6 +497,12 @@ impl Construct for TextElem {
|
||||
}
|
||||
}
|
||||
|
||||
impl PlainText for TextElem {
|
||||
fn plain_text(&self, text: &mut EcoString) {
|
||||
text.push_str(&self.text());
|
||||
}
|
||||
}
|
||||
|
||||
/// A lowercased font family like "arial".
|
||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||
pub struct FontFamily(EcoString);
|
||||
|
@ -72,6 +72,8 @@ pub struct LangItems {
|
||||
) -> Vec<(EcoString, Option<EcoString>)>,
|
||||
/// A section heading: `= Introduction`.
|
||||
pub heading: fn(level: NonZeroUsize, body: Content) -> Content,
|
||||
/// The heading function.
|
||||
pub heading_func: ElemFunc,
|
||||
/// An item in a bullet list: `- ...`.
|
||||
pub list_item: fn(body: Content) -> Content,
|
||||
/// An item in an enumeration (numbered list): `+ ...` or `1. ...`.
|
||||
|
@ -13,7 +13,6 @@ use pdf_writer::types::Direction;
|
||||
use pdf_writer::{Finish, Name, PdfWriter, Ref, TextStr};
|
||||
use xmp_writer::{LangId, RenditionClass, XmpWriter};
|
||||
|
||||
use self::outline::HeadingNode;
|
||||
use self::page::Page;
|
||||
use crate::doc::{Document, Lang};
|
||||
use crate::font::Font;
|
||||
@ -54,7 +53,6 @@ pub struct PdfContext<'a> {
|
||||
image_map: Remapper<Image>,
|
||||
glyph_sets: HashMap<Font, HashSet<u16>>,
|
||||
languages: HashMap<Lang, usize>,
|
||||
heading_tree: Vec<HeadingNode>,
|
||||
}
|
||||
|
||||
impl<'a> PdfContext<'a> {
|
||||
@ -76,36 +74,12 @@ impl<'a> PdfContext<'a> {
|
||||
image_map: Remapper::new(),
|
||||
glyph_sets: HashMap::new(),
|
||||
languages: HashMap::new(),
|
||||
heading_tree: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write the document catalog.
|
||||
fn write_catalog(ctx: &mut PdfContext) {
|
||||
// Build the outline tree.
|
||||
let outline_root_id = (!ctx.heading_tree.is_empty()).then(|| ctx.alloc.bump());
|
||||
let outline_start_ref = ctx.alloc;
|
||||
let len = ctx.heading_tree.len();
|
||||
let mut prev_ref = None;
|
||||
|
||||
for (i, node) in std::mem::take(&mut ctx.heading_tree).iter().enumerate() {
|
||||
prev_ref = Some(outline::write_outline_item(
|
||||
ctx,
|
||||
node,
|
||||
outline_root_id.unwrap(),
|
||||
prev_ref,
|
||||
i + 1 == len,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(outline_root_id) = outline_root_id {
|
||||
let mut outline_root = ctx.writer.outline(outline_root_id);
|
||||
outline_root.first(outline_start_ref);
|
||||
outline_root.last(Ref::new(ctx.alloc.get() - 1));
|
||||
outline_root.count(ctx.heading_tree.len() as i32);
|
||||
}
|
||||
|
||||
let lang = ctx
|
||||
.languages
|
||||
.iter()
|
||||
@ -118,6 +92,9 @@ fn write_catalog(ctx: &mut PdfContext) {
|
||||
Direction::L2R
|
||||
};
|
||||
|
||||
// Write the outline tree.
|
||||
let outline_root_id = outline::write_outline(ctx);
|
||||
|
||||
// Write the document information.
|
||||
let mut info = ctx.writer.document_info(ctx.alloc.bump());
|
||||
let mut xmp = XmpWriter::new();
|
||||
|
@ -1,32 +1,76 @@
|
||||
use ecow::EcoString;
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use pdf_writer::{Finish, Ref, TextStr};
|
||||
|
||||
use super::{AbsExt, PdfContext, RefExt};
|
||||
use crate::geom::{Abs, Point};
|
||||
use crate::geom::Abs;
|
||||
use crate::model::Content;
|
||||
use crate::util::NonZeroExt;
|
||||
|
||||
/// Construct the outline for the document.
|
||||
pub fn write_outline(ctx: &mut PdfContext) -> Option<Ref> {
|
||||
let mut tree: Vec<HeadingNode> = vec![];
|
||||
for heading in ctx.introspector.query(&item!(heading_func).select()) {
|
||||
let leaf = HeadingNode::leaf(heading);
|
||||
if let Some(last) = tree.last_mut() {
|
||||
if last.try_insert(leaf.clone(), NonZeroUsize::ONE) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
tree.push(leaf);
|
||||
}
|
||||
|
||||
if tree.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let root_id = ctx.alloc.bump();
|
||||
let start_ref = ctx.alloc;
|
||||
let len = tree.len();
|
||||
|
||||
let mut prev_ref = None;
|
||||
for (i, node) in tree.iter().enumerate() {
|
||||
prev_ref = Some(write_outline_item(ctx, node, root_id, prev_ref, i + 1 == len));
|
||||
}
|
||||
|
||||
ctx.writer
|
||||
.outline(root_id)
|
||||
.first(start_ref)
|
||||
.last(Ref::new(ctx.alloc.get() - 1))
|
||||
.count(tree.len() as i32);
|
||||
|
||||
Some(root_id)
|
||||
}
|
||||
|
||||
/// A heading in the outline panel.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HeadingNode {
|
||||
pub content: EcoString,
|
||||
pub level: usize,
|
||||
pub position: Point,
|
||||
pub page: Ref,
|
||||
pub children: Vec<HeadingNode>,
|
||||
struct HeadingNode {
|
||||
element: Content,
|
||||
level: NonZeroUsize,
|
||||
children: Vec<HeadingNode>,
|
||||
}
|
||||
|
||||
impl HeadingNode {
|
||||
pub fn len(&self) -> usize {
|
||||
fn leaf(element: Content) -> Self {
|
||||
HeadingNode {
|
||||
level: element.expect_field::<NonZeroUsize>("level"),
|
||||
element,
|
||||
children: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
1 + self.children.iter().map(Self::len).sum::<usize>()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn try_insert(&mut self, child: Self, level: usize) -> bool {
|
||||
fn try_insert(&mut self, child: Self, level: NonZeroUsize) -> bool {
|
||||
if level >= child.level {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(last) = self.children.last_mut() {
|
||||
if last.try_insert(child.clone(), level + 1) {
|
||||
if last.try_insert(child.clone(), level.saturating_add(1)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -37,7 +81,7 @@ impl HeadingNode {
|
||||
}
|
||||
|
||||
/// Write an outline item and all its children.
|
||||
pub fn write_outline_item(
|
||||
fn write_outline_item(
|
||||
ctx: &mut PdfContext,
|
||||
node: &HeadingNode,
|
||||
parent_ref: Ref,
|
||||
@ -65,12 +109,19 @@ pub fn write_outline_item(
|
||||
outline.count(-(node.children.len() as i32));
|
||||
}
|
||||
|
||||
outline.title(TextStr(&node.content));
|
||||
outline.dest_direct().page(node.page).xyz(
|
||||
node.position.x.to_f32(),
|
||||
(node.position.y + Abs::pt(3.0)).to_f32(),
|
||||
None,
|
||||
);
|
||||
outline.title(TextStr(node.element.plain_text().trim()));
|
||||
|
||||
let loc = node.element.location().unwrap();
|
||||
let pos = ctx.introspector.position(loc);
|
||||
let index = pos.page.get() - 1;
|
||||
if let Some(&height) = ctx.page_heights.get(index) {
|
||||
let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero());
|
||||
outline.dest_direct().page(ctx.page_refs[index]).xyz(
|
||||
pos.point.x.to_f32(),
|
||||
height - y.to_f32(),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
outline.finish();
|
||||
|
||||
|
@ -96,16 +96,16 @@ pub fn analyze_labels(
|
||||
|
||||
// Labels in the document.
|
||||
for elem in introspector.all() {
|
||||
let Some(label) = elem.label() else { continue };
|
||||
let Some(label) = elem.label().cloned() else { continue };
|
||||
let details = elem
|
||||
.field("caption")
|
||||
.or_else(|| elem.field("body"))
|
||||
.and_then(|field| match field {
|
||||
Value::Content(content) => Some(content),
|
||||
_ => None,
|
||||
})
|
||||
.and_then(|content| (items.text_str)(&content));
|
||||
output.push((label.clone(), details));
|
||||
.unwrap_or(elem)
|
||||
.plain_text();
|
||||
output.push((label, Some(details)));
|
||||
}
|
||||
|
||||
let split = output.len();
|
||||
|
@ -3,12 +3,12 @@ use std::fmt::{self, Debug, Formatter, Write};
|
||||
use std::iter::Sum;
|
||||
use std::ops::{Add, AddAssign};
|
||||
|
||||
use comemo::{Prehashed, Tracked};
|
||||
use comemo::Prehashed;
|
||||
use ecow::{eco_format, EcoString, EcoVec};
|
||||
|
||||
use super::{
|
||||
element, Behave, Behaviour, ElemFunc, Element, Fold, Guard, Introspector, Label,
|
||||
Locatable, Location, Recipe, Selector, Style, Styles, Synthesize,
|
||||
element, Behave, Behaviour, ElemFunc, Element, Fold, Guard, Label, Locatable,
|
||||
Location, PlainText, Recipe, Selector, Style, Styles, Synthesize,
|
||||
};
|
||||
use crate::diag::{SourceResult, StrResult};
|
||||
use crate::doc::Meta;
|
||||
@ -359,52 +359,53 @@ impl Content {
|
||||
|
||||
/// Queries the content tree for all elements that match the given selector.
|
||||
///
|
||||
/// # Show rules
|
||||
/// Elements produced in `show` rules will not be included in the results.
|
||||
pub fn query(
|
||||
&self,
|
||||
introspector: Tracked<Introspector>,
|
||||
selector: Selector,
|
||||
) -> Vec<&Content> {
|
||||
pub fn query(&self, selector: Selector) -> Vec<&Content> {
|
||||
let mut results = Vec::new();
|
||||
self.query_into(introspector, &selector, &mut results);
|
||||
self.traverse(&mut |element| {
|
||||
if selector.matches(element) {
|
||||
results.push(element);
|
||||
}
|
||||
});
|
||||
results
|
||||
}
|
||||
|
||||
/// Queries the content tree for all elements that match the given selector
|
||||
/// and stores the results inside of the `results` vec.
|
||||
fn query_into<'a>(
|
||||
&'a self,
|
||||
introspector: Tracked<Introspector>,
|
||||
selector: &Selector,
|
||||
results: &mut Vec<&'a Content>,
|
||||
) {
|
||||
if selector.matches(self) {
|
||||
results.push(self);
|
||||
}
|
||||
/// Extracts the plain text of this content.
|
||||
pub fn plain_text(&self) -> EcoString {
|
||||
let mut text = EcoString::new();
|
||||
self.traverse(&mut |element| {
|
||||
if let Some(textable) = element.with::<dyn PlainText>() {
|
||||
textable.plain_text(&mut text);
|
||||
}
|
||||
});
|
||||
text
|
||||
}
|
||||
|
||||
/// Traverse this content.
|
||||
fn traverse<'a, F>(&'a self, f: &mut F)
|
||||
where
|
||||
F: FnMut(&'a Content),
|
||||
{
|
||||
f(self);
|
||||
|
||||
for attr in &self.attrs {
|
||||
match attr {
|
||||
Attr::Child(child) => child.query_into(introspector, selector, results),
|
||||
Attr::Value(value) => walk_value(introspector, value, selector, results),
|
||||
Attr::Child(child) => child.traverse(f),
|
||||
Attr::Value(value) => walk_value(value, f),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Walks a given value to find any content that matches the selector.
|
||||
fn walk_value<'a>(
|
||||
introspector: Tracked<Introspector>,
|
||||
value: &'a Value,
|
||||
selector: &Selector,
|
||||
results: &mut Vec<&'a Content>,
|
||||
) {
|
||||
fn walk_value<'a, F>(value: &'a Value, f: &mut F)
|
||||
where
|
||||
F: FnMut(&'a Content),
|
||||
{
|
||||
match value {
|
||||
Value::Content(content) => {
|
||||
content.query_into(introspector, selector, results)
|
||||
}
|
||||
Value::Content(content) => content.traverse(f),
|
||||
Value::Array(array) => {
|
||||
for value in array {
|
||||
walk_value(introspector, value, selector, results);
|
||||
walk_value(value, f);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
@ -151,3 +151,9 @@ impl Debug for Label {
|
||||
|
||||
/// Indicates that an element cannot be labelled.
|
||||
pub trait Unlabellable {}
|
||||
|
||||
/// Tries to extract the plain-text representation of the element.
|
||||
pub trait PlainText {
|
||||
/// Write this element's plain text into the given buffer.
|
||||
fn plain_text(&self, text: &mut EcoString);
|
||||
}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
@ -32,5 +32,5 @@ Ok ...
|
||||
|
||||
#set heading(numbering: "(I)")
|
||||
|
||||
= Zusammenfassung
|
||||
= #text(blue)[Zusammen]fassung
|
||||
#lorem(10)
|
||||
|
Loading…
x
Reference in New Issue
Block a user