Skip to main content

memo_cli/preprocess/
detect.rs

1use super::ContentType;
2use super::validate::looks_like_url;
3
4pub fn detect_content_type(input: &str) -> ContentType {
5    let trimmed = input.trim();
6    if trimmed.is_empty() {
7        return ContentType::Unknown;
8    }
9    if looks_like_url(trimmed) {
10        return ContentType::Url;
11    }
12    if looks_like_json(trimmed) {
13        return ContentType::Json;
14    }
15    if looks_like_xml(trimmed) {
16        return ContentType::Xml;
17    }
18    if looks_like_yaml(trimmed) {
19        return ContentType::Yaml;
20    }
21    if looks_like_markdown(trimmed) {
22        return ContentType::Markdown;
23    }
24    ContentType::Text
25}
26
27fn looks_like_json(input: &str) -> bool {
28    matches!(input.chars().next(), Some('{') | Some('['))
29}
30
31fn looks_like_xml(input: &str) -> bool {
32    if !input.starts_with('<') || !input.contains('>') {
33        return false;
34    }
35    matches!(
36        input.chars().nth(1),
37        Some(ch) if ch.is_ascii_alphabetic() || matches!(ch, '/' | '!' | '?')
38    )
39}
40
41fn looks_like_yaml(input: &str) -> bool {
42    if input.starts_with("---") || input.starts_with("- ") {
43        return true;
44    }
45
46    for raw_line in input.lines() {
47        let line = raw_line.trim_start();
48        if line.is_empty() || line.starts_with('#') {
49            continue;
50        }
51        if let Some((key, _)) = line.split_once(':') {
52            let key = key.trim();
53            if key.is_empty() {
54                continue;
55            }
56            if line.contains("://") {
57                continue;
58            }
59            if key.contains('{') || key.contains('[') {
60                continue;
61            }
62            return true;
63        }
64    }
65    false
66}
67
68fn looks_like_markdown(input: &str) -> bool {
69    let mut saw_inline_marker = false;
70    for raw_line in input.lines() {
71        let line = raw_line.trim_start();
72        if line.starts_with('#')
73            || line.starts_with("> ")
74            || line.starts_with("```")
75            || is_ordered_list_item(line)
76            || contains_markdown_link(line)
77        {
78            return true;
79        }
80        if line.contains("**") || line.contains("__") || line.contains('`') {
81            saw_inline_marker = true;
82        }
83    }
84    saw_inline_marker
85}
86
87fn is_ordered_list_item(line: &str) -> bool {
88    let mut chars = line.chars().peekable();
89    let mut saw_digit = false;
90    while let Some(ch) = chars.peek() {
91        if ch.is_ascii_digit() {
92            saw_digit = true;
93            chars.next();
94            continue;
95        }
96        break;
97    }
98    if !saw_digit {
99        return false;
100    }
101    matches!(chars.next(), Some('.')) && matches!(chars.next(), Some(' '))
102}
103
104fn contains_markdown_link(line: &str) -> bool {
105    let Some(open_bracket) = line.find('[') else {
106        return false;
107    };
108    let Some(link_start_rel) = line[open_bracket + 1..].find("](") else {
109        return false;
110    };
111    let link_start = open_bracket + 1 + link_start_rel + 2;
112    line[link_start..].contains(')')
113}