Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-core/src/elements/list.rs
1//! Utilities for editing lists.
2 
3use enum_iterator::Sequence;
4use std::fmt;
5 
6#[cfg(test)]
7#[path = "list_tests.rs"]
8mod tests;
9 
10/// Technically we don't need to cap these numbers. But the list
11/// would be hard to render and read when the text gets too long.
12/// For now, we cap the alphabet list at 26*3 = 78 and roman list at
13/// 30, which should be sufficient for most of the use cases.
14const MAX_ALPHABET_NUM: usize = 78;
15const MAX_ROMAN_NUM: usize = 30;
16 
17/// The indentation level we support in unordered and ordered list.
18#[derive(Eq, PartialEq, Clone, Copy, Debug, Hash, Sequence, PartialOrd, Ord)]
19pub enum ListIndentLevel {
20 One,
21 Two,
22 Three,
23}
24 
25impl ListIndentLevel {
26 /// Only supports for indent level up to 2. If the indent level is greater than 2, it will snap
27 /// to [`ListIndentLevel::Three`].
28 pub fn from_usize(indent_level: usize) -> Self {
29 match indent_level {
30 0 => Self::One,
31 1 => Self::Two,
32 2 => Self::Three,
33 _ => {
34 log::warn!("Only support indent level up to 2");
35 Self::Three
36 }
37 }
38 }
39 
40 /// Supports for any indent level. If the indent level is greater than 2, it will return
41 /// the result of [`Self::from_usize`] with the indentation level mod 3 (cyclic).
42 pub fn from_usize_unbounded(indentation_level: usize) -> Self {
43 match indentation_level {
44 0 => Self::One,
45 1 => Self::Two,
46 2 => Self::Three,
47 _ => Self::from_usize(indentation_level % 3),
48 }
49 }
50 
51 pub fn as_usize(&self) -> usize {
52 match self {
53 Self::One => 0,
54 Self::Two => 1,
55 Self::Three => 2,
56 }
57 }
58 
59 pub fn shift_right(self) -> Self {
60 match self {
61 Self::One => Self::Two,
62 Self::Two | Self::Three => Self::Three,
63 }
64 }
65 
66 pub fn shift_left(self) -> Self {
67 match self {
68 Self::Three => Self::Two,
69 Self::One | Self::Two => Self::One,
70 }
71 }
72 
73 /// Get the string representation of the number for the ordered list item of the given indentation.
74 pub fn list_number_string(&self, number: usize) -> String {
75 match self {
76 ListIndentLevel::One => number.to_string(),
77 ListIndentLevel::Two => number_to_alphabet(number.saturating_sub(1)),
78 ListIndentLevel::Three => number_to_roman(number.saturating_sub(1)),
79 }
80 }
81}
82 
83impl fmt::Display for ListIndentLevel {
84 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
85 f.write_str(match self {
86 ListIndentLevel::One => "1",
87 ListIndentLevel::Two => "2",
88 ListIndentLevel::Three => "3",
89 })
90 }
91}
92 
93/// Tracker for ordered list numbers.
94#[derive(Default)]
95pub struct ListNumbering {
96 /// The current list index at each indent level, from 0 to the current indent level.
97 /// * When entering a new sublist (the indent level increases), we start its index at the first
98 /// item's number (or 1 for auto-numbered items).
99 /// * When exiting a sublist (the indent level decreases), we truncate to the parent indent
100 /// level so that numbering from one sublist doesn't affect later sublists that happen to be
101 /// at the same indent level.
102 /// * Within one list/sublist, we only use the first item's number, and auto-number all
103 /// subsequent items.
104 indices_by_level: Vec<usize>,
105}
106 
107#[derive(Debug, PartialEq)]
108pub struct OrderedListLabel {
109 /// The numerical value of the label.
110 pub label_index: usize,
111 /// The displayed string of the label.
112 pub display_label: String,
113}
114 
115impl ListNumbering {
116 /// Construct a new numbering tracker.
117 pub fn new() -> Self {
118 Self::default()
119 }
120 
121 /// Returns `true` if the next list item is explicitly numberable (i.e. if the `number`
122 /// parameter to [`Self::advance`] will be respected).
123 pub fn can_number(&self, indent: usize) -> bool {
124 self.indices_by_level.len() <= indent
125 }
126 
127 /// Advance to the next ordered list item, returning its index.
128 ///
129 /// ## Parameters
130 /// * `indent` the current list indent level, starting at 0.
131 /// * `number` the number assigned to the list, if present. If the item is not the first at its
132 /// indent level, this number is ignored. This matches Markdown's behavior and the semantics
133 /// of the HTML `start` attribute.
134 pub fn advance(&mut self, indent: usize, number: Option<usize>) -> OrderedListLabel {
135 let can_number = self.can_number(indent);
136 self.indices_by_level.resize(indent + 1, 0);
137 
138 // Panic-safety: Due to the resize above, `self.indices` contains exactly `indent + 1`
139 // items, so `indent` is a valid index.
140 let slot = &mut self.indices_by_level[indent];
141 
142 match number {
143 Some(number) if can_number => *slot = number,
144 _ => *slot += 1,
145 }
146 
147 let indentation_level = ListIndentLevel::from_usize_unbounded(indent);
148 OrderedListLabel {
149 label_index: *slot,
150 display_label: indentation_level.list_number_string(*slot),
151 }
152 }
153 
154 /// Reset after encountering a non-ordered-list item.
155 pub fn reset(&mut self) {
156 self.indices_by_level.clear();
157 }
158}
159 
160/// Convert a number into a alphabet for ordered lists. We would repeat the alphabet to
161/// represent any number larger than 26. For example, 27 would be "aa".
162/// Note that this is 0-based so 0 -> 'a', 1 -> 'b'.
163fn number_to_alphabet(num: usize) -> String {
164 // Cap it to the max number of alphabet repeats.
165 let capped_num = num % MAX_ALPHABET_NUM;
166 
167 let num_repeat = capped_num / 26;
168 let remainder = (capped_num % 26) as u8;
169 
170 let alphabet = (remainder + 97) as char;
171 alphabet.to_string().repeat(num_repeat + 1)
172}
173 
174/// Convert a number into a roman number for ordered lists.
175/// Note that this is 0-based so 0 -> 'i', 1 -> 'ii'.
176fn number_to_roman(num: usize) -> String {
177 // Cap it to the max number we want to represent.
178 let mut capped_num = (num % MAX_ROMAN_NUM) + 1;
179 
180 let roman_pairs = [("x", 10), ("ix", 9), ("v", 5), ("iv", 4), ("i", 1)];
181 let mut result = String::new();
182 for (name, value) in roman_pairs.iter() {
183 while capped_num >= *value {
184 capped_num -= value;
185 result.push_str(name);
186 }
187 }
188 
189 result
190}
191