papyri_lang/utils/
text.rs1use std::rc::Rc;
4use deunicode;
5use htmlentity::entity;
6use indexmap::IndexSet;
7
8pub fn is_identifier(s: &str) -> bool {
10 let mut s_chars = s.chars();
11 matches!(s_chars.next(), Some(c) if is_ident_start(c))
12 && s_chars.all(is_ident_cont)
13}
14
15pub fn is_ident_start(c: char) -> bool {
18 c.is_ascii_alphabetic() || c == '_'
19}
20
21pub fn is_ident_cont(c: char) -> bool {
24 c.is_ascii_alphanumeric() || c == '_'
25}
26
27pub fn make_identifier(s: &str, max_len: usize) -> String {
31 let s = deunicode::deunicode(s);
32 let mut s_chars = s.chars();
33 let mut id = "".to_string();
34 match s_chars.next() {
35 Some(c) if c.is_ascii_alphabetic() => id.push(c),
36 _ => id.push('_'),
37 }
38 for c in s_chars {
39 if id.len() >= max_len {
40 break;
41 } else if c.is_ascii_alphanumeric() {
42 id.push(c);
43 } else if !id.ends_with('_') {
44 id += "_";
45 }
46 }
47 id
48}
49
50pub fn looks_like_glob(s: &str) -> bool {
53 s.contains(|c| matches!(c, '*' | '?' | '[' | ']'))
54}
55
56pub fn is_whitespace(s: &str) -> bool {
59 s.chars().all(|c| c.is_ascii_whitespace())
60}
61
62pub fn pluralise(quantity: u32) -> &'static str {
65 if quantity == 1 { "" } else { "s" }
66}
67
68pub fn encode_entities(s: &str, escape_quotes: bool) -> String {
71 entity::encode(
72 s,
73 if escape_quotes { entity::EntitySet::SpecialChars } else { entity::EntitySet::Html },
74 entity::EncodeType::NamedOrHex,
75 ).into_iter().collect()
76}
77
78pub fn fix_indentation(s: &str) -> String {
83 let mut indentation_to_remove: Option<&str> = None;
84 let mut out = "".to_string();
85 for line in s.trim_end().lines() {
86 match indentation_to_remove {
87 Some(indentation) => {
88 if let Some(stripped) = line.strip_prefix(indentation) {
89 out += stripped;
90 } else {
91 out += line.trim_start();
92 }
93 out += "\n";
94 },
95 None => {
96 if let Some((index, _)) = line.chars().enumerate().find(|(_, c)| !c.is_whitespace()) {
97 if index == 0 { return s.trim().to_string(); }
99
100 indentation_to_remove = Some(&line[..index]);
101 out += &line[index..];
102 out += "\n";
103 }
104 },
105 }
106 }
107 out
108}
109
110pub fn get_source_language_hint<'a>(src: &'a str, default: &'a str) -> (&'a str, &'a str) {
115 let Some(k) = src.find('\n') else {
116 return (default, src);
117 };
118
119 let first_line = src[..k].trim_end();
120 if is_identifier(first_line) {
121 (first_line, &src[k + 1..])
122 } else {
123 (default, src)
124 }
125}
126
127pub struct UniqueIDGenerator {
131 ids_used: IndexSet<Rc<str>, fxhash::FxBuildHasher>,
132}
133
134impl Default for UniqueIDGenerator {
135 fn default() -> UniqueIDGenerator {
136 UniqueIDGenerator::new()
137 }
138}
139
140impl UniqueIDGenerator {
141 pub fn new() -> UniqueIDGenerator {
143 UniqueIDGenerator {
144 ids_used: IndexSet::default(),
145 }
146 }
147
148 pub fn clear(&mut self) {
150 self.ids_used.clear();
151 }
152
153 pub fn get_unique_id(&mut self, id_base: &str, max_len: usize) -> Rc<str> {
160 let mut id = if !is_identifier(id_base) {
161 make_identifier(id_base, max_len)
162 } else if id_base.len() > max_len {
163 id_base[..max_len].to_string()
164 } else {
165 id_base.to_string()
166 };
167 id.make_ascii_lowercase();
168
169 let id: Rc<str> = Rc::from(id);
170 if self.ids_used.insert(id.clone()) { return id; }
171
172 let mut id_base = id_base;
173 if id_base.len() + 2 > max_len && max_len >= 3 {
174 id_base = &id_base[..max_len - 2];
175 }
176
177 let mut counter = 2;
178 let id: Rc<str> = loop {
179 let id = format!("{id_base}_{counter}");
180 if !self.ids_used.contains(id.as_str()) {
181 break Rc::from(id);
182 }
183
184 counter += 1;
185 if id.len() >= max_len && id_base.len() > 1 && id.trim_end_matches('9').ends_with('_') {
187 id_base = &id_base[..id_base.len() - 1];
188 }
189 };
190
191 self.ids_used.insert(id.clone());
192 id
193 }
194}