Skip to main content

mazer_core/
document.rs

1use crate::{parser::MathML, tokenizer::{Emphasis, LinkKind, MarkdownTag}};
2
3#[derive(Debug)]
4pub struct Document {
5    head: String,
6    title: String,
7    body: Vec<String>,
8}
9
10impl Document {
11    pub fn new(title: &str) -> Self {
12        Document {
13            head: format!("<!DOCTYPE html lang=\"en\">\n<html>\n<head>\n<meta charset=\"utf-8\">\n</head>\n"),
14            title: format!("<title> Mazer - {} </title>", title),
15            body: Vec::new(),
16        }
17    }
18
19    pub fn title(&self) -> String {
20        self.title.clone()
21    }
22
23    pub fn output(&self) -> String {
24        let mut body = String::new();
25
26        body.push_str(&self.head);
27        body.push_str(&self.title);
28
29        body.push_str("<body>\n");
30
31        for content in self.body.clone() {
32            body.push_str(&content);
33        }
34
35        body.push_str("</body>\n");
36
37        body
38    }
39
40    pub fn stylistic_output(&self) -> String {
41        let mut body = String::new();
42
43        let before = r#"
44        <!DOCTYPE html>
45<html lang="en-US">
46<head>
47    <meta charset="UTF-8">
48    <meta name="viewport" content="width=device-width, initial-scale=1.0">
49    <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 32 32%22><text y=%2232%22 font-size=%2232%22>๐Ÿ</text></svg>">
50    <style>
51        /* General styles */
52        h1, h2, h3 {
53            margin: 0;
54            padding: 0,
55        }
56
57        body {
58            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
59            margin: 0;
60            padding: 0;
61            transition: background-color 0.3s, color 0.3s;
62        }
63
64        /* Dark mode styles */
65        body.dark-mode {
66            background-color: #0d1117;
67            color: #c9d1d9;
68        }
69
70        body.dark-mode blockquote {
71            padding: 10px 20px;
72            margin: 20px 0;
73            border-left: 3px solid #8b949e;
74            background-color: #161b22;
75            color: #c9d1d9;
76        }
77
78        code.dark-mode {
79            background-color: #0d1117; 
80            color: #c9d1d9; 
81            padding: 10px 20px;
82            overflow-x: auto;
83            border-radius: 15px;
84        }
85
86        .inline-code {
87            font-family: monospace;
88            background-color: #0d1117;
89            color: #c9d1d9;
90            display: inline;
91            border-radius: 3px;
92            padding: 2px 4px;
93        }
94
95        img {
96            width: 600px;
97            height: 400px;
98        }
99
100        /* Light mode styles */
101        body.light-mode {
102            background-color: #ffffff;
103            color: #000000;
104        }
105
106        body.light-mode h1, body.light-mode h2, body.light-mode h3 {
107            color: #333;
108        }
109
110        body.light-mode blockquote {
111            padding: 10px 20px;
112            margin: 20px 0;
113            border-left: 3px solid #444d56;
114            background-color: #f6f8fa;
115            color: #24292e;
116        }
117
118        body.light-mode code {
119            background-color: #f9f9f9;
120            color: #333;
121        }
122
123        body.light-mode .inline-code {
124            background-color: #f9f9f9;
125            color: #333;
126            display: inline;
127        }
128
129        /* Toggle switch styles */
130        .toggle-container {
131            position: fixed;
132            top: 10px;
133            right: 10px;
134            z-index: 1000;
135            display: flex;
136            flex-direction: column;
137            align-items: flex-end;
138        }
139
140        .toggle-switch {
141            position: relative;
142            display: inline-block;
143            width: 60px;
144            height: 34px;
145            margin-bottom: 10px;
146            margin-right: 20px;
147        }
148
149        .toggle-switch input {
150            opacity: 0;
151            width: 0;
152            height: 0;
153        }
154
155        .slider {
156            position: absolute;
157            cursor: pointer;
158            top: 0;
159            left: 0;
160            right: 0;
161            bottom: 0;
162            background-color: #ccc;
163            transition: .4s;
164            border-radius: 34px;
165        }
166
167        .slider:before {
168            position: absolute;
169            content: "";
170            height: 26px;
171            width: 26px;
172            left: 4px;
173            bottom: 4px;
174            background-color: white;
175            transition: .4s;
176            border-radius: 50%;
177        }
178
179        input:checked + .slider {
180            background-color: #2196F3;
181        }
182
183        input:checked + .slider:before {
184            transform: translateX(26px);
185        }
186
187        .app-info {
188            color: #8b949e;
189            font-size: 0.9em;
190            text-align: center;
191            margin-right: 20px;
192        }
193
194        .content {
195            max-width: 1200px;
196            margin: 20px 50px;
197            padding: 0 20px;
198            line-height: 1.6;
199        }
200    </style>
201    <style>
202        .eval-result {
203          display: inline-block;
204          padding: 2px 6px;
205          border: 1px solid #888;
206          border-radius: 4px;
207          background-color: #f8f8f8;
208          cursor: pointer;
209          font-family: monospace;
210          transition: background-color 0.2s ease;
211        }
212
213        .eval-result.dark-mode {
214          background-color: #0d1117;
215          color: #ffffff;
216        }
217        
218        .eval-result:hover {
219          background-color: #e0e0e0;
220        }
221        
222        .hover-hint {
223          visibility: hidden;
224          position: absolute;
225          background: #333;
226          color: #fff;
227          padding: 4px 8px;
228          border-radius: 4px;
229          font-size: 0.9em;
230          white-space: nowrap;
231          transform: translateY(-30px);
232          opacity: 0;
233          transition: opacity 0.2s ease, transform 0.2s ease;
234        }
235        
236        .eval-container {
237          position: relative;
238          display: inline-block;
239        }
240      
241        .eval-container:hover .hover-hint {
242          visibility: visible;
243          opacity: 1;
244          transform: translateY(-35px);
245        }
246      </style>
247</head>
248<script>
249        function toggleTheme() {
250            const body = document.body;
251            body.classList.toggle('dark-mode');
252            body.classList.toggle('light-mode');
253        }
254
255        // Automatically set the toggle switch based on current theme
256        document.addEventListener('DOMContentLoaded', (event) => {
257            const isDarkMode = document.body.classList.contains('dark-mode');
258            document.getElementById('themeToggle').checked = !isDarkMode;
259        });
260</script>
261<body class="dark-mode">
262    <div class="toggle-container">
263        <label class="toggle-switch">
264            <input type="checkbox" id="themeToggle" onclick="toggleTheme()">
265            <span class="slider"></span>
266        </label>
267        <div class="app-info">
268            <div>๐ŸMazer๐ŸŒฐ</div>
269        </div>
270    </div>
271        "#;
272
273    body.push_str(before);
274
275    for content in self.body.clone() {
276        body.push_str(&content);
277    }
278
279    body.push_str("</body>\n");
280    body.push_str("</html>\n");
281
282    body
283    }
284
285    // appends to the body tag
286    pub fn append(&mut self, content: String) {
287        self.body.push(content);
288    }
289
290    pub fn append_void(&mut self, tag: &str) {
291        self.body.push(format!("<{} />", tag));
292    }
293
294    pub fn append_wrapped_with_attr(&mut self, tag: &str, attr: &str, content: &str) {
295        self.body
296            .push(format!("<{} {}>{}</{}>", tag, attr, content, tag));
297    }
298
299    pub fn append_wrapped(&mut self, tag: &str, content: &str) {
300        self.body.push(format!("<{}>{}</{}>", tag, content, tag));
301    }
302
303    pub fn append_newline(&mut self) {
304        self.body.push(String::from("<br>\n"));
305    }
306
307    pub fn append_math_ml(&mut self, content: &str) {
308        self.append_wrapped("math", content);
309    }
310
311    pub fn append_raw_math_ml(&mut self, content: MathML) {
312        self.append(content.to_string());
313    }
314
315    pub fn append_code(&mut self, content: &str) {
316        self.append_wrapped("code", &content);
317    }
318
319    pub fn append_evaluation(&mut self, expression: &str, result: &str) {
320        // <div class="eval-container">
321        //      <span class="eval-result">result</span>
322        //      <span class="hover-hint">expression</span>
323        // </div>
324        let content = format!(
325            "<div class=\"eval-container\"><span class=\"eval-result\">{}</span><span class=\"hover-hint\">{}</span></div>",
326            result, expression
327        );
328
329        self.append(content);
330    }
331
332    pub fn append_text(&mut self, emphasis: Option<Emphasis>, content: &str) {
333        if emphasis.is_none() {
334            self.append(content.to_string());
335        } else {
336            match emphasis.unwrap() {
337                Emphasis::Bold => {
338                    self.append_wrapped("b", content);
339                }
340                Emphasis::Italic => {
341                    self.append_wrapped("i", content);
342                }
343                Emphasis::Strikethrough => {
344                    self.append_wrapped("s", content);
345                }
346            }
347        }
348    }
349
350    pub fn add_markdown(&mut self, markdown: MarkdownTag) {
351        match markdown {
352            MarkdownTag::Header(level, content) => {
353                let header_count: usize = level.into();
354                self.append_wrapped(&format!("h{}", header_count), &content);
355            }
356            MarkdownTag::LineSeparator => {
357                self.append_void("hr");
358            }
359            MarkdownTag::Checkbox(state, content) => {
360                let checked = if state { "checked" } else { "" };
361
362                self.append_wrapped_with_attr(
363                    "input",
364                    &format!("type=\"checkbox\" disabled {}", checked),
365                    "",
366                );
367
368                if state {
369                    self.append_text(Some(Emphasis::Strikethrough), &content);
370                } else {
371                    self.append_text(None, &content);
372                }
373
374                self.append_newline();
375            }
376            MarkdownTag::BulletPoint(content) => {
377                self.append_wrapped("li", &content);
378            }
379            MarkdownTag::Blockquote(content) => {
380                self.append_wrapped("blockquote", &content);
381            }
382            MarkdownTag::CodeBlock(content) => {
383                let content = content.replace("\n", "<br>");
384                self.append_wrapped("pre", &content);
385            }
386            MarkdownTag::Link(kind, display, link) => match kind {
387                LinkKind::Image => {
388                    self.append_newline();
389                    self.append_wrapped_with_attr(
390                        "img",
391                        &format!("src=\"{}\" alt=\"{}\"", link, display),
392                        "",
393                    );
394                }
395                LinkKind::Hyperlink => {
396                    self.append_wrapped_with_attr(
397                        "a",
398                        &format!("href=\"{}\" target=\"_blank\" ", link),
399                        &display,
400                    );
401                }
402            },
403        }
404    }
405}