List label styling
This commit is contained in:
parent
980f898d55
commit
acae6e2a54
@ -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>)>,
|
||||
|
@ -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")),
|
||||
|
@ -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")?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
108
src/library/numbering.rs
Normal file
108
src/library/numbering.rs
Normal file
@ -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)))
|
||||
}
|
@ -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")?;
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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::*;
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 26 KiB |
@ -21,3 +21,40 @@
|
||||
#for i in range(5) {
|
||||
[. #roman(1 + i)]
|
||||
}
|
||||
|
||||
---
|
||||
// Test label pattern.
|
||||
#set enum(label: "~ A:")
|
||||
. First
|
||||
. Second
|
||||
|
||||
#set enum(label: "(*)")
|
||||
. A
|
||||
. B
|
||||
. C
|
||||
|
||||
#set enum(label: "i)")
|
||||
. A
|
||||
. B
|
||||
|
||||
---
|
||||
// Test label closure.
|
||||
#enum(
|
||||
start: 4,
|
||||
spacing: -3pt,
|
||||
label: n => text(fill: (red, green, blue)(mod(n, 3)), [#upper(letter(n))]),
|
||||
[Red], [Green], [Blue],
|
||||
)
|
||||
|
||||
---
|
||||
// Error: 18-20 invalid pattern
|
||||
#set enum(label: "")
|
||||
|
||||
---
|
||||
// Error: 18-24 invalid pattern
|
||||
#set enum(label: "(())")
|
||||
|
||||
---
|
||||
// Error: 18-28 expected template, found boolean
|
||||
#set enum(label: n => false)
|
||||
. A
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Test string functions.
|
||||
|
||||
---
|
||||
// Test the `upper`, `lower`, and number formatting functions.
|
||||
// Test the `upper`, `lower`, and number formatting functions.
|
||||
#upper("Abc 8 def")
|
||||
|
||||
#lower("SCREAMING MUST BE SILENCED in " + roman(1672) + " years")
|
||||
@ -14,10 +14,6 @@
|
||||
parbreak()
|
||||
}
|
||||
|
||||
---
|
||||
// Error: 8-15 cannot convert integers greater than 3,999,999 to roman numerals
|
||||
#roman(8000000)
|
||||
|
||||
---
|
||||
// Error: 9-11 must be at least zero
|
||||
#symbol(-1)
|
||||
|
Loading…
x
Reference in New Issue
Block a user