lean_ctx/core/
nc_compress.rs1#![allow(clippy::unnecessary_literal_bound)]
23
24use std::sync::Arc;
25
26use super::extension_registry::{Compressor, ExtensionRegistry, truncate_to_budget};
27
28struct ProseCompressor;
30impl Compressor for ProseCompressor {
31 fn name(&self) -> &str {
32 "prose"
33 }
34 fn compress(&self, input: &str, budget: Option<usize>) -> String {
35 truncate_to_budget(normalize_prose(input), budget)
36 }
37}
38
39struct MarkdownCompressor;
42impl Compressor for MarkdownCompressor {
43 fn name(&self) -> &str {
44 "markdown"
45 }
46 fn compress(&self, input: &str, budget: Option<usize>) -> String {
47 let stripped = strip_html_comments(input);
48 let delinked = rewrite_md_links(&stripped);
49 truncate_to_budget(normalize_prose(&delinked), budget)
50 }
51}
52
53pub fn register_into(reg: &mut ExtensionRegistry) {
56 reg.register_compressor(Arc::new(ProseCompressor));
57 reg.register_compressor(Arc::new(MarkdownCompressor));
58}
59
60fn normalize_prose(input: &str) -> String {
63 let mut out: Vec<String> = Vec::new();
64 let mut blank_run = 0u32;
65 for line in input.lines() {
66 let collapsed = collapse_spaces(line.trim());
67 if collapsed.is_empty() {
68 blank_run += 1;
69 if blank_run <= 1 {
70 out.push(String::new());
71 }
72 } else {
73 blank_run = 0;
74 if out.last().map(String::as_str) == Some(collapsed.as_str()) {
76 continue;
77 }
78 out.push(collapsed);
79 }
80 }
81 out.join("\n").trim().to_string()
82}
83
84fn collapse_spaces(s: &str) -> String {
86 let mut out = String::with_capacity(s.len());
87 let mut prev_space = false;
88 for ch in s.chars() {
89 if ch == ' ' || ch == '\t' {
90 if !prev_space {
91 out.push(' ');
92 prev_space = true;
93 }
94 } else {
95 out.push(ch);
96 prev_space = false;
97 }
98 }
99 out.trim_end().to_string()
100}
101
102fn strip_html_comments(input: &str) -> String {
104 let mut out = String::with_capacity(input.len());
105 let mut rest = input;
106 while let Some(start) = rest.find("<!--") {
107 out.push_str(&rest[..start]);
108 if let Some(end) = rest[start..].find("-->") {
109 rest = &rest[start + end + 3..];
110 } else {
111 rest = "";
112 break;
113 }
114 }
115 out.push_str(rest);
116 out
117}
118
119fn rewrite_md_links(input: &str) -> String {
121 let mut out = String::with_capacity(input.len());
122 let mut i = 0;
123 while i < input.len() {
124 let rest = &input[i..];
125 if let Some(stripped) = rest.strip_prefix("![")
126 && let Some((_, consumed)) = parse_md_link(stripped)
127 {
128 i += 1 + consumed;
130 continue;
131 }
132 if rest.starts_with('[')
133 && let Some((text, consumed)) = parse_md_link(rest)
134 {
135 out.push_str(&text);
136 i += consumed;
137 continue;
138 }
139 let ch = rest.chars().next().unwrap();
140 out.push(ch);
141 i += ch.len_utf8();
142 }
143 out
144}
145
146fn parse_md_link(s: &str) -> Option<(String, usize)> {
149 let close_br = s.find(']')?;
150 if s.as_bytes().get(close_br + 1) != Some(&b'(') {
151 return None;
152 }
153 let after = &s[close_br + 2..];
154 let close_par = after.find(')')?;
155 let text = s[1..close_br].to_string();
156 let consumed = close_br + 2 + close_par + 1;
157 Some((text, consumed))
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 fn prose() -> ProseCompressor {
165 ProseCompressor
166 }
167 fn markdown() -> MarkdownCompressor {
168 MarkdownCompressor
169 }
170
171 #[test]
172 fn prose_collapses_blanks_and_trailing_ws() {
173 let out = prose().compress("a \n\n\n\nb\t\n", None);
174 assert_eq!(out, "a\n\nb");
175 }
176
177 #[test]
178 fn prose_drops_adjacent_duplicate_lines() {
179 let out = prose().compress("same\nsame\nother\nother\nsame", None);
180 assert_eq!(out, "same\nother\nsame");
181 }
182
183 #[test]
184 fn prose_actually_saves_bytes() {
185 let input = "line one \n\n\n\nline one \nline two\n\n\n";
186 let out = prose().compress(input, None);
187 assert!(out.len() < input.len());
188 }
189
190 #[test]
191 fn markdown_strips_comments_images_and_link_urls() {
192 let input =
193 "<!-- hidden -->Visit  the [docs](https://example.com/x) now.";
194 let out = markdown().compress(input, None);
195 assert!(!out.contains("hidden"));
196 assert!(!out.contains("http://img"));
197 assert!(!out.contains("https://example.com"));
198 assert!(out.contains("docs"));
199 assert!(out.contains("Visit"));
200 }
201
202 #[test]
203 fn budget_is_a_hard_byte_ceiling_utf8_safe() {
204 let out = markdown().compress("äöü漢字 text", Some(3));
205 assert!(out.len() <= 3);
206 }
207
208 #[test]
209 fn deterministic() {
210 let input = "a\n\nb \n[x](http://y)";
211 assert_eq!(
212 markdown().compress(input, None),
213 markdown().compress(input, None)
214 );
215 }
216
217 #[test]
218 fn empty_input_stays_empty() {
219 assert_eq!(prose().compress("", None), "");
220 assert_eq!(markdown().compress("", None), "");
221 }
222}