Skip to content

Commit 9059fe3

Browse files
committed
feat: rendering
- generated tho
1 parent 17a3ec1 commit 9059fe3

File tree

2 files changed

+371
-8
lines changed

2 files changed

+371
-8
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ rustyline = { version = "13.0", features = ["with-file-history"] }
3131
dirs = "6.0.0"
3232
colored = "2.0"
3333
crossterm = "0.27"
34+
pulldown-cmark = "0.9"
35+
syntect = "5.2.0"
3436

3537
[[bin]]
3638
name = "yappus"

src/utils.rs

Lines changed: 369 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use directories::ProjectDirs;
2+
use pulldown_cmark::{Options, Parser};
23
use std::fs;
34
use std::path::{Path, PathBuf};
45

@@ -13,14 +14,374 @@ pub fn get_config_dir() -> PathBuf {
1314
}
1415

1516
pub fn render_response(text: &str) -> String {
16-
let mut formatted = text.replace("```bash", "\x1b[1;33m"); // Yellow for bash code
17-
formatted = formatted.replace("```shell", "\x1b[1;33m"); // Yellow for shell code
18-
formatted = formatted.replace("```javascript", "\x1b[1;34m"); // Blue for JavaScript
19-
formatted = formatted.replace("```python", "\x1b[1;32m"); // Green for Python
20-
formatted = formatted.replace("```rust", "\x1b[1;31m"); // Red for Rust
21-
formatted = formatted.replace("```", "\x1b[0m"); // Reset color at end of code block
22-
23-
formatted
17+
render_markdown(text)
18+
}
19+
20+
fn render_markdown(markdown: &str) -> String {
21+
let mut options = Options::empty();
22+
options.insert(Options::ENABLE_STRIKETHROUGH);
23+
options.insert(Options::ENABLE_TABLES);
24+
options.insert(Options::ENABLE_FOOTNOTES);
25+
options.insert(Options::ENABLE_TASKLISTS);
26+
27+
let parser = Parser::new_ext(markdown, options);
28+
let mut output = String::new();
29+
let mut in_code_block = false;
30+
let mut code_language: String = String::new();
31+
let mut code_content = String::new();
32+
let mut list_depth: usize = 0;
33+
34+
for event in parser {
35+
match event {
36+
pulldown_cmark::Event::Start(tag) => {
37+
match tag {
38+
pulldown_cmark::Tag::Heading(level, _, _) => {
39+
output.push('\n');
40+
let (color, symbol) = match level {
41+
pulldown_cmark::HeadingLevel::H1 => ("\x1b[1;96m", "█"), // Bright cyan
42+
pulldown_cmark::HeadingLevel::H2 => ("\x1b[1;95m", "▓"), // Bright magenta
43+
pulldown_cmark::HeadingLevel::H3 => ("\x1b[1;94m", "▒"), // Bright blue
44+
pulldown_cmark::HeadingLevel::H4 => ("\x1b[1;93m", "░"), // Bright yellow
45+
_ => ("\x1b[1;97m", "▪"), // Bright white
46+
};
47+
output.push_str(&format!("{}{} ", color, symbol));
48+
}
49+
pulldown_cmark::Tag::Strong => {
50+
output.push_str("\x1b[1;97m"); // Bright white bold
51+
}
52+
pulldown_cmark::Tag::Emphasis => {
53+
output.push_str("\x1b[3;96m"); // Italic cyan
54+
}
55+
pulldown_cmark::Tag::CodeBlock(kind) => {
56+
in_code_block = true;
57+
code_content.clear();
58+
59+
if let pulldown_cmark::CodeBlockKind::Fenced(lang) = kind {
60+
code_language = lang.to_string();
61+
} else {
62+
code_language.clear();
63+
}
64+
65+
output.push('\n');
66+
// Top border
67+
output.push_str("\x1b[90m┌");
68+
if !code_language.is_empty() {
69+
output.push_str(&format!("─ \x1b[93m{}\x1b[90m ", code_language));
70+
output.push_str(
71+
&"─".repeat(60_usize.saturating_sub(code_language.len() + 3)),
72+
);
73+
} else {
74+
output.push_str(&"─".repeat(60));
75+
}
76+
output.push_str("┐\x1b[0m\n");
77+
}
78+
pulldown_cmark::Tag::List(_) => {
79+
if list_depth == 0 {
80+
output.push('\n');
81+
}
82+
list_depth += 1;
83+
}
84+
pulldown_cmark::Tag::Item => {
85+
let indent = " ".repeat(list_depth.saturating_sub(1));
86+
output.push_str(&format!("{}∘ ", indent));
87+
}
88+
pulldown_cmark::Tag::BlockQuote => {
89+
output.push_str("\x1b[90m▐ \x1b[3;37m"); // Gray bar with italic text
90+
}
91+
pulldown_cmark::Tag::Link(_, _, _) => {
92+
output.push_str("\x1b[4;34m"); // Blue underlined
93+
}
94+
pulldown_cmark::Tag::Strikethrough => {
95+
output.push_str("\x1b[9m"); // Strikethrough
96+
}
97+
pulldown_cmark::Tag::Table(_) => {
98+
output.push('\n');
99+
output.push_str("\x1b[90m┌─ Table ─┐\x1b[0m\n");
100+
}
101+
pulldown_cmark::Tag::TableHead => {
102+
output.push_str("\x1b[1;93m"); // Bold yellow headers
103+
}
104+
pulldown_cmark::Tag::TableCell => {
105+
output.push_str("│ ");
106+
}
107+
_ => {}
108+
}
109+
}
110+
pulldown_cmark::Event::End(tag) => match tag {
111+
pulldown_cmark::Tag::Heading(level, _, _) => {
112+
output.push_str("\x1b[0m");
113+
match level {
114+
pulldown_cmark::HeadingLevel::H1 => {
115+
output.push('\n');
116+
output.push_str("\x1b[90m");
117+
output.push_str(&"═".repeat(60));
118+
output.push_str("\x1b[0m");
119+
}
120+
pulldown_cmark::HeadingLevel::H2 => {
121+
output.push('\n');
122+
output.push_str("\x1b[90m");
123+
output.push_str(&"─".repeat(40));
124+
output.push_str("\x1b[0m");
125+
}
126+
_ => {}
127+
}
128+
output.push('\n');
129+
}
130+
pulldown_cmark::Tag::Strong
131+
| pulldown_cmark::Tag::Emphasis
132+
| pulldown_cmark::Tag::Strikethrough
133+
| pulldown_cmark::Tag::Link(_, _, _) => {
134+
output.push_str("\x1b[0m");
135+
}
136+
pulldown_cmark::Tag::CodeBlock(_) => {
137+
in_code_block = false;
138+
139+
if !code_content.is_empty() {
140+
let highlighted = highlight_code_block(&code_content, &code_language);
141+
for line in highlighted.lines() {
142+
output.push_str("\x1b[90m│\x1b[0m ");
143+
output.push_str(line);
144+
output.push('\n');
145+
}
146+
}
147+
148+
output.push_str("\x1b[90m└");
149+
output.push_str(&"─".repeat(60));
150+
output.push_str("┘\x1b[0m\n");
151+
}
152+
pulldown_cmark::Tag::List(_) => {
153+
list_depth = list_depth.saturating_sub(1);
154+
if list_depth == 0 {
155+
output.push('\n');
156+
}
157+
}
158+
pulldown_cmark::Tag::Item => {
159+
output.push('\n');
160+
}
161+
pulldown_cmark::Tag::Paragraph => {
162+
if !in_code_block {
163+
output.push('\n');
164+
}
165+
}
166+
pulldown_cmark::Tag::BlockQuote => {
167+
output.push_str("\x1b[0m\n");
168+
}
169+
pulldown_cmark::Tag::TableHead => {
170+
output.push_str("\x1b[0m\n");
171+
output.push_str("\x1b[90m");
172+
output.push_str(&"─".repeat(60));
173+
output.push_str("\x1b[0m\n");
174+
}
175+
pulldown_cmark::Tag::TableRow => {
176+
output.push('\n');
177+
}
178+
_ => {}
179+
},
180+
pulldown_cmark::Event::Text(text) => {
181+
if in_code_block {
182+
code_content.push_str(&text);
183+
} else {
184+
output.push_str(&text);
185+
}
186+
}
187+
pulldown_cmark::Event::Code(code) => {
188+
output.push_str("\x1b[48;5;238;38;5;156m");
189+
output.push_str(" ");
190+
output.push_str(&code);
191+
output.push_str(" ");
192+
output.push_str("\x1b[0m");
193+
}
194+
pulldown_cmark::Event::SoftBreak => {
195+
if in_code_block {
196+
code_content.push('\n');
197+
} else {
198+
output.push(' ');
199+
}
200+
}
201+
pulldown_cmark::Event::HardBreak => {
202+
if in_code_block {
203+
code_content.push('\n');
204+
} else {
205+
output.push('\n');
206+
}
207+
}
208+
pulldown_cmark::Event::TaskListMarker(checked) => {
209+
if checked {
210+
output.push_str("\x1b[92m✓\x1b[0m ");
211+
} else {
212+
output.push_str("\x1b[90m○\x1b[0m ");
213+
}
214+
}
215+
_ => {}
216+
}
217+
}
218+
219+
output
220+
}
221+
222+
fn highlight_code_block(code: &str, language: &str) -> String {
223+
if !language.is_empty() {
224+
if let Ok(highlighted) = highlight_with_syntect(code, language) {
225+
return highlighted;
226+
}
227+
}
228+
229+
let mut output = String::new();
230+
for line in code.lines() {
231+
if line.trim().is_empty() {
232+
output.push('\n');
233+
continue;
234+
}
235+
236+
let colored_line = match language.to_lowercase().as_str() {
237+
"rust" | "rs" => highlight_rust_line(line),
238+
"python" | "py" => highlight_python_line(line),
239+
"javascript" | "js" | "typescript" | "ts" => highlight_js_line(line),
240+
"json" => highlight_json_line(line),
241+
"bash" | "sh" | "shell" => highlight_bash_line(line),
242+
_ => format!("\x1b[37m{}\x1b[0m", line), // Default white
243+
};
244+
245+
output.push_str(&colored_line);
246+
output.push('\n');
247+
}
248+
249+
output
250+
}
251+
252+
fn highlight_with_syntect(
253+
code: &str,
254+
language: &str,
255+
) -> Result<String, Box<dyn std::error::Error>> {
256+
use syntect::easy::HighlightLines;
257+
use syntect::highlighting::{Style, ThemeSet};
258+
use syntect::parsing::SyntaxSet;
259+
use syntect::util::{as_24_bit_terminal_escaped, LinesWithEndings};
260+
261+
let ss = SyntaxSet::load_defaults_newlines();
262+
let ts = ThemeSet::load_defaults();
263+
let theme = &ts.themes["base16-ocean.dark"];
264+
265+
let syntax = ss
266+
.find_syntax_by_extension(language)
267+
.or_else(|| ss.find_syntax_by_token(language))
268+
.unwrap_or_else(|| ss.find_syntax_plain_text());
269+
270+
let mut h = HighlightLines::new(syntax, theme);
271+
let mut output = String::new();
272+
273+
for line in LinesWithEndings::from(code) {
274+
let ranges: Vec<(Style, &str)> = h.highlight_line(line, &ss)?;
275+
let escaped = as_24_bit_terminal_escaped(&ranges[..], false);
276+
output.push_str(&escaped);
277+
}
278+
279+
Ok(output)
280+
}
281+
282+
fn highlight_rust_line(line: &str) -> String {
283+
let mut result = String::new();
284+
let keywords = [
285+
"fn", "let", "mut", "if", "else", "for", "while", "match", "struct", "enum", "impl", "use",
286+
"pub",
287+
];
288+
289+
for word in line.split_whitespace() {
290+
if keywords.contains(&word) {
291+
result.push_str(&format!("\x1b[94m{}\x1b[0m ", word)); // Blue
292+
} else if word.starts_with("//") {
293+
result.push_str(&format!("\x1b[90m{}\x1b[0m ", word)); // Gray
294+
} else if word.starts_with('"') && word.ends_with('"') {
295+
result.push_str(&format!("\x1b[92m{}\x1b[0m ", word)); // Green
296+
} else {
297+
result.push_str(&format!("\x1b[37m{}\x1b[0m ", word)); // White
298+
}
299+
}
300+
301+
result
302+
}
303+
304+
fn highlight_python_line(line: &str) -> String {
305+
let mut result = String::new();
306+
let keywords = [
307+
"def", "class", "if", "else", "elif", "for", "while", "import", "from", "return", "try",
308+
"except",
309+
];
310+
311+
for word in line.split_whitespace() {
312+
if keywords.contains(&word) {
313+
result.push_str(&format!("\x1b[94m{}\x1b[0m ", word)); // Blue
314+
} else if word.starts_with("#") {
315+
result.push_str(&format!("\x1b[90m{}\x1b[0m ", word)); // Gray
316+
} else if (word.starts_with('"') && word.ends_with('"'))
317+
|| (word.starts_with('\'') && word.ends_with('\''))
318+
{
319+
result.push_str(&format!("\x1b[92m{}\x1b[0m ", word)); // Green
320+
} else {
321+
result.push_str(&format!("\x1b[37m{}\x1b[0m ", word)); // White
322+
}
323+
}
324+
325+
result
326+
}
327+
328+
fn highlight_js_line(line: &str) -> String {
329+
let mut result = String::new();
330+
let keywords = [
331+
"function", "const", "let", "var", "if", "else", "for", "while", "return", "class",
332+
"import", "export",
333+
];
334+
335+
for word in line.split_whitespace() {
336+
if keywords.contains(&word) {
337+
result.push_str(&format!("\x1b[94m{}\x1b[0m ", word)); // Blue
338+
} else if word.starts_with("//") {
339+
result.push_str(&format!("\x1b[90m{}\x1b[0m ", word)); // Gray
340+
} else if word.starts_with('"') && word.ends_with('"') {
341+
result.push_str(&format!("\x1b[92m{}\x1b[0m ", word)); // Green
342+
} else {
343+
result.push_str(&format!("\x1b[37m{}\x1b[0m ", word)); // White
344+
}
345+
}
346+
347+
result
348+
}
349+
350+
fn highlight_json_line(line: &str) -> String {
351+
let trimmed = line.trim();
352+
if trimmed.starts_with('"') && trimmed.contains(':') {
353+
format!("\x1b[96m{}\x1b[0m", line) // Cyan for keys
354+
} else if trimmed.starts_with('"') {
355+
format!("\x1b[92m{}\x1b[0m", line) // Green for string values
356+
} else if trimmed
357+
.chars()
358+
.all(|c| c.is_ascii_digit() || c == '.' || c == '-')
359+
{
360+
format!("\x1b[93m{}\x1b[0m", line) // Yellow for numbers
361+
} else {
362+
format!("\x1b[37m{}\x1b[0m", line) // White for everything else
363+
}
364+
}
365+
366+
fn highlight_bash_line(line: &str) -> String {
367+
let mut result = String::new();
368+
let keywords = [
369+
"if", "then", "else", "fi", "for", "do", "done", "while", "case", "esac", "function",
370+
];
371+
372+
for word in line.split_whitespace() {
373+
if keywords.contains(&word) {
374+
result.push_str(&format!("\x1b[94m{}\x1b[0m ", word)); // Blue
375+
} else if word.starts_with("#") {
376+
result.push_str(&format!("\x1b[90m{}\x1b[0m ", word)); // Gray
377+
} else if word.starts_with('$') {
378+
result.push_str(&format!("\x1b[95m{}\x1b[0m ", word)); // Magenta for variables
379+
} else {
380+
result.push_str(&format!("\x1b[37m{}\x1b[0m ", word)); // White
381+
}
382+
}
383+
384+
result
24385
}
25386

26387
pub fn set_model(model_name: &str, config_dir: &Path) -> bool {

0 commit comments

Comments
 (0)