summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--library/src/lib.rs1
-rw-r--r--library/src/meta/figure.rs12
-rw-r--r--library/src/prelude.rs6
-rw-r--r--library/src/text/misc.rs8
-rw-r--r--library/src/text/mod.rs8
-rw-r--r--src/eval/library.rs2
-rw-r--r--src/export/pdf/mod.rs29
-rw-r--r--src/export/pdf/outline.rs89
-rw-r--r--src/ide/analyze.rs8
-rw-r--r--src/model/content.rs67
-rw-r--r--src/model/element.rs6
-rw-r--r--tests/ref/meta/outline.pngbin80513 -> 81300 bytes
-rw-r--r--tests/typ/meta/outline.typ2
13 files changed, 144 insertions, 94 deletions
diff --git a/library/src/lib.rs b/library/src/lib.rs
index caf76ded..ccdf448c 100644
--- a/library/src/lib.rs
+++ b/library/src/lib.rs
@@ -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);
diff --git a/library/src/meta/figure.rs b/library/src/meta/figure.rs
index 42b32c9d..1f1499fc 100644
--- a/library/src/meta/figure.rs
+++ b/library/src/meta/figure.rs
@@ -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()
diff --git a/library/src/prelude.rs b/library/src/prelude.rs
index fc71e2c2..cb21a732 100644
--- a/library/src/prelude.rs
+++ b/library/src/prelude.rs
@@ -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};
diff --git a/library/src/text/misc.rs b/library/src/text/misc.rs
index e1d9c0f2..a707d130 100644
--- a/library/src/text/misc.rs
+++ b/library/src/text/misc.rs
@@ -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
diff --git a/library/src/text/mod.rs b/library/src/text/mod.rs
index 16268aad..f4b3c0de 100644
--- a/library/src/text/mod.rs
+++ b/library/src/text/mod.rs
@@ -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);
diff --git a/src/eval/library.rs b/src/eval/library.rs
index b93aa0dd..0c635864 100644
--- a/src/eval/library.rs
+++ b/src/eval/library.rs
@@ -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. ...`.
diff --git a/src/export/pdf/mod.rs b/src/export/pdf/mod.rs
index bdbb2bb7..8fc76471 100644
--- a/src/export/pdf/mod.rs
+++ b/src/export/pdf/mod.rs
@@ -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();
diff --git a/src/export/pdf/outline.rs b/src/export/pdf/outline.rs
index 1b335474..03ca3b27 100644
--- a/src/export/pdf/outline.rs
+++ b/src/export/pdf/outline.rs
@@ -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();
diff --git a/src/ide/analyze.rs b/src/ide/analyze.rs
index 27c6c2a4..62816cbc 100644
--- a/src/ide/analyze.rs
+++ b/src/ide/analyze.rs
@@ -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();
diff --git a/src/model/content.rs b/src/model/content.rs
index 3f02369f..4300790c 100644
--- a/src/model/content.rs
+++ b/src/model/content.rs
@@ -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);
}
}
_ => {}
diff --git a/src/model/element.rs b/src/model/element.rs
index 4c825a20..e26848b1 100644
--- a/src/model/element.rs
+++ b/src/model/element.rs
@@ -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);
+}
diff --git a/tests/ref/meta/outline.png b/tests/ref/meta/outline.png
index 5d05b015..c485ca9e 100644
--- a/tests/ref/meta/outline.png
+++ b/tests/ref/meta/outline.png
Binary files differ
diff --git a/tests/typ/meta/outline.typ b/tests/typ/meta/outline.typ
index 83b2e906..9f45b2f3 100644
--- a/tests/typ/meta/outline.typ
+++ b/tests/typ/meta/outline.typ
@@ -32,5 +32,5 @@ Ok ...
#set heading(numbering: "(I)")
-= Zusammenfassung
+= #text(blue)[Zusammen]fassung
#lorem(10)