Skip to main content

fur_cli/renderer/
markdown.rs

1use std::fs;
2use std::path::Path;
3use serde_json::Value;
4
5use crate::commands::timeline::TimelineArgs;
6use crate::renderer::utils::load_message;
7
8/// Escape % inside math blocks: $ ... % ... $ → $ ... \% ... $
9fn escape_percent_in_math(text: &str) -> String {
10    let mut out = String::new();
11    let mut chars = text.chars().peekable();
12    let mut in_math = false;
13
14    while let Some(c) = chars.next() {
15        if c == '$' {
16            // Toggle math mode
17            in_math = !in_math;
18            out.push('$');
19            continue;
20        }
21
22        if in_math && c == '%' {
23            out.push_str("\\%");
24            continue;
25        }
26
27        out.push(c);
28    }
29
30    out
31}
32
33/// Convert isolated [ ... ] math blocks into $ ... $ and then escape % inside them.
34fn convert_bracket_math_block(text: &str) -> String {
35    let mut out = String::new();
36    let mut lines = text.lines().peekable();
37
38    while let Some(line) = lines.next() {
39        if line.trim() == "[" {
40            let mut block = String::new();
41
42            while let Some(inner) = lines.next() {
43                if inner.trim() == "]" {
44                    break;
45                }
46                block.push_str(inner);
47                block.push('\n');
48            }
49
50            // Wrap with math and escape %
51            let math = format!("$\n{}$", escape_percent_in_math(&block));
52            out.push_str(&math);
53            out.push('\n');
54            continue;
55        }
56
57        out.push_str(line);
58        out.push('\n');
59    }
60
61    // After processing bracket-blocks, escape % in inline math too
62    escape_percent_in_math(&out)
63}
64
65pub fn render_message_to_md(
66    fur_dir: &Path,
67    msg_id: &str,
68    label: String,
69    args: &TimelineArgs,
70    avatars: &Value,
71    out: &mut String,
72) {
73    let Some(msg) = load_message(fur_dir, msg_id, avatars) else { return };
74
75    if let Some(att) = msg.attachment {
76        if att.ends_with(".png")
77            || att.ends_with(".jpg")
78            || att.ends_with(".jpeg")
79            || att.ends_with(".gif")
80        {
81            out.push_str(&format!("\n![attachment]({})\n\n", att));
82        } else if att.ends_with(".pdf") {
83            out.push_str(&format!(
84                "\n[Attached PDF: {}]({})\n\n",
85                Path::new(&att).file_name().unwrap().to_string_lossy(),
86                att
87            ));
88        } else {
89            out.push_str(&format!("\n[Attachment: {}]\n\n", att));
90        }
91    }
92
93    out.push_str(&format!("**{} [{}]:** {}\n", msg.name, msg.emoji, msg.text));
94    out.push_str(&format!("_{} {} - {}_\n\n", msg.date_str, msg.time_str, label));
95
96    if args.verbose || args.contents {
97        if let Some(path_str) = msg.markdown {
98            if let Ok(contents) = fs::read_to_string(path_str) {
99                // 🔥 Math-block conversion applied here
100                let converted = convert_bracket_math_block(&contents);
101                out.push_str(&format!("\n{}\n", converted));
102
103            }
104        }
105    }
106
107    // Branches
108    for (bi, block) in msg.branches.iter().enumerate() {
109        let branch_label = format!("{} - Branch {}", label, bi + 1);
110        for cid in block {
111            render_message_to_md(fur_dir, cid, branch_label.clone(), args, avatars, out);
112        }
113    }
114}