summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/eval/template.rs23
-rw-r--r--src/library/heading.rs12
-rw-r--r--src/library/list.rs126
-rw-r--r--src/library/mod.rs3
-rw-r--r--src/library/numbering.rs108
-rw-r--r--src/library/utility.rs65
-rw-r--r--src/parse/scanner.rs6
-rw-r--r--src/parse/tokens.rs2
-rw-r--r--src/util/eco_string.rs10
9 files changed, 251 insertions, 104 deletions
diff --git a/src/eval/template.rs b/src/eval/template.rs
index 0db63535..68974452 100644
--- a/src/eval/template.rs
+++ b/src/eval/template.rs
@@ -13,7 +13,7 @@ use crate::diag::StrResult;
use crate::layout::{Layout, LayoutNode};
use crate::library::prelude::*;
use crate::library::{
- DecoNode, FlowChild, FlowNode, Labelling, ListItem, ListNode, PageNode, ParChild,
+ DecoNode, FlowChild, FlowNode, ListItem, ListKind, ListNode, PageNode, ParChild,
ParNode, PlaceNode, SpacingKind, TextNode, ORDERED, UNDERLINE, UNORDERED,
};
use crate::util::EcoString;
@@ -307,14 +307,14 @@ impl<'a> Builder<'a> {
builder.staged.push((template, styles));
return Ok(());
}
- Template::List(item) if builder.labelling == UNORDERED => {
+ Template::List(item) if builder.kind == UNORDERED => {
builder.wide |=
builder.staged.iter().any(|&(t, _)| *t == Template::Parbreak);
builder.staged.clear();
builder.items.push(item.clone());
return Ok(());
}
- Template::Enum(item) if builder.labelling == ORDERED => {
+ Template::Enum(item) if builder.kind == ORDERED => {
builder.wide |=
builder.staged.iter().any(|&(t, _)| *t == Template::Parbreak);
builder.staged.clear();
@@ -376,7 +376,7 @@ impl<'a> Builder<'a> {
Template::List(item) => {
self.list = Some(ListBuilder {
styles,
- labelling: UNORDERED,
+ kind: UNORDERED,
items: vec![item.clone()],
wide: false,
staged: vec![],
@@ -385,7 +385,7 @@ impl<'a> Builder<'a> {
Template::Enum(item) => {
self.list = Some(ListBuilder {
styles,
- labelling: ORDERED,
+ kind: ORDERED,
items: vec![item.clone()],
wide: false,
staged: vec![],
@@ -447,13 +447,12 @@ impl<'a> Builder<'a> {
/// Finish the currently built list.
fn finish_list(&mut self, vm: &mut Vm) -> TypResult<()> {
- let ListBuilder { styles, labelling, items, wide, staged } =
- match self.list.take() {
- Some(list) => list,
- None => return Ok(()),
- };
+ let ListBuilder { styles, kind, items, wide, staged } = match self.list.take() {
+ Some(list) => list,
+ None => return Ok(()),
+ };
- let template = match labelling {
+ let template = match kind {
UNORDERED => Template::show(ListNode::<UNORDERED> { items, wide, start: 1 }),
ORDERED | _ => Template::show(ListNode::<ORDERED> { items, wide, start: 1 }),
};
@@ -492,7 +491,7 @@ impl<'a> Builder<'a> {
/// Builds an unordered or ordered list from items.
struct ListBuilder<'a> {
styles: StyleChain<'a>,
- labelling: Labelling,
+ kind: ListKind,
items: Vec<ListItem>,
wide: bool,
staged: Vec<(&'a Template, StyleChain<'a>)>,
diff --git a/src/library/heading.rs b/src/library/heading.rs
index dd78b147..3438c7b7 100644
--- a/src/library/heading.rs
+++ b/src/library/heading.rs
@@ -113,9 +113,9 @@ pub enum Leveled<T> {
impl<T: Cast> Leveled<T> {
/// Resolve the value based on the level.
pub fn resolve(self, vm: &mut Vm, level: usize) -> TypResult<T> {
- match self {
- Self::Value(value) => Ok(value),
- Self::Mapping(mapping) => Ok(mapping(level)),
+ Ok(match self {
+ Self::Value(value) => value,
+ Self::Mapping(mapping) => mapping(level),
Self::Func(func, span) => {
let args = Args {
span,
@@ -125,9 +125,9 @@ impl<T: Cast> Leveled<T> {
value: Spanned::new(Value::Int(level as i64), span),
}],
};
- func.call(vm, args)?.cast().at(span)
+ func.call(vm, args)?.cast().at(span)?
}
- }
+ })
}
}
@@ -138,7 +138,7 @@ impl<T: Cast> Cast<Spanned<Value>> for Leveled<T> {
fn cast(value: Spanned<Value>) -> StrResult<Self> {
match value.v {
- Value::Func(f) => Ok(Self::Func(f, value.span)),
+ Value::Func(v) => Ok(Self::Func(v, value.span)),
v => T::cast(v)
.map(Self::Value)
.map_err(|msg| with_alternative(msg, "function")),
diff --git a/src/library/list.rs b/src/library/list.rs
index 1ec89da2..fe499cb1 100644
--- a/src/library/list.rs
+++ b/src/library/list.rs
@@ -1,11 +1,13 @@
//! Unordered (bulleted) and ordered (numbered) lists.
use super::prelude::*;
-use super::{GridNode, ParNode, TextNode, TrackSizing};
+use super::{GridNode, Numbering, ParNode, TextNode, TrackSizing};
+
+use crate::parse::Scanner;
/// An unordered or ordered list.
#[derive(Debug, Hash)]
-pub struct ListNode<const L: Labelling> {
+pub struct ListNode<const L: ListKind> {
/// The individual bulleted or numbered items.
pub items: Vec<ListItem>,
/// If true, there is paragraph spacing between the items, if false
@@ -25,13 +27,15 @@ pub struct ListItem {
}
#[class]
-impl<const L: Labelling> ListNode<L> {
+impl<const L: ListKind> ListNode<L> {
+ /// How the list is labelled.
+ pub const LABEL: Label = Label::Default;
+ /// The spacing between the list items of a non-wide list.
+ pub const SPACING: Linear = Linear::zero();
/// The indentation of each item's label.
pub const LABEL_INDENT: Linear = Relative::new(0.0).into();
/// The space between the label and the body of each item.
pub const BODY_INDENT: Linear = Relative::new(0.5).into();
- /// The spacing between the list items of a non-wide list.
- pub const SPACING: Linear = Linear::zero();
fn construct(_: &mut Vm, args: &mut Args) -> TypResult<Template> {
Ok(Template::show(Self {
@@ -46,30 +50,27 @@ impl<const L: Labelling> ListNode<L> {
}
}
-impl<const L: Labelling> Show for ListNode<L> {
- fn show(&self, _: &mut Vm, styles: StyleChain) -> TypResult<Template> {
+impl<const L: ListKind> Show for ListNode<L> {
+ fn show(&self, vm: &mut Vm, styles: StyleChain) -> TypResult<Template> {
let mut children = vec![];
let mut number = self.start;
+ let label = styles.get_ref(Self::LABEL);
+
for item in &self.items {
number = item.number.unwrap_or(number);
-
- let label = match L {
- UNORDERED => '•'.into(),
- ORDERED | _ => format_eco!("{}.", number),
- };
+ if L == UNORDERED {
+ number = 1;
+ }
children.push(LayoutNode::default());
- children.push(Template::Text(label).pack());
+ children.push(label.resolve(vm, L, number)?.pack());
children.push(LayoutNode::default());
children.push(item.body.clone());
-
number += 1;
}
let em = styles.get(TextNode::SIZE).abs;
- let label_indent = styles.get(Self::LABEL_INDENT).resolve(em);
- let body_indent = styles.get(Self::BODY_INDENT).resolve(em);
let leading = styles.get(ParNode::LEADING);
let spacing = if self.wide {
styles.get(ParNode::SPACING)
@@ -78,6 +79,9 @@ impl<const L: Labelling> Show for ListNode<L> {
};
let gutter = (leading + spacing).resolve(em);
+ let label_indent = styles.get(Self::LABEL_INDENT).resolve(em);
+ let body_indent = styles.get(Self::BODY_INDENT).resolve(em);
+
Ok(Template::block(GridNode {
tracks: Spec::with_x(vec![
TrackSizing::Linear(label_indent.into()),
@@ -91,17 +95,99 @@ impl<const L: Labelling> Show for ListNode<L> {
}
}
-impl<const L: Labelling> From<ListItem> for ListNode<L> {
+impl<const L: ListKind> From<ListItem> for ListNode<L> {
fn from(item: ListItem) -> Self {
Self { items: vec![item], wide: false, start: 1 }
}
}
/// How to label a list.
-pub type Labelling = usize;
+pub type ListKind = usize;
/// Unordered list labelling style.
-pub const UNORDERED: Labelling = 0;
+pub const UNORDERED: ListKind = 0;
/// Ordered list labelling style.
-pub const ORDERED: Labelling = 1;
+pub const ORDERED: ListKind = 1;
+
+/// Either a template or a closure mapping to a template.
+#[derive(Debug, Clone, PartialEq, Hash)]
+pub enum Label {
+ /// The default labelling.
+ Default,
+ /// A pattern with prefix, numbering, lower / upper case and suffix.
+ Pattern(EcoString, Numbering, bool, EcoString),
+ /// A bare template.
+ Template(Template),
+ /// A simple mapping from an item number to a template.
+ Mapping(fn(usize) -> Template),
+ /// A closure mapping from an item number to a value.
+ Func(Func, Span),
+}
+
+impl Label {
+ /// Resolve the value based on the level.
+ pub fn resolve(
+ &self,
+ vm: &mut Vm,
+ kind: ListKind,
+ number: usize,
+ ) -> TypResult<Template> {
+ Ok(match self {
+ Self::Default => match kind {
+ UNORDERED => Template::Text('•'.into()),
+ ORDERED | _ => Template::Text(format_eco!("{}.", number)),
+ },
+ Self::Pattern(prefix, numbering, upper, suffix) => {
+ let fmt = numbering.apply(number);
+ let mid = if *upper { fmt.to_uppercase() } else { fmt.to_lowercase() };
+ Template::Text(format_eco!("{}{}{}", prefix, mid, suffix))
+ }
+ Self::Template(template) => template.clone(),
+ Self::Mapping(mapping) => mapping(number),
+ &Self::Func(ref func, span) => {
+ let args = Args {
+ span,
+ items: vec![Arg {
+ span,
+ name: None,
+ value: Spanned::new(Value::Int(number as i64), span),
+ }],
+ };
+ func.call(vm, args)?.cast().at(span)?
+ }
+ })
+ }
+}
+
+impl Cast<Spanned<Value>> for Label {
+ fn is(value: &Spanned<Value>) -> bool {
+ matches!(&value.v, Value::Template(_) | Value::Func(_))
+ }
+
+ fn cast(value: Spanned<Value>) -> StrResult<Self> {
+ match value.v {
+ Value::Str(pattern) => {
+ let mut s = Scanner::new(&pattern);
+ let mut prefix;
+ let numbering = loop {
+ prefix = s.eaten();
+ match s.eat().map(|c| c.to_ascii_lowercase()) {
+ Some('1') => break Numbering::Arabic,
+ Some('a') => break Numbering::Letter,
+ Some('i') => break Numbering::Roman,
+ Some('*') => break Numbering::Symbol,
+ Some(_) => {}
+ None => Err("invalid pattern")?,
+ }
+ };
+ let upper = s.prev(0).map_or(false, char::is_uppercase);
+ let suffix = s.rest().into();
+ Ok(Self::Pattern(prefix.into(), numbering, upper, suffix))
+ }
+ Value::Template(v) => Ok(Self::Template(v)),
+ Value::Func(v) => Ok(Self::Func(v, value.span)),
+ _ => Err("expected pattern, template or function")?,
+ }
+ }
+}
diff --git a/src/library/mod.rs b/src/library/mod.rs
index 707264d9..ad66f2c3 100644
--- a/src/library/mod.rs
+++ b/src/library/mod.rs
@@ -15,6 +15,7 @@ pub mod image;
pub mod link;
pub mod list;
pub mod math;
+pub mod numbering;
pub mod pad;
pub mod page;
pub mod par;
@@ -40,6 +41,7 @@ pub use hide::*;
pub use link::*;
pub use list::*;
pub use math::*;
+pub use numbering::*;
pub use pad::*;
pub use page::*;
pub use par::*;
@@ -144,6 +146,7 @@ pub fn new() -> Scope {
std.def_func("cmyk", cmyk);
std.def_func("lower", lower);
std.def_func("upper", upper);
+ std.def_func("letter", letter);
std.def_func("roman", roman);
std.def_func("symbol", symbol);
std.def_func("len", len);
diff --git a/src/library/numbering.rs b/src/library/numbering.rs
new file mode 100644
index 00000000..4a2fdbb5
--- /dev/null
+++ b/src/library/numbering.rs
@@ -0,0 +1,108 @@
+//! Conversion of numbers into letters, roman numerals and symbols.
+
+use super::prelude::*;
+
+static ROMANS: &'static [(&'static str, usize)] = &[
+ ("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),
+];
+
+static SYMBOLS: &'static [char] = &['*', '†', '‡', '§', '‖', '¶'];
+
+/// The different kinds of numberings.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+pub enum Numbering {
+ Arabic,
+ Letter,
+ Roman,
+ Symbol,
+}
+
+impl Numbering {
+ /// 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();
+ }
+
+ let mut letters = vec![];
+ while n > 0 {
+ letters.push(b'a' - 1 + (n % 26) as u8);
+ n /= 26;
+ }
+
+ 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 ROMANS {
+ while n >= value {
+ n -= value;
+ fmt.push_str(name);
+ }
+ }
+
+ fmt
+ }
+ Self::Symbol => {
+ if n == 0 {
+ return '-'.into();
+ }
+
+ let symbol = SYMBOLS[(n - 1) % SYMBOLS.len()];
+ let amount = ((n - 1) / SYMBOLS.len()) + 1;
+ std::iter::repeat(symbol).take(amount).collect()
+ }
+ }
+ }
+}
+
+/// Converts an integer into one or multiple letters.
+pub fn letter(_: &mut Vm, args: &mut Args) -> TypResult<Value> {
+ convert(Numbering::Letter, args)
+}
+
+/// Converts an integer into a roman numeral.
+pub fn roman(_: &mut Vm, args: &mut Args) -> TypResult<Value> {
+ convert(Numbering::Roman, args)
+}
+
+/// Convert a number into a symbol.
+pub fn symbol(_: &mut Vm, args: &mut Args) -> TypResult<Value> {
+ convert(Numbering::Symbol, args)
+}
+
+fn convert(numbering: Numbering, args: &mut Args) -> TypResult<Value> {
+ let n = args.expect::<usize>("non-negative integer")?;
+ Ok(Value::Str(numbering.apply(n)))
+}
diff --git a/src/library/utility.rs b/src/library/utility.rs
index 91f0e3ad..f7f46a8f 100644
--- a/src/library/utility.rs
+++ b/src/library/utility.rs
@@ -261,71 +261,6 @@ pub fn upper(_: &mut Vm, args: &mut Args) -> TypResult<Value> {
Ok(args.expect::<EcoString>("string")?.to_uppercase().into())
}
-/// Converts an integer into a roman numeral.
-///
-/// Works for integer between 0 and 3,999,999.
-pub fn roman(_: &mut Vm, args: &mut Args) -> TypResult<Value> {
- // Adapted from Yann Villessuzanne's roman.rs under the Unlicense, at
- // https://github.com/linfir/roman.rs/
- static PAIRS: &'static [(&'static str, usize)] = &[
- ("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),
- ];
-
- let Spanned { mut v, span } = args.expect("non-negative integer")?;
- match v {
- 0_usize => return Ok("N".into()),
- 4_000_000 .. => {
- bail!(
- span,
- "cannot convert integers greater than 3,999,999 to roman numerals"
- )
- }
- _ => {}
- }
-
- let mut roman = String::new();
- for &(name, value) in PAIRS {
- while v >= value {
- v -= value;
- roman.push_str(name);
- }
- }
-
- Ok(roman.into())
-}
-
-/// Convert a number into a roman numeral.
-pub fn symbol(_: &mut Vm, args: &mut Args) -> TypResult<Value> {
- static SYMBOLS: &'static [char] = &['*', '†', '‡', '§', '‖', '¶'];
-
- let n = args.expect::<usize>("non-negative integer")?;
-
- let symbol = SYMBOLS[n % SYMBOLS.len()];
- let amount = (n / SYMBOLS.len()) + 1;
-
- let symbols: String = std::iter::repeat(symbol).take(amount).collect();
- Ok(symbols.into())
-}
-
/// The length of a string, an array or a dictionary.
pub fn len(_: &mut Vm, args: &mut Args) -> TypResult<Value> {
let Spanned { v, span } = args.expect("collection")?;
diff --git a/src/parse/scanner.rs b/src/parse/scanner.rs
index 81fa4fba..685503c3 100644
--- a/src/parse/scanner.rs
+++ b/src/parse/scanner.rs
@@ -90,6 +90,12 @@ impl<'s> Scanner<'s> {
self.rest().chars().next()
}
+ /// Get the nth-previous eaten char.
+ #[inline]
+ pub fn prev(&self, n: usize) -> Option<char> {
+ self.eaten().chars().nth_back(n)
+ }
+
/// Checks whether the next char fulfills a condition.
///
/// Returns `default` if there is no next char.
diff --git a/src/parse/tokens.rs b/src/parse/tokens.rs
index ca2cb74a..e88b49f9 100644
--- a/src/parse/tokens.rs
+++ b/src/parse/tokens.rs
@@ -538,7 +538,7 @@ impl<'s> Tokens<'s> {
fn in_word(&self) -> bool {
let alphanumeric = |c: Option<char>| c.map_or(false, |c| c.is_alphanumeric());
- let prev = self.s.get(.. self.s.last_index()).chars().next_back();
+ let prev = self.s.prev(1);
let next = self.s.peek();
alphanumeric(prev) && alphanumeric(next)
}
diff --git a/src/util/eco_string.rs b/src/util/eco_string.rs
index 953508ce..e3c8fc8a 100644
--- a/src/util/eco_string.rs
+++ b/src/util/eco_string.rs
@@ -366,6 +366,16 @@ impl From<EcoString> for String {
}
}
+impl FromIterator<char> for EcoString {
+ fn from_iter<T: IntoIterator<Item = char>>(iter: T) -> Self {
+ let mut s = Self::new();
+ for c in iter {
+ s.push(c);
+ }
+ s
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;