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