summaryrefslogtreecommitdiff
path: root/library/src/shared
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-11-26 12:49:30 +0100
committerLaurenz <laurmaedje@gmail.com>2022-11-26 12:51:15 +0100
commit3cdd8bfa40fe5fdf0c676af905c3c2c1f614ef24 (patch)
treefe837d4a30bcad673f581fab18fd946364a60ef3 /library/src/shared
parentbf5edbbbbb75120d065d1c9587ccfa4eed4fdca1 (diff)
Extract numbering pattern from list node
Diffstat (limited to 'library/src/shared')
-rw-r--r--library/src/shared/behave.rs128
-rw-r--r--library/src/shared/ext.rs147
-rw-r--r--library/src/shared/mod.rs9
-rw-r--r--library/src/shared/numbering.rs139
4 files changed, 423 insertions, 0 deletions
diff --git a/library/src/shared/behave.rs b/library/src/shared/behave.rs
new file mode 100644
index 00000000..ec8fade9
--- /dev/null
+++ b/library/src/shared/behave.rs
@@ -0,0 +1,128 @@
+//! Node interaction.
+
+use typst::model::{capability, Content, StyleChain, StyleVec, StyleVecBuilder};
+
+/// How a node interacts with other nodes.
+#[capability]
+pub trait Behave {
+ /// The node's interaction behaviour.
+ fn behaviour(&self) -> Behaviour;
+
+ /// Whether this weak node is larger than a previous one and thus picked as
+ /// the maximum when the levels are the same.
+ #[allow(unused_variables)]
+ fn larger(&self, prev: &Content) -> bool {
+ false
+ }
+}
+
+/// How a node interacts with other nodes in a stream.
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+pub enum Behaviour {
+ /// A weak node which only survives when a supportive node is before and
+ /// after it. Furthermore, per consecutive run of weak nodes, only one
+ /// survives: The one with the lowest weakness level (or the larger one if
+ /// there is a tie).
+ Weak(u8),
+ /// A node that enables adjacent weak nodes to exist. The default.
+ Supportive,
+ /// A node that destroys adjacent weak nodes.
+ Destructive,
+ /// A node that does not interact at all with other nodes, having the
+ /// same effect as if it didn't exist.
+ Ignorant,
+}
+
+/// A wrapper around a [`StyleVecBuilder`] that allows items to interact.
+pub struct BehavedBuilder<'a> {
+ /// The internal builder.
+ builder: StyleVecBuilder<'a, Content>,
+ /// Staged weak and ignorant items that we can't yet commit to the builder.
+ /// The option is `Some(_)` for weak items and `None` for ignorant items.
+ staged: Vec<(Content, Behaviour, StyleChain<'a>)>,
+ /// What the last non-ignorant item was.
+ last: Behaviour,
+}
+
+impl<'a> BehavedBuilder<'a> {
+ /// Create a new style-vec builder.
+ pub fn new() -> Self {
+ Self {
+ builder: StyleVecBuilder::new(),
+ staged: vec![],
+ last: Behaviour::Destructive,
+ }
+ }
+
+ /// Whether the builder is empty.
+ pub fn is_empty(&self) -> bool {
+ self.builder.is_empty() && self.staged.is_empty()
+ }
+
+ /// Push an item into the sequence.
+ pub fn push(&mut self, item: Content, styles: StyleChain<'a>) {
+ let interaction = item
+ .with::<dyn Behave>()
+ .map_or(Behaviour::Supportive, Behave::behaviour);
+
+ match interaction {
+ Behaviour::Weak(level) => {
+ if matches!(self.last, Behaviour::Weak(_)) {
+ let item = item.with::<dyn Behave>().unwrap();
+ let i = self.staged.iter().position(|prev| {
+ let Behaviour::Weak(prev_level) = prev.1 else { return false };
+ level < prev_level
+ || (level == prev_level && item.larger(&prev.0))
+ });
+ let Some(i) = i else { return };
+ self.staged.remove(i);
+ }
+
+ if self.last != Behaviour::Destructive {
+ self.staged.push((item, interaction, styles));
+ self.last = interaction;
+ }
+ }
+ Behaviour::Supportive => {
+ self.flush(true);
+ self.builder.push(item, styles);
+ self.last = interaction;
+ }
+ Behaviour::Destructive => {
+ self.flush(false);
+ self.builder.push(item, styles);
+ self.last = interaction;
+ }
+ Behaviour::Ignorant => {
+ self.staged.push((item, interaction, styles));
+ }
+ }
+ }
+
+ /// Iterate over the contained items.
+ pub fn items(&self) -> impl DoubleEndedIterator<Item = &Content> {
+ self.builder.items().chain(self.staged.iter().map(|(item, ..)| item))
+ }
+
+ /// Return the finish style vec and the common prefix chain.
+ pub fn finish(mut self) -> (StyleVec<Content>, StyleChain<'a>) {
+ self.flush(false);
+ self.builder.finish()
+ }
+
+ /// Push the staged items, filtering out weak items if `supportive` is
+ /// false.
+ fn flush(&mut self, supportive: bool) {
+ for (item, interaction, styles) in self.staged.drain(..) {
+ if supportive || interaction == Behaviour::Ignorant {
+ self.builder.push(item, styles);
+ }
+ }
+ }
+}
+
+impl<'a> Default for BehavedBuilder<'a> {
+ fn default() -> Self {
+ Self::new()
+ }
+}
diff --git a/library/src/shared/ext.rs b/library/src/shared/ext.rs
new file mode 100644
index 00000000..f90260ad
--- /dev/null
+++ b/library/src/shared/ext.rs
@@ -0,0 +1,147 @@
+//! Extension traits.
+
+use crate::prelude::*;
+
+/// Additional methods on content.
+pub trait ContentExt {
+ /// Make this content strong.
+ fn strong(self) -> Self;
+
+ /// Make this content emphasized.
+ fn emph(self) -> Self;
+
+ /// Underline this content.
+ fn underlined(self) -> Self;
+
+ /// Force a size for this content.
+ fn boxed(self, sizing: Axes<Option<Rel<Length>>>) -> Self;
+
+ /// Set alignments for this content.
+ fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self;
+
+ /// Pad this content at the sides.
+ fn padded(self, padding: Sides<Rel<Length>>) -> Self;
+
+ /// Transform this content's contents without affecting layout.
+ fn moved(self, delta: Axes<Rel<Length>>) -> Self;
+
+ /// Fill the frames resulting from a content.
+ fn filled(self, fill: Paint) -> Self;
+
+ /// Stroke the frames resulting from a content.
+ fn stroked(self, stroke: Stroke) -> Self;
+}
+
+impl ContentExt for Content {
+ fn strong(self) -> Self {
+ crate::text::StrongNode(self).pack()
+ }
+
+ fn emph(self) -> Self {
+ crate::text::EmphNode(self).pack()
+ }
+
+ fn underlined(self) -> Self {
+ crate::text::DecoNode::<{ crate::text::UNDERLINE }>(self).pack()
+ }
+
+ fn boxed(self, sizing: Axes<Option<Rel<Length>>>) -> Self {
+ crate::layout::BoxNode { sizing, child: self }.pack()
+ }
+
+ fn aligned(self, aligns: Axes<Option<GenAlign>>) -> Self {
+ crate::layout::AlignNode { aligns, child: self }.pack()
+ }
+
+ fn padded(self, padding: Sides<Rel<Length>>) -> Self {
+ crate::layout::PadNode { padding, child: self }.pack()
+ }
+
+ fn moved(self, delta: Axes<Rel<Length>>) -> Self {
+ crate::layout::MoveNode { delta, child: self }.pack()
+ }
+
+ fn filled(self, fill: Paint) -> Self {
+ FillNode { fill, child: self }.pack()
+ }
+
+ fn stroked(self, stroke: Stroke) -> Self {
+ StrokeNode { stroke, child: self }.pack()
+ }
+}
+
+/// Additional methods for style maps.
+pub trait StyleMapExt {
+ /// Set a font family composed of a preferred family and existing families
+ /// from a style chain.
+ fn set_family(&mut self, preferred: crate::text::FontFamily, existing: StyleChain);
+}
+
+impl StyleMapExt for StyleMap {
+ fn set_family(&mut self, preferred: crate::text::FontFamily, existing: StyleChain) {
+ self.set(
+ crate::text::TextNode::FAMILY,
+ crate::text::FallbackList(
+ std::iter::once(preferred)
+ .chain(existing.get(crate::text::TextNode::FAMILY).0.iter().cloned())
+ .collect(),
+ ),
+ );
+ }
+}
+
+/// Fill the frames resulting from content.
+#[derive(Debug, Hash)]
+struct FillNode {
+ /// How to fill the frames resulting from the `child`.
+ fill: Paint,
+ /// The content whose frames should be filled.
+ child: Content,
+}
+
+#[node(LayoutBlock)]
+impl FillNode {}
+
+impl LayoutBlock for FillNode {
+ fn layout_block(
+ &self,
+ world: Tracked<dyn World>,
+ styles: StyleChain,
+ regions: &Regions,
+ ) -> SourceResult<Vec<Frame>> {
+ let mut frames = self.child.layout_block(world, styles, regions)?;
+ for frame in &mut frames {
+ let shape = Geometry::Rect(frame.size()).filled(self.fill);
+ frame.prepend(Point::zero(), Element::Shape(shape));
+ }
+ Ok(frames)
+ }
+}
+
+/// Stroke the frames resulting from content.
+#[derive(Debug, Hash)]
+struct StrokeNode {
+ /// How to stroke the frames resulting from the `child`.
+ stroke: Stroke,
+ /// The content whose frames should be stroked.
+ child: Content,
+}
+
+#[node(LayoutBlock)]
+impl StrokeNode {}
+
+impl LayoutBlock for StrokeNode {
+ fn layout_block(
+ &self,
+ world: Tracked<dyn World>,
+ styles: StyleChain,
+ regions: &Regions,
+ ) -> SourceResult<Vec<Frame>> {
+ let mut frames = self.child.layout_block(world, styles, regions)?;
+ for frame in &mut frames {
+ let shape = Geometry::Rect(frame.size()).stroked(self.stroke);
+ frame.prepend(Point::zero(), Element::Shape(shape));
+ }
+ Ok(frames)
+ }
+}
diff --git a/library/src/shared/mod.rs b/library/src/shared/mod.rs
new file mode 100644
index 00000000..55522190
--- /dev/null
+++ b/library/src/shared/mod.rs
@@ -0,0 +1,9 @@
+//! Shared definitions for the standard library.
+
+mod behave;
+mod ext;
+mod numbering;
+
+pub use behave::*;
+pub use ext::*;
+pub use numbering::*;
diff --git a/library/src/shared/numbering.rs b/library/src/shared/numbering.rs
new file mode 100644
index 00000000..739edafe
--- /dev/null
+++ b/library/src/shared/numbering.rs
@@ -0,0 +1,139 @@
+use std::str::FromStr;
+
+use typst::model::{castable, Value};
+use typst::util::{format_eco, EcoString};
+use unscanny::Scanner;
+
+/// A numbering pattern for lists or headings.
+#[derive(Debug, Clone, Eq, PartialEq, Hash)]
+pub struct NumberingPattern {
+ prefix: EcoString,
+ numbering: NumberingKind,
+ upper: bool,
+ suffix: EcoString,
+}
+
+impl NumberingPattern {
+ /// Apply the pattern to the given number.
+ pub fn apply(&self, n: usize) -> EcoString {
+ let fmt = self.numbering.apply(n);
+ let mid = if self.upper { fmt.to_uppercase() } else { fmt.to_lowercase() };
+ format_eco!("{}{}{}", self.prefix, mid, self.suffix)
+ }
+}
+
+impl FromStr for NumberingPattern {
+ type Err = &'static str;
+
+ fn from_str(pattern: &str) -> Result<Self, Self::Err> {
+ let mut s = Scanner::new(pattern);
+ let mut prefix;
+ let numbering = loop {
+ prefix = s.before();
+ match s.eat().map(|c| c.to_ascii_lowercase()) {
+ Some('1') => break NumberingKind::Arabic,
+ Some('a') => break NumberingKind::Letter,
+ Some('i') => break NumberingKind::Roman,
+ Some('*') => break NumberingKind::Symbol,
+ Some(_) => {}
+ None => Err("invalid numbering pattern")?,
+ }
+ };
+ let upper = s.scout(-1).map_or(false, char::is_uppercase);
+ let suffix = s.after().into();
+ Ok(Self { prefix: prefix.into(), numbering, upper, suffix })
+ }
+}
+
+castable! {
+ NumberingPattern,
+ Expected: "numbering pattern",
+ Value::Str(s) => s.parse()?,
+}
+
+/// Different kinds of numberings.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum NumberingKind {
+ Arabic,
+ Letter,
+ Roman,
+ Symbol,
+}
+
+impl NumberingKind {
+ /// Apply the numbering to the given number.
+ pub fn apply(self, mut n: usize) -> EcoString {
+ match self {
+ Self::Arabic => {
+ format_eco!("{n}")
+ }
+ Self::Letter => {
+ if n == 0 {
+ return '-'.into();
+ }
+
+ n -= 1;
+
+ let mut letters = vec![];
+ loop {
+ letters.push(b'a' + (n % 26) as u8);
+ n /= 26;
+ if n == 0 {
+ break;
+ }
+ }
+
+ letters.reverse();
+ String::from_utf8(letters).unwrap().into()
+ }
+ Self::Roman => {
+ if n == 0 {
+ return 'N'.into();
+ }
+
+ // Adapted from Yann Villessuzanne's roman.rs under the
+ // Unlicense, at https://github.com/linfir/roman.rs/
+ let mut fmt = EcoString::new();
+ for &(name, value) in &[
+ ("M̅", 1000000),
+ ("D̅", 500000),
+ ("C̅", 100000),
+ ("L̅", 50000),
+ ("X̅", 10000),
+ ("V̅", 5000),
+ ("I̅V̅", 4000),
+ ("M", 1000),
+ ("CM", 900),
+ ("D", 500),
+ ("CD", 400),
+ ("C", 100),
+ ("XC", 90),
+ ("L", 50),
+ ("XL", 40),
+ ("X", 10),
+ ("IX", 9),
+ ("V", 5),
+ ("IV", 4),
+ ("I", 1),
+ ] {
+ while n >= value {
+ n -= value;
+ fmt.push_str(name);
+ }
+ }
+
+ fmt
+ }
+ Self::Symbol => {
+ if n == 0 {
+ return '-'.into();
+ }
+
+ const SYMBOLS: &[char] = &['*', '†', '‡', '§', '‖', '¶'];
+ let symbol = SYMBOLS[(n - 1) % SYMBOLS.len()];
+ let amount = ((n - 1) / SYMBOLS.len()) + 1;
+ std::iter::repeat(symbol).take(amount).collect()
+ }
+ }
+ }
+}