rs_web/markdown/
encrypted_blocks.rs1use once_cell::sync::Lazy;
2use regex::Regex;
3
4#[derive(Debug, Clone)]
6pub struct EncryptedBlock {
7 pub content: String,
9 pub id: usize,
11 pub password: Option<String>,
13}
14
15#[derive(Debug)]
17pub struct PreprocessResult {
18 pub markdown: String,
20 pub blocks: Vec<EncryptedBlock>,
22}
23
24static ENCRYPTED_BLOCK_RE: Lazy<Regex> = Lazy::new(|| {
26 Regex::new(r#"(?ms)^:::encrypted(?:\s+password="([^"]*)")?\s*\n(.*?)\n:::(?:\s*$|\n)"#)
27 .expect("Invalid encrypted block regex pattern")
28});
29
30static HTML_ENCRYPTED_BLOCK_RE: Lazy<Regex> = Lazy::new(|| {
32 Regex::new(r#"(?ms)<encrypted(?:\s+password="([^"]*)")?>(.*?)</encrypted>"#)
33 .expect("Invalid HTML encrypted block regex pattern")
34});
35
36pub fn extract_encrypted_blocks(content: &str) -> PreprocessResult {
40 let mut blocks = Vec::new();
41 let mut block_id = 0;
42
43 let markdown = ENCRYPTED_BLOCK_RE
44 .replace_all(content, |caps: ®ex::Captures| {
45 let password = caps.get(1).map(|m| m.as_str().to_string());
46 let block_content = caps.get(2).map_or("", |m| m.as_str()).to_string();
47 let placeholder = format!("<!-- ENCRYPTED_BLOCK_{} -->", block_id);
48
49 blocks.push(EncryptedBlock {
50 content: block_content,
51 id: block_id,
52 password,
53 });
54
55 block_id += 1;
56 placeholder
57 })
58 .to_string();
59
60 PreprocessResult { markdown, blocks }
61}
62
63pub fn extract_html_encrypted_blocks(content: &str) -> PreprocessResult {
68 let mut blocks = Vec::new();
69 let mut block_id = 0;
70
71 let html = HTML_ENCRYPTED_BLOCK_RE
72 .replace_all(content, |caps: ®ex::Captures| {
73 let password = caps.get(1).map(|m| m.as_str().to_string());
74 let block_content = caps.get(2).map_or("", |m| m.as_str()).to_string();
75 let placeholder = format!("<!-- ENCRYPTED_BLOCK_{} -->", block_id);
76
77 blocks.push(EncryptedBlock {
78 content: block_content,
79 id: block_id,
80 password,
81 });
82
83 block_id += 1;
84 placeholder
85 })
86 .to_string();
87
88 PreprocessResult {
89 markdown: html,
90 blocks,
91 }
92}
93
94pub fn replace_placeholders(
97 html: &str,
98 encrypted_htmls: &[(usize, String, String, String, bool)],
99 slug: &str,
100) -> String {
101 let mut result = html.to_string();
102
103 for (id, ciphertext, salt, nonce, has_own_password) in encrypted_htmls {
104 let placeholder = format!("<!-- ENCRYPTED_BLOCK_{} -->", id);
105 let own_password_attr = if *has_own_password {
106 "\n data-own-password=\"true\""
107 } else {
108 ""
109 };
110 let replacement = format!(
111 r#"<div class="encrypted-content encrypted-block"
112 data-encrypted="{}"
113 data-salt="{}"
114 data-nonce="{}"
115 data-slug="{}"
116 data-block-id="{}"{}>
117 <div class="decrypt-prompt">
118 <p class="encrypted-message">This section is encrypted.</p>
119 <input type="password" placeholder="Enter password..." aria-label="Password">
120 <label class="remember-label">
121 <input type="checkbox" class="remember">
122 Remember for this post
123 </label>
124 <button type="button">Decrypt</button>
125 </div>
126</div>"#,
127 ciphertext, salt, nonce, slug, id, own_password_attr
128 );
129 result = result.replace(&placeholder, &replacement);
130 }
131
132 result
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn test_extract_single_block() {
141 let content = r#"# Hello
142
143Some public content.
144
145:::encrypted
146This is secret content.
147It has multiple lines.
148:::
149
150More public content.
151"#;
152
153 let result = extract_encrypted_blocks(content);
154
155 assert_eq!(result.blocks.len(), 1);
156 assert_eq!(result.blocks[0].id, 0);
157 assert!(result.blocks[0].content.contains("This is secret content."));
158 assert!(result.blocks[0].password.is_none());
159 assert!(result.markdown.contains("<!-- ENCRYPTED_BLOCK_0 -->"));
160 assert!(!result.markdown.contains(":::encrypted"));
161 }
162
163 #[test]
164 fn test_extract_multiple_blocks() {
165 let content = r#"# Hello
166
167:::encrypted
168First secret.
169:::
170
171Public part.
172
173:::encrypted
174Second secret.
175:::
176
177End.
178"#;
179
180 let result = extract_encrypted_blocks(content);
181
182 assert_eq!(result.blocks.len(), 2);
183 assert_eq!(result.blocks[0].id, 0);
184 assert_eq!(result.blocks[1].id, 1);
185 assert!(result.blocks[0].content.contains("First secret."));
186 assert!(result.blocks[1].content.contains("Second secret."));
187 assert!(result.markdown.contains("<!-- ENCRYPTED_BLOCK_0 -->"));
188 assert!(result.markdown.contains("<!-- ENCRYPTED_BLOCK_1 -->"));
189 }
190
191 #[test]
192 fn test_extract_block_with_password() {
193 let content = r#"# Hello
194
195:::encrypted password="secret123"
196This is secret content.
197:::
198
199More content.
200"#;
201
202 let result = extract_encrypted_blocks(content);
203
204 assert_eq!(result.blocks.len(), 1);
205 assert_eq!(result.blocks[0].password, Some("secret123".to_string()));
206 assert!(result.blocks[0].content.contains("This is secret content."));
207 }
208
209 #[test]
210 fn test_extract_blocks_with_different_passwords() {
211 let content = r#"# Hello
212
213:::encrypted password="pass1"
214First secret.
215:::
216
217:::encrypted password="pass2"
218Second secret.
219:::
220
221:::encrypted
222Third secret (no password).
223:::
224"#;
225
226 let result = extract_encrypted_blocks(content);
227
228 assert_eq!(result.blocks.len(), 3);
229 assert_eq!(result.blocks[0].password, Some("pass1".to_string()));
230 assert_eq!(result.blocks[1].password, Some("pass2".to_string()));
231 assert_eq!(result.blocks[2].password, None);
232 }
233
234 #[test]
235 fn test_no_encrypted_blocks() {
236 let content = "# Hello\n\nJust regular content.\n";
237
238 let result = extract_encrypted_blocks(content);
239
240 assert!(result.blocks.is_empty());
241 assert_eq!(result.markdown, content);
242 }
243
244 #[test]
245 fn test_replace_placeholders() {
246 let html = "<p>Hello</p>\n<!-- ENCRYPTED_BLOCK_0 -->\n<p>World</p>";
247 let encrypted = vec![(
248 0,
249 "cipher".to_string(),
250 "salt".to_string(),
251 "nonce".to_string(),
252 false,
253 )];
254
255 let result = replace_placeholders(html, &encrypted, "test-post");
256
257 assert!(result.contains("data-encrypted=\"cipher\""));
258 assert!(result.contains("data-salt=\"salt\""));
259 assert!(result.contains("data-nonce=\"nonce\""));
260 assert!(result.contains("data-slug=\"test-post\""));
261 assert!(result.contains("data-block-id=\"0\""));
262 assert!(!result.contains("data-own-password"));
263 assert!(!result.contains("<!-- ENCRYPTED_BLOCK_0 -->"));
264 }
265
266 #[test]
267 fn test_replace_placeholders_with_own_password() {
268 let html = "<!-- ENCRYPTED_BLOCK_0 -->";
269 let encrypted = vec![(
270 0,
271 "cipher".to_string(),
272 "salt".to_string(),
273 "nonce".to_string(),
274 true,
275 )];
276
277 let result = replace_placeholders(html, &encrypted, "test-post");
278
279 assert!(result.contains("data-own-password=\"true\""));
280 }
281
282 #[test]
283 fn test_html_extract_single_block() {
284 let content = r#"<h1>Hello</h1>
285<p>Some public content.</p>
286<encrypted>
287This is secret content.
288It has multiple lines.
289</encrypted>
290<p>More public content.</p>"#;
291
292 let result = extract_html_encrypted_blocks(content);
293
294 assert_eq!(result.blocks.len(), 1);
295 assert_eq!(result.blocks[0].id, 0);
296 assert!(result.blocks[0].content.contains("This is secret content."));
297 assert!(result.blocks[0].password.is_none());
298 assert!(result.markdown.contains("<!-- ENCRYPTED_BLOCK_0 -->"));
299 assert!(!result.markdown.contains("<encrypted>"));
300 }
301
302 #[test]
303 fn test_html_extract_multiple_blocks() {
304 let content = r#"<h1>Hello</h1>
305<encrypted>First secret.</encrypted>
306<p>Public part.</p>
307<encrypted>Second secret.</encrypted>
308<p>End.</p>"#;
309
310 let result = extract_html_encrypted_blocks(content);
311
312 assert_eq!(result.blocks.len(), 2);
313 assert_eq!(result.blocks[0].id, 0);
314 assert_eq!(result.blocks[1].id, 1);
315 assert!(result.blocks[0].content.contains("First secret."));
316 assert!(result.blocks[1].content.contains("Second secret."));
317 assert!(result.markdown.contains("<!-- ENCRYPTED_BLOCK_0 -->"));
318 assert!(result.markdown.contains("<!-- ENCRYPTED_BLOCK_1 -->"));
319 }
320
321 #[test]
322 fn test_html_extract_block_with_password() {
323 let content = r#"<h1>Hello</h1>
324<encrypted password="secret123">This is secret content.</encrypted>
325<p>More content.</p>"#;
326
327 let result = extract_html_encrypted_blocks(content);
328
329 assert_eq!(result.blocks.len(), 1);
330 assert_eq!(result.blocks[0].password, Some("secret123".to_string()));
331 assert!(result.blocks[0].content.contains("This is secret content."));
332 }
333
334 #[test]
335 fn test_html_extract_blocks_with_different_passwords() {
336 let content = r#"<encrypted password="pass1">First secret.</encrypted>
337<encrypted password="pass2">Second secret.</encrypted>
338<encrypted>Third secret (no password).</encrypted>"#;
339
340 let result = extract_html_encrypted_blocks(content);
341
342 assert_eq!(result.blocks.len(), 3);
343 assert_eq!(result.blocks[0].password, Some("pass1".to_string()));
344 assert_eq!(result.blocks[1].password, Some("pass2".to_string()));
345 assert_eq!(result.blocks[2].password, None);
346 }
347
348 #[test]
349 fn test_html_no_encrypted_blocks() {
350 let content = "<h1>Hello</h1>\n<p>Just regular content.</p>\n";
351
352 let result = extract_html_encrypted_blocks(content);
353
354 assert!(result.blocks.is_empty());
355 assert_eq!(result.markdown, content);
356 }
357}