kanbrain_core/model/
text.rs1const MAX_SLUG_LEN: usize = 40;
5
6pub fn parse_tags(s: &str) -> Vec<String> {
8 let mut tags: Vec<String> = Vec::new();
9 for raw in s.split(|c: char| c == ',' || c.is_whitespace()) {
10 let tag = raw.trim_start_matches('#').to_lowercase();
11 if !tag.is_empty() && !tags.contains(&tag) {
12 tags.push(tag);
13 }
14 }
15 tags
16}
17
18pub fn counted(n: usize, one: &str, many: &str) -> String {
20 format!("{n} {}", if n == 1 { one } else { many })
21}
22
23pub fn one_line(title: &str) -> String {
26 title.lines().map(str::trim).filter(|line| !line.is_empty()).collect::<Vec<_>>().join(" ")
27}
28
29pub fn slugify(title: &str) -> String {
31 let mut slug = String::new();
32 for c in title.chars() {
33 if slug.len() >= MAX_SLUG_LEN {
34 break;
35 }
36 if c.is_ascii_alphanumeric() {
37 slug.push(c.to_ascii_lowercase());
38 } else if !slug.ends_with('-') {
39 slug.push('-');
40 }
41 }
42 let slug = slug.trim_matches('-');
43 if slug.is_empty() { "task".to_string() } else { slug.to_string() }
44}
45
46pub fn sanitize_description(text: &str) -> (String, bool) {
50 demote(text, &["## "], "### ")
51}
52
53pub fn sanitize_comment(text: &str) -> (String, bool) {
55 demote(text, &["## ", "### "], "#### ")
56}
57
58fn demote(text: &str, prefixes: &[&str], to: &str) -> (String, bool) {
60 let mut changed = false;
61 let lines: Vec<String> = text
62 .lines()
63 .map(|line| match prefixes.iter().find_map(|p| line.strip_prefix(p)) {
64 Some(rest) => {
65 changed = true;
66 format!("{to}{rest}")
67 }
68 None => line.to_string(),
69 })
70 .collect();
71 (lines.join("\n").trim().to_string(), changed)
72}
73
74#[cfg(test)]
75#[path = "../../tests/model/text.rs"]
76mod tests;