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 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 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}