1use std::collections::VecDeque;
2
3use itertools::Itertools;
4use url::Url;
5
6#[derive(Clone, Debug, Default, clap::ValueEnum)]
8pub enum DocFormat {
9 #[default]
10 Markdown,
11 Text,
12 Html,
13}
14
15struct ModuleDoc {
17 name: String,
18 symbols: VecDeque<[String; 4]>,
19 selectors: VecDeque<[String; 2]>,
20}
21
22pub fn generate_docs(
28 module_names: &Option<Vec<String>>,
29 files: &Option<Vec<(String, String)>>,
30 format: &DocFormat,
31 include_builtin: bool,
32) -> Result<String, miette::Error> {
33 let has_files = files.as_ref().is_some_and(|f| !f.is_empty());
34 let has_modules = module_names.as_ref().is_some_and(|m| !m.is_empty());
35
36 let module_docs = if has_files || has_modules {
37 let mut docs = Vec::new();
38
39 if include_builtin {
40 let mut hir = mq_hir::Hir::default();
41 hir.add_code(None, "");
42 docs.push(ModuleDoc {
43 name: "Built-in".to_string(),
44 symbols: extract_symbols(&hir),
45 selectors: extract_selectors(&hir),
46 });
47 }
48
49 if let Some(file_contents) = files {
50 for (filename, content) in file_contents {
51 let mut hir = mq_hir::Hir::default();
52 hir.builtin.disabled = true;
53 let url = Url::parse(&format!("file:///{filename}")).ok();
54 hir.add_code(url, content);
55 docs.push(ModuleDoc {
56 name: filename.clone(),
57 symbols: extract_symbols(&hir),
58 selectors: extract_selectors(&hir),
59 });
60 }
61 }
62
63 if let Some(module_names) = module_names {
64 for module_name in module_names {
65 let mut hir = mq_hir::Hir::default();
66 hir.builtin.disabled = true;
67 hir.add_code(None, &format!("include \"{module_name}\""));
68 docs.push(ModuleDoc {
69 name: module_name.clone(),
70 symbols: extract_symbols(&hir),
71 selectors: extract_selectors(&hir),
72 });
73 }
74 }
75
76 docs
77 } else {
78 let mut hir = mq_hir::Hir::default();
79 hir.add_code(None, "");
80 vec![ModuleDoc {
81 name: "Built-in functions and macros".to_string(),
82 symbols: extract_symbols(&hir),
83 selectors: extract_selectors(&hir),
84 }]
85 };
86
87 match format {
88 DocFormat::Markdown => format_markdown(&module_docs),
89 DocFormat::Text => Ok(format_text(&module_docs)),
90 DocFormat::Html => Ok(format_html(&module_docs)),
91 }
92}
93
94fn extract_symbols(hir: &mq_hir::Hir) -> VecDeque<[String; 4]> {
96 hir.symbols()
97 .sorted_by_key(|(_, symbol)| symbol.value.clone())
98 .filter_map(|(_, symbol)| match symbol {
99 mq_hir::Symbol {
100 kind: mq_hir::SymbolKind::Function(params),
101 value: Some(value),
102 doc,
103 ..
104 }
105 | mq_hir::Symbol {
106 kind: mq_hir::SymbolKind::Macro(params),
107 value: Some(value),
108 doc,
109 ..
110 } if !symbol.is_internal_function() => {
111 let name = if symbol.is_deprecated() {
112 format!("~~`{}`~~", value)
113 } else {
114 format!("`{}`", value)
115 };
116 let description = doc.iter().map(|(_, d)| d.to_string()).join("\n");
117 let args = params.iter().map(|p| format!("`{}`", p.name)).join(", ");
118 let example = format!("{}({})", value, params.iter().map(|p| p.name.as_str()).join(", "));
119
120 Some([name, description, args, example])
121 }
122 _ => None,
123 })
124 .collect()
125}
126
127fn extract_selectors(hir: &mq_hir::Hir) -> VecDeque<[String; 2]> {
129 hir.symbols()
130 .sorted_by_key(|(_, symbol)| symbol.value.clone())
131 .filter_map(|(_, symbol)| match symbol {
132 mq_hir::Symbol {
133 kind: mq_hir::SymbolKind::Selector,
134 value: Some(value),
135 doc,
136 ..
137 } => {
138 let name = format!("`{}`", value);
139 let description = doc.iter().map(|(_, d)| d.to_string()).join("\n");
140 Some([name, description])
141 }
142 _ => None,
143 })
144 .collect()
145}
146
147fn format_markdown(module_docs: &[ModuleDoc]) -> Result<String, miette::Error> {
149 let all_symbols: VecDeque<_> = module_docs.iter().flat_map(|m| m.symbols.iter()).cloned().collect();
150 let all_selectors: VecDeque<_> = module_docs.iter().flat_map(|m| m.selectors.iter()).cloned().collect();
151
152 let mut doc_csv = all_symbols
153 .iter()
154 .map(|[name, description, args, example]| {
155 mq_lang::RuntimeValue::String([name, description, args, example].into_iter().join("\t"))
156 })
157 .collect::<VecDeque<_>>();
158
159 doc_csv.push_front(mq_lang::RuntimeValue::String(
160 ["Function Name", "Description", "Parameters", "Example"]
161 .iter()
162 .join("\t"),
163 ));
164
165 let mut engine = mq_lang::DefaultEngine::default();
166 engine.load_builtin_module();
167
168 let doc_values = engine
169 .eval(
170 r#"include "csv" | tsv_parse(false) | csv_to_markdown_table()"#,
171 mq_lang::raw_input(&doc_csv.iter().join("\n")).into_iter(),
172 )
173 .map_err(|e| *e)?;
174
175 let mut result = doc_values.values().iter().map(|v| v.to_string()).join("\n");
176
177 if !all_selectors.is_empty() {
178 let mut selector_csv = all_selectors
179 .iter()
180 .map(|[name, description]| {
181 mq_lang::RuntimeValue::String([name.as_str(), description.as_str()].into_iter().join("\t"))
182 })
183 .collect::<VecDeque<_>>();
184
185 selector_csv.push_front(mq_lang::RuntimeValue::String(
186 ["Selector", "Description"].iter().join("\t"),
187 ));
188
189 let mut engine = mq_lang::DefaultEngine::default();
190 engine.load_builtin_module();
191
192 let selector_values = engine
193 .eval(
194 r#"include "csv" | tsv_parse(false) | csv_to_markdown_table()"#,
195 mq_lang::raw_input(&selector_csv.iter().join("\n")).into_iter(),
196 )
197 .map_err(|e| *e)?;
198
199 result.push_str("\n\n## Selectors\n\n");
200 result.push_str(&selector_values.values().iter().map(|v| v.to_string()).join("\n"));
201 }
202
203 Ok(result)
204}
205
206fn format_text(module_docs: &[ModuleDoc]) -> String {
208 let functions = module_docs
209 .iter()
210 .flat_map(|m| m.symbols.iter())
211 .map(|[name, description, args, _]| {
212 let name = name.replace('`', "");
213 let args = args.replace('`', "");
214 format!("# {description}\ndef {name}({args})")
215 })
216 .join("\n\n");
217
218 let selectors = module_docs
219 .iter()
220 .flat_map(|m| m.selectors.iter())
221 .map(|[name, description]| {
222 let name = name.replace('`', "");
223 format!("# {description}\nselector {name}")
224 })
225 .join("\n\n");
226
227 if selectors.is_empty() {
228 functions
229 } else {
230 format!("{functions}\n\n{selectors}")
231 }
232}
233
234fn build_table_rows(symbols: &VecDeque<[String; 4]>) -> String {
236 symbols
237 .iter()
238 .map(|[name, description, args, example]| {
239 let name_html = if name.starts_with("~~") {
240 let inner = name.trim_start_matches("~~`").trim_end_matches("`~~");
241 format!("<del><code>{}</code></del>", escape_html(inner))
242 } else {
243 let inner = name.trim_start_matches('`').trim_end_matches('`');
244 format!("<code>{}</code>", escape_html(inner))
245 };
246 let args_html = args
247 .split(", ")
248 .filter(|a| !a.is_empty())
249 .map(|a| {
250 let inner = a.trim_start_matches('`').trim_end_matches('`');
251 format!("<code>{}</code>", escape_html(inner))
252 })
253 .join(", ");
254 let desc_html = escape_html(description);
255 let example_html = escape_html(example);
256
257 format!(
258 " <tr>\n\
259 \x20 <td>{name_html}</td>\n\
260 \x20 <td>{desc_html}</td>\n\
261 \x20 <td>{args_html}</td>\n\
262 \x20 <td><code>{example_html}</code></td>\n\
263 \x20 </tr>"
264 )
265 })
266 .join("\n")
267}
268
269fn build_selector_table_rows(selectors: &VecDeque<[String; 2]>) -> String {
271 selectors
272 .iter()
273 .map(|[name, description]| {
274 let inner = name.trim_start_matches('`').trim_end_matches('`');
275 let name_html = format!("<code>{}</code>", escape_html(inner));
276 let desc_html = escape_html(description);
277
278 format!(
279 " <tr>\n\
280 \x20 <td>{name_html}</td>\n\
281 \x20 <td>{desc_html}</td>\n\
282 \x20 </tr>"
283 )
284 })
285 .join("\n")
286}
287
288fn build_module_page(id: &str, symbols: &VecDeque<[String; 4]>, active: bool) -> String {
290 let rows = build_table_rows(symbols);
291 let count = symbols.len();
292 let active_class = if active { " active" } else { "" };
293 format!(
294 "<div class=\"module-page{active_class}\" id=\"{id}\">\n\
295 \x20 <div class=\"search-box\">\n\
296 \x20 <svg class=\"search-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"/></svg>\n\
297 \x20 <input type=\"text\" class=\"search-input\" placeholder=\"Filter functions...\" />\n\
298 \x20 </div>\n\
299 \x20 <p class=\"count\"><span class=\"count-num\">{count}</span> functions</p>\n\
300 \x20 <table>\n\
301 \x20 <thead><tr><th>Function</th><th>Description</th><th>Parameters</th><th>Example</th></tr></thead>\n\
302 \x20 <tbody>\n{rows}\n\x20 </tbody>\n\
303 \x20 </table>\n\
304 </div>"
305 )
306}
307
308fn build_selector_page(id: &str, selectors: &VecDeque<[String; 2]>, active: bool) -> String {
310 let rows = build_selector_table_rows(selectors);
311 let count = selectors.len();
312 let active_class = if active { " active" } else { "" };
313 format!(
314 "<div class=\"module-page{active_class}\" id=\"{id}\">\n\
315 \x20 <div class=\"search-box\">\n\
316 \x20 <svg class=\"search-icon\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"/></svg>\n\
317 \x20 <input type=\"text\" class=\"search-input\" placeholder=\"Filter selectors...\" />\n\
318 \x20 </div>\n\
319 \x20 <p class=\"count\"><span class=\"count-num\">{count}</span> selectors</p>\n\
320 \x20 <table>\n\
321 \x20 <thead><tr><th>Selector</th><th>Description</th></tr></thead>\n\
322 \x20 <tbody>\n{rows}\n\x20 </tbody>\n\
323 \x20 </table>\n\
324 </div>"
325 )
326}
327
328fn format_html(module_docs: &[ModuleDoc]) -> String {
330 let has_multiple = module_docs.len() > 1;
331 let has_selectors = module_docs.iter().any(|m| !m.selectors.is_empty());
332
333 let sidebar_items = if has_multiple {
335 let all_count: usize = module_docs.iter().map(|m| m.symbols.len()).sum();
336 let all_icon = svg_icon(
337 "<rect x=\"3\" y=\"3\" width=\"7\" height=\"7\"/>\
338 <rect x=\"14\" y=\"3\" width=\"7\" height=\"7\"/>\
339 <rect x=\"3\" y=\"14\" width=\"7\" height=\"7\"/>\
340 <rect x=\"14\" y=\"14\" width=\"7\" height=\"7\"/>",
341 );
342 let mut items = format!(
343 "<a class=\"sidebar-link active\" href=\"#\" data-module=\"mod-all\">\
344 <span class=\"sidebar-icon\">{all_icon}</span>\
345 <span class=\"sidebar-label\">All</span>\
346 <span class=\"sidebar-count\">{all_count}</span></a>\n"
347 );
348 for (i, m) in module_docs.iter().enumerate() {
349 let name = escape_html(&m.name);
350 let count = m.symbols.len();
351 let icon = module_icon(&m.name);
352 items.push_str(&format!(
353 "<a class=\"sidebar-link\" href=\"#\" data-module=\"mod-{i}\">\
354 <span class=\"sidebar-icon\">{icon}</span>\
355 <span class=\"sidebar-label\">{name}</span>\
356 <span class=\"sidebar-count\">{count}</span></a>\n"
357 ));
358 }
359 items
360 } else {
361 let m = &module_docs[0];
362 let name = escape_html(&m.name);
363 let count = m.symbols.len();
364 let icon = module_icon(&m.name);
365 format!(
366 "<a class=\"sidebar-link active\" href=\"#\" data-module=\"mod-all\">\
367 <span class=\"sidebar-icon\">{icon}</span>\
368 <span class=\"sidebar-label\">{name}</span>\
369 <span class=\"sidebar-count\">{count}</span></a>\n"
370 )
371 };
372
373 let selector_sidebar_items = if has_selectors {
375 let mut items = String::new();
376 for (i, m) in module_docs.iter().enumerate() {
377 if m.selectors.is_empty() {
378 continue;
379 }
380 let name = escape_html(&m.name);
381 let count = m.selectors.len();
382 let icon = selector_icon();
383 items.push_str(&format!(
384 "<a class=\"sidebar-link\" href=\"#\" data-module=\"sel-{i}\">\
385 <span class=\"sidebar-icon\">{icon}</span>\
386 <span class=\"sidebar-label\">{name}</span>\
387 <span class=\"sidebar-count\">{count}</span></a>\n"
388 ));
389 }
390 items
391 } else {
392 String::new()
393 };
394
395 let mut pages = if has_multiple {
397 let all_symbols: VecDeque<_> = module_docs.iter().flat_map(|m| m.symbols.iter()).cloned().collect();
398 let mut pages_html = build_module_page("mod-all", &all_symbols, true);
399 for (i, m) in module_docs.iter().enumerate() {
400 pages_html.push('\n');
401 pages_html.push_str(&build_module_page(&format!("mod-{i}"), &m.symbols, false));
402 }
403 pages_html
404 } else {
405 build_module_page("mod-all", &module_docs[0].symbols, true)
406 };
407
408 if has_selectors {
410 for (i, m) in module_docs.iter().enumerate() {
411 if m.selectors.is_empty() {
412 continue;
413 }
414 pages.push('\n');
415 pages.push_str(&build_selector_page(&format!("sel-{i}"), &m.selectors, false));
416 }
417 }
418
419 let selector_section = if has_selectors {
421 format!(
422 " <nav class=\"sidebar-section\">\n\
423 \x20 <div class=\"sidebar-section-title\">Selectors</div>\n\
424 {selector_sidebar_items}\
425 \x20 </nav>\n"
426 )
427 } else {
428 String::new()
429 };
430
431 format!(
432 r#"<!DOCTYPE html>
433<html lang="en">
434 <head>
435 <meta charset="UTF-8" />
436 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
437 <title>mq - Function Reference</title>
438 <link rel="preconnect" href="https://fonts.googleapis.com" />
439 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
440 <link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet" />
441 <style>
442 :root {{
443 --bg-primary: #2a3444;
444 --bg-secondary: #232d3b;
445 --bg-tertiary: #3d4a5c;
446 --text-primary: #e2e8f0;
447 --text-secondary: #cbd5e1;
448 --text-muted: #94a3b8;
449 --accent-primary: #67b8e3;
450 --accent-secondary: #4fc3f7;
451 --border-default: #4a5568;
452 --border-muted: #374151;
453 --code-bg: #1e293b;
454 --code-bg-inline: #374151;
455 --code-color: #e2e8f0;
456 --sidebar-width: 260px;
457 }}
458
459 * {{ margin: 0; padding: 0; box-sizing: border-box; }}
460 html {{ height: 100%; scroll-behavior: smooth; }}
461
462 body {{
463 background-color: var(--bg-primary);
464 color: var(--text-primary);
465 font-family: "Montserrat", -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif;
466 font-weight: 400;
467 line-height: 1.6;
468 min-height: 100vh;
469 }}
470
471 /* ---- Layout ---- */
472 .layout {{
473 display: flex;
474 min-height: 100vh;
475 }}
476
477 .sidebar {{
478 background-color: var(--bg-secondary);
479 border-right: 1px solid var(--border-default);
480 display: flex;
481 flex-direction: column;
482 height: 100vh;
483 overflow-y: auto;
484 position: fixed;
485 top: 0;
486 left: 0;
487 width: var(--sidebar-width);
488 z-index: 50;
489 }}
490
491 .sidebar-header {{
492 border-bottom: 1px solid var(--border-default);
493 padding: 1.25rem 1.25rem 1rem;
494 }}
495
496 .sidebar-header h1 {{
497 color: var(--accent-primary);
498 font-size: 1.3rem;
499 font-weight: 700;
500 letter-spacing: -0.3px;
501 }}
502
503 .sidebar-header p {{
504 color: var(--text-muted);
505 font-size: 0.75rem;
506 margin-top: 0.2rem;
507 }}
508
509 .sidebar-section {{
510 padding: 0.75rem 0;
511 }}
512
513 .sidebar-section-title {{
514 color: var(--text-muted);
515 font-size: 0.7rem;
516 font-weight: 600;
517 letter-spacing: 0.8px;
518 padding: 0.25rem 1.25rem 0.5rem;
519 text-transform: uppercase;
520 }}
521
522 .sidebar-link {{
523 align-items: center;
524 border-left: 3px solid transparent;
525 color: var(--text-secondary);
526 cursor: pointer;
527 display: flex;
528 font-size: 0.85rem;
529 gap: 0.6rem;
530 padding: 0.5rem 1.25rem;
531 text-decoration: none;
532 transition: all 0.15s;
533 }}
534
535 .sidebar-link:hover {{
536 background-color: rgba(103, 184, 227, 0.06);
537 color: var(--text-primary);
538 }}
539
540 .sidebar-link.active {{
541 background-color: rgba(103, 184, 227, 0.1);
542 border-left-color: var(--accent-primary);
543 color: var(--accent-primary);
544 font-weight: 600;
545 }}
546
547 .sidebar-icon {{
548 display: flex;
549 align-items: center;
550 flex-shrink: 0;
551 }}
552
553 .sidebar-icon svg {{
554 height: 16px;
555 width: 16px;
556 }}
557
558 .sidebar-label {{
559 flex: 1;
560 overflow: hidden;
561 text-overflow: ellipsis;
562 white-space: nowrap;
563 }}
564
565 .sidebar-count {{
566 background-color: var(--bg-tertiary);
567 border-radius: 10px;
568 color: var(--text-muted);
569 font-size: 0.7rem;
570 font-weight: 600;
571 min-width: 1.6rem;
572 padding: 0.1rem 0.45rem;
573 text-align: center;
574 }}
575
576 .sidebar-link.active .sidebar-count {{
577 background-color: rgba(103, 184, 227, 0.15);
578 color: var(--accent-primary);
579 }}
580
581 .content {{
582 flex: 1;
583 margin-left: var(--sidebar-width);
584 padding: 2rem 2.5rem;
585 max-width: calc(100% - var(--sidebar-width));
586 }}
587
588 /* ---- Mobile sidebar toggle ---- */
589 .sidebar-toggle {{
590 background-color: var(--bg-tertiary);
591 border: 1px solid var(--border-default);
592 border-radius: 8px;
593 color: var(--text-primary);
594 cursor: pointer;
595 display: none;
596 left: 1rem;
597 padding: 0.5rem;
598 position: fixed;
599 top: 1rem;
600 z-index: 60;
601 }}
602
603 .sidebar-toggle svg {{
604 display: block;
605 height: 20px;
606 width: 20px;
607 }}
608
609 .sidebar-overlay {{
610 background-color: rgba(0, 0, 0, 0.5);
611 display: none;
612 inset: 0;
613 position: fixed;
614 z-index: 40;
615 }}
616
617 /* ---- Pages ---- */
618 .module-page {{ display: none; }}
619 .module-page.active {{ display: block; }}
620
621 .page-title {{
622 color: var(--text-primary);
623 font-size: 1.5rem;
624 font-weight: 700;
625 margin-bottom: 1.5rem;
626 }}
627
628 .search-box {{
629 margin-bottom: 1.5rem;
630 position: relative;
631 }}
632
633 .search-box input {{
634 background-color: var(--bg-tertiary);
635 border: 1px solid var(--border-default);
636 border-radius: 8px;
637 color: var(--text-primary);
638 font-family: inherit;
639 font-size: 0.95rem;
640 padding: 0.75rem 1rem 0.75rem 2.5rem;
641 width: 100%;
642 transition: border-color 0.2s;
643 }}
644
645 .search-box input:focus {{
646 border-color: var(--accent-primary);
647 outline: none;
648 }}
649
650 .search-box .search-icon {{
651 color: var(--text-muted);
652 height: 16px;
653 left: 0.85rem;
654 pointer-events: none;
655 position: absolute;
656 top: 50%;
657 transform: translateY(-50%);
658 width: 16px;
659 }}
660
661 .count {{
662 color: var(--text-muted);
663 font-size: 0.85rem;
664 margin-bottom: 1rem;
665 }}
666
667 table {{ border-collapse: collapse; width: 100%; }}
668
669 thead th {{
670 background-color: var(--bg-tertiary);
671 border-bottom: 2px solid var(--accent-primary);
672 color: var(--accent-primary);
673 font-size: 0.8rem;
674 font-weight: 600;
675 letter-spacing: 0.5px;
676 padding: 0.75rem 1rem;
677 position: sticky;
678 text-align: left;
679 text-transform: uppercase;
680 top: 0;
681 z-index: 5;
682 }}
683
684 tbody tr {{
685 border-bottom: 1px solid var(--border-muted);
686 cursor: pointer;
687 transition: background-color 0.15s;
688 }}
689
690 tbody tr:hover {{ background-color: var(--bg-tertiary); }}
691
692 tbody td {{
693 font-size: 0.9rem;
694 padding: 0.65rem 1rem;
695 vertical-align: top;
696 }}
697
698 tbody td:first-child {{ white-space: nowrap; }}
699
700 code {{
701 background-color: var(--code-bg-inline);
702 border-radius: 4px;
703 color: var(--code-color);
704 font-family: "Consolas", "Monaco", "Courier New", monospace;
705 font-size: 0.85em;
706 padding: 0.15em 0.4em;
707 }}
708
709 del code {{ opacity: 0.6; }}
710
711 footer {{
712 border-top: 1px solid var(--border-default);
713 margin-left: var(--sidebar-width);
714 padding: 1.5rem 2.5rem;
715 }}
716
717 footer p {{
718 color: var(--text-muted);
719 font-size: 0.85rem;
720 }}
721
722 footer a {{
723 color: var(--accent-primary);
724 text-decoration: none;
725 }}
726
727 footer a:hover {{
728 color: var(--accent-secondary);
729 text-decoration: underline;
730 }}
731
732 @media (max-width: 768px) {{
733 .sidebar {{
734 transform: translateX(-100%);
735 transition: transform 0.25s ease;
736 }}
737
738 .sidebar.open {{
739 transform: translateX(0);
740 }}
741
742 .sidebar-toggle {{
743 display: block;
744 }}
745
746 .sidebar-overlay.open {{
747 display: block;
748 }}
749
750 .content {{
751 margin-left: 0;
752 max-width: 100%;
753 padding: 1.5rem 1rem;
754 padding-top: 4rem;
755 }}
756
757 footer {{
758 margin-left: 0;
759 padding: 1.5rem 1rem;
760 }}
761
762 table {{ display: block; overflow-x: auto; }}
763
764 tbody td, thead th {{
765 font-size: 0.8rem;
766 padding: 0.6rem 0.75rem;
767 }}
768 }}
769 </style>
770 </head>
771 <body>
772 <button class="sidebar-toggle" id="sidebarToggle">
773 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
774 <line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/>
775 </svg>
776 </button>
777 <div class="sidebar-overlay" id="sidebarOverlay"></div>
778
779 <div class="layout">
780 <aside class="sidebar" id="sidebar">
781 <div class="sidebar-header">
782 <h1>mq</h1>
783 <p>Function Reference</p>
784 </div>
785 <nav class="sidebar-section">
786 <div class="sidebar-section-title">Modules</div>
787{sidebar_items}
788 </nav>
789{selector_section}
790 </aside>
791
792 <div class="content">
793{pages}
794 </div>
795 </div>
796
797 <footer>
798 <p>Generated by <a href="https://github.com/harehare/mq">mq</a></p>
799 </footer>
800
801 <script>
802 // Sidebar navigation
803 document.querySelectorAll(".sidebar-link").forEach(function (link) {{
804 link.addEventListener("click", function (e) {{
805 e.preventDefault();
806 document.querySelectorAll(".sidebar-link").forEach(function (l) {{
807 l.classList.remove("active");
808 }});
809 link.classList.add("active");
810
811 var target = link.getAttribute("data-module");
812 document.querySelectorAll(".module-page").forEach(function (page) {{
813 page.classList.toggle("active", page.id === target);
814 }});
815
816 // Close mobile sidebar
817 document.getElementById("sidebar").classList.remove("open");
818 document.getElementById("sidebarOverlay").classList.remove("open");
819 }});
820 }});
821
822 // Search filter
823 document.querySelectorAll(".search-input").forEach(function (input) {{
824 input.addEventListener("input", function () {{
825 var page = input.closest(".module-page");
826 var q = input.value.toLowerCase();
827 var rows = page.querySelectorAll("tbody tr");
828 var visible = 0;
829 rows.forEach(function (row) {{
830 var text = row.textContent.toLowerCase();
831 var show = text.includes(q);
832 row.style.display = show ? "" : "none";
833 if (show) visible++;
834 }});
835 page.querySelector(".count-num").textContent = visible;
836 }});
837 }});
838
839 // Mobile sidebar toggle
840 document.getElementById("sidebarToggle").addEventListener("click", function () {{
841 document.getElementById("sidebar").classList.toggle("open");
842 document.getElementById("sidebarOverlay").classList.toggle("open");
843 }});
844 document.getElementById("sidebarOverlay").addEventListener("click", function () {{
845 document.getElementById("sidebar").classList.remove("open");
846 document.getElementById("sidebarOverlay").classList.remove("open");
847 }});
848 </script>
849 </body>
850</html>"#,
851 )
852}
853
854fn svg_icon(inner: &str) -> String {
856 format!(
857 "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" \
858 stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">{inner}</svg>"
859 )
860}
861
862fn module_icon(name: &str) -> String {
864 if name.starts_with("Built-in") {
865 svg_icon(
867 "<path d=\"M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z\"/>\
868 <polyline points=\"3.27 6.96 12 12.01 20.73 6.96\"/>\
869 <line x1=\"12\" y1=\"22.08\" x2=\"12\" y2=\"12\"/>",
870 )
871 } else {
872 svg_icon(
874 "<line x1=\"16.5\" y1=\"9.4\" x2=\"7.5\" y2=\"4.21\"/>\
875 <path d=\"M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z\"/>\
876 <polyline points=\"3.27 6.96 12 12.01 20.73 6.96\"/>\
877 <line x1=\"12\" y1=\"22.08\" x2=\"12\" y2=\"12\"/>",
878 )
879 }
880}
881
882fn selector_icon() -> String {
884 svg_icon(
886 "<circle cx=\"12\" cy=\"12\" r=\"10\"/>\
887 <line x1=\"22\" y1=\"12\" x2=\"18\" y2=\"12\"/>\
888 <line x1=\"6\" y1=\"12\" x2=\"2\" y2=\"12\"/>\
889 <line x1=\"12\" y1=\"6\" x2=\"12\" y2=\"2\"/>\
890 <line x1=\"12\" y1=\"22\" x2=\"12\" y2=\"18\"/>",
891 )
892}
893
894fn escape_html(s: &str) -> String {
896 s.replace('&', "&")
897 .replace('<', "<")
898 .replace('>', ">")
899 .replace('"', """)
900}