#[derive(Debug, Clone, PartialEq, Default)]
pub struct ListStyle {
pub marker: Option<ListMarker>,
pub position: ListMarkerPosition,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum ListMarker {
Disc,
Circle,
Square,
Dash,
Decimal(u32),
LowerAlpha(u32),
UpperAlpha(u32),
LowerRoman(u32),
UpperRoman(u32),
Custom(String),
}
impl ListMarker {
#[must_use]
pub fn text(&self) -> String {
match self {
Self::Disc => "\u{00B7}".into(), Self::Circle => "o".into(), Self::Square => "-".into(), Self::Dash => "-".into(), Self::Decimal(n) => format!("{n}."),
Self::LowerAlpha(n) => format!("{}.", nth_alpha(*n, false)),
Self::UpperAlpha(n) => format!("{}.", nth_alpha(*n, true)),
Self::LowerRoman(n) => format!("{}.", to_roman(*n, false)),
Self::UpperRoman(n) => format!("{}.", to_roman(*n, true)),
Self::Custom(s) => s.clone(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum ListMarkerPosition {
#[default]
Outside,
Inside,
}
fn nth_alpha(n: u32, upper: bool) -> String {
if n == 0 {
return String::new();
}
let mut result = String::new();
let mut val = n - 1;
loop {
let ch = (val % 26) as u8;
let base = if upper { b'A' } else { b'a' };
result.insert(0, (base + ch) as char);
if val < 26 {
break;
}
val = val / 26 - 1;
}
result
}
fn to_roman(n: u32, upper: bool) -> String {
const VALS: [(u32, &str, &str); 13] = [
(1000, "m", "M"),
(900, "cm", "CM"),
(500, "d", "D"),
(400, "cd", "CD"),
(100, "c", "C"),
(90, "xc", "XC"),
(50, "l", "L"),
(40, "xl", "XL"),
(10, "x", "X"),
(9, "ix", "IX"),
(5, "v", "V"),
(4, "iv", "IV"),
(1, "i", "I"),
];
if n == 0 {
return "0".into();
}
let mut result = String::new();
let mut remaining = n;
for &(val, lower, upper_str) in &VALS {
while remaining >= val {
result.push_str(if upper { upper_str } else { lower });
remaining -= val;
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decimal_markers() {
assert_eq!(ListMarker::Decimal(1).text(), "1.");
assert_eq!(ListMarker::Decimal(42).text(), "42.");
}
#[test]
fn alpha_markers() {
assert_eq!(ListMarker::LowerAlpha(1).text(), "a.");
assert_eq!(ListMarker::LowerAlpha(26).text(), "z.");
assert_eq!(ListMarker::LowerAlpha(27).text(), "aa.");
assert_eq!(ListMarker::UpperAlpha(3).text(), "C.");
}
#[test]
fn roman_markers() {
assert_eq!(ListMarker::LowerRoman(1).text(), "i.");
assert_eq!(ListMarker::LowerRoman(4).text(), "iv.");
assert_eq!(ListMarker::LowerRoman(9).text(), "ix.");
assert_eq!(ListMarker::LowerRoman(14).text(), "xiv.");
assert_eq!(ListMarker::UpperRoman(2024).text(), "MMXXIV.");
}
#[test]
fn bullet_markers() {
assert_eq!(ListMarker::Disc.text(), "\u{00B7}"); assert_eq!(ListMarker::Circle.text(), "o");
assert_eq!(ListMarker::Square.text(), "-");
assert_eq!(ListMarker::Dash.text(), "-");
}
#[test]
fn custom_marker() {
assert_eq!(ListMarker::Custom(">>".into()).text(), ">>");
}
}