memo_cli/preprocess/
detect.rs1use 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}