typstify_generator/
static_assets.rs1use std::{fs, path::Path};
7
8use thiserror::Error;
9
10#[derive(Debug, Error)]
12pub enum StaticAssetError {
13 #[error("IO error: {0}")]
15 Io(#[from] std::io::Error),
16}
17
18pub type Result<T> = std::result::Result<T, StaticAssetError>;
20
21pub fn generate_static_assets(output_dir: &Path) -> Result<()> {
25 let assets_dir = output_dir.join("assets");
27 fs::create_dir_all(&assets_dir)?;
28
29 fs::write(assets_dir.join("style.css"), DEFAULT_CSS)?;
31
32 fs::write(assets_dir.join("main.js"), DEFAULT_JS)?;
34
35 Ok(())
36}
37
38pub const DEFAULT_CSS: &str = r#"
39:root {
40 --color-primary: #d4760a;
41 --color-primary-deep: #a85c08;
42 --color-accent: #1e5a8a;
43 --color-bg: #ffffff;
44 --color-surface: #f6f5f2;
45 --color-ink: #1a1a1e;
46 --color-muted: #6b6b73;
47 --color-border: #e2e1dd;
48 --color-primary-bg: #fdf3ea;
49 --color-header-bg: #ffffff;
50 --color-success: #2d8a4e;
51 --color-error: #c4382a;
52 --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, "Inter", Roboto, sans-serif;
53 --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
54 color-scheme: light;
55}
56
57[data-theme="dark"] {
58 --color-bg: #131316;
59 --color-surface: #1e1e23;
60 --color-ink: #e8e7e3;
61 --color-muted: #b0b0b8;
62 --color-border: #2c2c32;
63 --color-primary-bg: #2a1f14;
64 --color-header-bg: #131316;
65 color-scheme: dark;
66}
67
68*, *::before, *::after {
69 box-sizing: border-box;
70 margin: 0;
71 padding: 0;
72}
73
74html {
75 font-size: 16px;
76 -webkit-font-smoothing: antialiased;
77 -moz-osx-font-smoothing: grayscale;
78 text-rendering: optimizeLegibility;
79}
80
81body {
82 font-family: var(--font-sans);
83 font-weight: 400;
84 line-height: 1.6;
85 color: var(--color-ink);
86 background-color: var(--color-bg);
87 min-height: 100vh;
88}
89
90a {
91 color: var(--color-primary);
92 text-decoration: none;
93}
94
95a:hover {
96 color: var(--color-primary-deep);
97}
98
99.container {
100 width: 100%;
101 max-width: 42rem;
102 margin: 0 auto;
103 padding: 0 1.25rem;
104}
105
106@media (min-width: 48rem) {
107 .container {
108 max-width: 48rem;
109 padding: 0 2rem;
110 }
111}
112
113header {
114 position: sticky;
115 top: 0;
116 z-index: 40;
117 background-color: var(--color-header-bg);
118 border-bottom: 1px solid var(--color-border);
119}
120
121nav {
122 display: flex;
123 align-items: center;
124 justify-content: space-between;
125 padding: 0.875rem 0;
126 gap: 0.75rem;
127 flex-wrap: wrap;
128}
129
130.site-title {
131 font-size: 1.125rem;
132 font-weight: 700;
133 color: var(--color-ink);
134 white-space: nowrap;
135}
136
137.site-title:hover {
138 color: var(--color-primary);
139}
140
141.nav-links {
142 display: flex;
143 align-items: center;
144 gap: 0.125rem;
145 flex-wrap: wrap;
146}
147
148.nav-links > a {
149 padding: 0.375rem 0.625rem;
150 font-size: 0.875rem;
151 font-weight: 500;
152 color: var(--color-muted);
153 border-radius: 0.375rem;
154}
155
156.nav-links > a:hover {
157 color: var(--color-ink);
158 background-color: var(--color-surface);
159}
160
161.nav-actions {
162 display: flex;
163 align-items: center;
164 gap: 0.375rem;
165 margin-left: auto;
166}
167
168.lang-switcher {
169 position: relative;
170 cursor: pointer;
171 user-select: none;
172}
173
174.lang-switcher > .lang-code {
175 display: flex;
176 align-items: center;
177 justify-content: center;
178 padding: 0.375rem 0.5rem;
179 font-size: 0.75rem;
180 font-weight: 600;
181 color: var(--color-muted);
182 border: 1px solid var(--color-border);
183 border-radius: 0.375rem;
184 background-color: var(--color-bg);
185 min-width: 2.125rem;
186 height: 2rem;
187}
188
189.lang-switcher:hover > .lang-code,
190.lang-switcher.open > .lang-code {
191 color: var(--color-ink);
192 border-color: var(--color-primary);
193}
194
195.lang-dropdown {
196 position: absolute;
197 top: calc(100% + 0.25rem);
198 right: 0;
199 min-width: 7.5rem;
200 background-color: var(--color-bg);
201 border: 1px solid var(--color-border);
202 border-radius: 0.5rem;
203 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
204 padding: 0.25rem;
205 display: none;
206 z-index: 50;
207}
208
209.lang-switcher.open .lang-dropdown {
210 display: block;
211}
212
213.lang-option {
214 display: block;
215 padding: 0.4375rem 0.625rem;
216 font-size: 0.875rem;
217 color: var(--color-ink);
218 border-radius: 0.375rem;
219}
220
221.lang-option:hover {
222 background-color: var(--color-surface);
223}
224
225.lang-option.active {
226 color: var(--color-primary);
227 font-weight: 600;
228 background-color: var(--color-primary-bg);
229}
230
231.theme-toggle {
232 display: flex;
233 align-items: center;
234 justify-content: center;
235 width: 2rem;
236 height: 2rem;
237 padding: 0;
238 color: var(--color-muted);
239 background: var(--color-bg);
240 border: 1px solid var(--color-border);
241 border-radius: 0.375rem;
242 cursor: pointer;
243}
244
245.theme-toggle:hover {
246 color: var(--color-ink);
247 border-color: var(--color-primary);
248}
249
250.theme-toggle svg {
251 width: 1rem;
252 height: 1rem;
253}
254
255.theme-toggle .icon-sun { display: none; }
256.theme-toggle .icon-moon { display: block; }
257
258[data-theme="dark"] .theme-toggle .icon-sun { display: block; }
259[data-theme="dark"] .theme-toggle .icon-moon { display: none; }
260
261main {
262 padding: 2rem 0 3rem;
263 min-height: calc(100vh - 12rem);
264}
265
266article h1 {
267 font-size: 1.875rem;
268 font-weight: 700;
269 margin-bottom: 0.5rem;
270 line-height: 1.3;
271}
272
273article.page h1,
274section > h1 {
275 font-size: 1.875rem;
276 font-weight: 700;
277 margin-bottom: 2rem;
278 line-height: 1.3;
279}
280
281article.post header {
282 position: static;
283 background: none;
284 border: none;
285 backdrop-filter: none;
286 padding: 0;
287 margin-bottom: 2rem;
288}
289
290article.post header h1 {
291 margin-bottom: 0.5rem;
292}
293
294article.post header time {
295 display: block;
296 font-size: 0.875rem;
297 color: var(--color-muted);
298 margin-bottom: 0.75rem;
299}
300
301article .content,
302section .content {
303 font-size: 1rem;
304 line-height: 1.75;
305}
306
307article .content > *:last-child,
308section .content > *:last-child {
309 margin-bottom: 0;
310}
311
312footer {
313 border-top: 1px solid var(--color-border);
314 padding: 2rem 0;
315 color: var(--color-muted);
316 font-size: 0.875rem;
317 text-align: center;
318}
319
320footer a {
321 color: var(--color-muted);
322}
323
324footer a:hover {
325 color: var(--color-primary);
326}
327
328.post-list ul {
329 list-style: none;
330 padding: 0;
331 margin: 0;
332}
333
334.post-item {
335 padding: 1.25rem 0;
336 border-bottom: 1px solid var(--color-border);
337}
338
339.post-item:last-child {
340 border-bottom: none;
341}
342
343.post-item-header {
344 display: flex;
345 align-items: baseline;
346 gap: 1rem;
347 flex-wrap: wrap;
348 margin-bottom: 0.25rem;
349}
350
351.post-title {
352 font-size: 1.125rem;
353 font-weight: 600;
354 color: var(--color-ink);
355}
356
357.post-title:hover {
358 color: var(--color-primary);
359}
360
361.post-item time {
362 font-size: 0.8125rem;
363 color: var(--color-muted);
364 font-family: var(--font-mono);
365 white-space: nowrap;
366}
367
368.post-description {
369 color: var(--color-muted);
370 font-size: 0.9375rem;
371 margin: 0;
372 line-height: 1.6;
373}
374
375.tags {
376 display: flex;
377 flex-wrap: wrap;
378 gap: 0.5rem;
379 margin-top: 0.75rem;
380}
381
382.tags a {
383 font-size: 0.75rem;
384 padding: 0.2rem 0.6rem;
385 background-color: var(--color-surface);
386 color: var(--color-muted);
387 border-radius: 9999px;
388 transition: color 0.15s ease, background-color 0.15s ease;
389}
390
391.tags a:hover {
392 color: var(--color-primary);
393 background-color: var(--color-primary-bg);
394}
395
396.taxonomy-index h1 {
397 font-size: 1.5rem;
398 font-weight: 700;
399 margin-bottom: 1.5rem;
400}
401
402.tags-cloud {
403 display: flex;
404 flex-wrap: wrap;
405 gap: 0.625rem;
406}
407
408.tag-item {
409 display: inline-flex;
410 align-items: center;
411 gap: 0.375rem;
412 padding: 0.5rem 0.875rem;
413 background-color: var(--color-surface);
414 border-radius: 0.5rem;
415 font-size: 0.875rem;
416 color: var(--color-ink);
417}
418
419.tag-item:hover {
420 color: var(--color-primary);
421}
422
423.tag-name { font-weight: 500; }
424
425.tag-count {
426 font-size: 0.75rem;
427 color: var(--color-muted);
428 background-color: var(--color-bg);
429 padding: 0.1rem 0.4rem;
430 border-radius: 0.25rem;
431}
432
433.categories-list {
434 list-style: none;
435 padding: 0;
436 margin: 0;
437}
438
439.categories-list li {
440 padding: 0.75rem 0;
441 border-bottom: 1px solid var(--color-border);
442 font-size: 0.9375rem;
443}
444
445.categories-list li:last-child { border-bottom: none; }
446
447.categories-list .count {
448 color: var(--color-muted);
449 font-size: 0.875rem;
450}
451
452.archives h1 {
453 font-size: 1.5rem;
454 font-weight: 700;
455 margin-bottom: 2rem;
456}
457
458.archive-year h2 {
459 font-size: 1.125rem;
460 font-weight: 600;
461 color: var(--color-ink);
462 margin-top: 2rem;
463 margin-bottom: 0.5rem;
464 padding-bottom: 0.5rem;
465 border-bottom: 2px solid var(--color-primary);
466 display: inline-block;
467}
468
469.archive-year:first-child h2 { margin-top: 0; }
470
471.archive-year ul {
472 list-style: none;
473 padding: 0;
474 margin: 0;
475}
476
477.archive-year li {
478 display: flex;
479 align-items: center;
480 padding: 0.5rem 0;
481 gap: 0.75rem;
482 font-size: 0.9375rem;
483}
484
485.archive-date {
486 font-family: var(--font-mono);
487 font-size: 0.8125rem;
488 color: var(--color-muted);
489 min-width: 3.5rem;
490 flex-shrink: 0;
491}
492
493.archive-year li a {
494 color: var(--color-ink);
495 flex: 1;
496 min-width: 0;
497 overflow: hidden;
498 text-overflow: ellipsis;
499 white-space: nowrap;
500}
501
502.archive-year li a:hover { color: var(--color-primary); }
503
504.section-description {
505 color: var(--color-muted);
506 margin-bottom: 1.5rem;
507 font-size: 0.9375rem;
508}
509
510.shorts-section .short-list {
511 display: flex;
512 flex-direction: column;
513}
514
515.short-item {
516 padding: 1.5rem 0;
517 border-bottom: 1px solid var(--color-border);
518}
519
520.short-item:last-child { border-bottom: none; }
521
522.short-date {
523 font-size: 0.8125rem;
524 color: var(--color-muted);
525 font-family: var(--font-mono);
526 display: block;
527 margin-bottom: 0.75rem;
528}
529
530.short-content {
531 font-size: 0.9375rem;
532 line-height: 1.7;
533}
534
535.short-content > *:last-child { margin-bottom: 0; }
536
537.pagination {
538 display: flex;
539 justify-content: space-between;
540 align-items: center;
541 margin-top: 2rem;
542 padding-top: 1.5rem;
543 border-top: 1px solid var(--color-border);
544 font-size: 0.875rem;
545 color: var(--color-muted);
546}
547
548.pagination a {
549 padding: 0.5rem 1rem;
550 border: 1px solid var(--color-border);
551 border-radius: 0.375rem;
552 font-weight: 500;
553 color: var(--color-ink);
554}
555
556.pagination a:hover {
557 border-color: var(--color-primary);
558 color: var(--color-primary);
559}
560
561.search-wrapper {
562 position: relative;
563 width: 12rem;
564}
565
566.search-input {
567 width: 100%;
568 padding: 0.4rem 2rem 0.4rem 0.75rem;
569 font-size: 0.875rem;
570 border: 1px solid var(--color-border);
571 border-radius: 0.375rem;
572 background-color: var(--color-bg);
573 color: var(--color-ink);
574 font-family: var(--font-sans);
575}
576
577.search-input::placeholder { color: var(--color-muted); }
578
579.search-input:focus {
580 outline: none;
581 border-color: var(--color-primary);
582 box-shadow: 0 0 0 2px var(--color-primary-bg);
583}
584
585.search-btn {
586 position: absolute;
587 right: 0.5rem;
588 top: 50%;
589 transform: translateY(-50%);
590 padding: 0.25rem;
591 color: var(--color-muted);
592 cursor: pointer;
593 background: none;
594 border: none;
595 display: flex;
596 align-items: center;
597 justify-content: center;
598}
599
600.search-btn:hover { color: var(--color-ink); }
601
602.search-btn svg {
603 width: 1rem;
604 height: 1rem;
605 pointer-events: none;
606}
607
608.search-results {
609 position: absolute;
610 top: calc(100% + 0.25rem);
611 left: 0;
612 right: 0;
613 background-color: var(--color-bg);
614 border: 1px solid var(--color-border);
615 border-radius: 0.5rem;
616 box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
617 max-height: 24rem;
618 overflow-y: auto;
619 z-index: 50;
620 display: none;
621}
622
623.search-results.visible { display: block; }
624
625.search-result-item {
626 display: block;
627 padding: 0.75rem 0.875rem;
628 font-size: 0.875rem;
629 color: var(--color-ink);
630 border-bottom: 1px solid var(--color-border);
631 cursor: pointer;
632}
633
634.search-result-item:last-child { border-bottom: none; }
635
636.search-result-item:hover,
637.search-result-item[data-active="true"] {
638 background-color: var(--color-surface);
639}
640
641.search-result-item-title {
642 font-weight: 500;
643 color: var(--color-ink);
644 line-height: 1.4;
645}
646
647.search-result-item-summary {
648 margin-top: 0.25rem;
649 font-size: 0.75rem;
650 color: var(--color-muted);
651 line-height: 1.5;
652 display: -webkit-box;
653 -webkit-line-clamp: 2;
654 -webkit-box-orient: vertical;
655 overflow: hidden;
656}
657
658.search-result-item-tags {
659 display: flex;
660 flex-wrap: wrap;
661 gap: 0.375rem;
662 margin-top: 0.5rem;
663}
664
665.search-result-tag {
666 display: inline-flex;
667 padding: 0.125rem 0.5rem;
668 font-size: 0.6875rem;
669 background-color: var(--color-surface);
670 color: var(--color-muted);
671 border-radius: 9999px;
672}
673
674.search-loading,
675.search-empty,
676.search-error {
677 padding: 1rem;
678 text-align: center;
679 font-size: 0.875rem;
680 color: var(--color-muted);
681}
682
683.search-error { color: var(--color-error); }
684
685.prose { max-width: none; }
686
687.prose h1 { font-size: 1.875rem; font-weight: 700; margin-bottom: 1rem; line-height: 1.3; }
688.prose h2 { font-size: 1.5rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.75rem; line-height: 1.35; }
689.prose h3 { font-size: 1.25rem; font-weight: 600; margin-top: 1.5rem; margin-bottom: 0.5rem; line-height: 1.4; }
690.prose p { margin-bottom: 1rem; line-height: 1.75; }
691
692.prose code {
693 background-color: var(--color-surface);
694 padding: 0.125rem 0.375rem;
695 font-family: var(--font-mono);
696 font-size: 0.875em;
697 border-radius: 0.25rem;
698}
699
700.prose pre {
701 background-color: #1a1a1e;
702 color: #e8e7e3;
703 padding: 1rem 1.25rem;
704 border-radius: 0.5rem;
705 overflow-x: auto;
706 margin-bottom: 1rem;
707 font-size: 0.875rem;
708 line-height: 1.6;
709}
710
711.prose pre code { background-color: transparent; padding: 0; font-size: inherit; }
712
713.prose a {
714 color: var(--color-primary);
715 text-decoration: underline;
716 text-underline-offset: 2px;
717}
718
719.prose a:hover { text-decoration: none; }
720
721.prose ul { list-style-type: disc; list-style-position: inside; margin-bottom: 1rem; padding-left: 0.5rem; }
722.prose ol { list-style-type: decimal; list-style-position: inside; margin-bottom: 1rem; padding-left: 0.5rem; }
723.prose li { margin-bottom: 0.25rem; }
724
725.prose blockquote {
726 border-left: 3px solid var(--color-primary);
727 padding-left: 1rem;
728 margin: 1rem 0;
729 color: var(--color-muted);
730 font-style: italic;
731}
732
733.prose img { max-width: 100%; height: auto; border-radius: 0.5rem; margin: 1rem 0; }
734
735.prose table {
736 width: 100%;
737 border-collapse: collapse;
738 margin-bottom: 1rem;
739 font-size: 0.9375rem;
740}
741
742.prose th, .prose td {
743 border: 1px solid var(--color-border);
744 padding: 0.5rem 0.75rem;
745 text-align: left;
746}
747
748.prose th { background-color: var(--color-surface); font-weight: 600; }
749.prose hr { border: none; border-top: 1px solid var(--color-border); margin: 2rem 0; }
750
751.tweet-timeline { max-width: 42rem; margin-left: auto; margin-right: auto; }
752.tweet-list { border-top: 1px solid var(--color-border); }
753
754.tweet-card {
755 display: flex;
756 gap: 0.75rem;
757 padding: 1rem;
758 border-bottom: 1px solid var(--color-border);
759 cursor: pointer;
760 transition: background-color 0.15s ease;
761}
762
763.tweet-card:hover { background-color: var(--color-surface); }
764.tweet-single { cursor: default; padding: 1.5rem; }
765.tweet-single:hover { background-color: transparent; }
766.tweet-avatar { flex-shrink: 0; }
767
768.avatar-initials {
769 display: flex;
770 align-items: center;
771 justify-content: center;
772 width: 2.5rem;
773 height: 2.5rem;
774 border-radius: 9999px;
775 background-color: var(--color-primary);
776 color: white;
777 font-size: 0.875rem;
778 font-weight: 600;
779}
780
781.tweet-main { flex: 1; min-width: 0; }
782.tweet-header { display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; margin-bottom: 0.25rem; }
783.tweet-author { font-weight: 600; color: var(--color-ink); }
784.tweet-author:hover { text-decoration: underline; }
785.tweet-time { font-size: 0.875rem; color: var(--color-muted); }
786.tweet-content { color: var(--color-ink); line-height: 1.5; font-size: 0.9375rem; }
787.tweet-content p { margin-bottom: 0.5rem; }
788.tweet-content p:last-child { margin-bottom: 0; }
789.date-separator { margin: 1.5rem 0; border-top: 1px solid var(--color-border); }
790
791.archive-badge {
792 display: inline-flex;
793 align-items: center;
794 padding: 0.125rem 0.5rem;
795 margin-right: 0.5rem;
796 border-radius: 0.25rem;
797 font-size: 0.6875rem;
798 font-weight: 700;
799 text-transform: uppercase;
800 flex-shrink: 0;
801 color: white;
802}
803
804.badge-post { background-color: var(--color-success); }
805.badge-short { background-color: var(--color-accent); }
806
807@media (max-width: 48rem) {
808 .search-wrapper { width: 100%; }
809 .nav-links { width: 100%; justify-content: center; }
810 nav { padding-top: 0.75rem; padding-bottom: 0.75rem; }
811 .nav-actions { width: 100%; justify-content: center; margin-left: 0; margin-top: 0.5rem; }
812 .nav-links > a { padding: 0.35rem 0.5rem; font-size: 0.8125rem; }
813 main { padding-top: 1.5rem; padding-bottom: 2rem; }
814 .post-item-header { flex-direction: column; gap: 0.25rem; }
815 .archive-year li { flex-wrap: wrap; }
816}
817
818@media (prefers-reduced-motion: reduce) {
819 *, *::before, *::after {
820 animation-duration: 0.01ms !important;
821 animation-iteration-count: 1 !important;
822 transition-duration: 0.01ms !important;
823 }
824}
825"#;
826
827pub const DEFAULT_JS: &str = r#"
828(function() {
829 'use strict';
830
831 function initThemeToggle() {
832 var toggle = document.querySelector('.theme-toggle');
833 if (!toggle) return;
834 var html = document.documentElement;
835
836 toggle.addEventListener('click', function() {
837 var current = html.getAttribute('data-theme');
838 if (current !== 'dark' && current !== 'light') {
839 current = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
840 }
841 var next = current === 'dark' ? 'light' : 'dark';
842 html.setAttribute('data-theme', next);
843 try { localStorage.setItem('theme', next); } catch (e) {}
844 });
845
846 window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
847 try {
848 if (!localStorage.getItem('theme')) {
849 html.setAttribute('data-theme', e.matches ? 'dark' : 'light');
850 }
851 } catch (e) {}
852 });
853 }
854
855 function initLangSwitcher() {
856 var switcher = document.querySelector('.lang-switcher');
857 if (!switcher) return;
858
859 switcher.addEventListener('click', function(e) {
860 e.stopPropagation();
861 var isOpen = switcher.classList.contains('open');
862 document.querySelectorAll('.lang-switcher.open').forEach(function(el) {
863 el.classList.remove('open');
864 });
865 if (!isOpen) {
866 switcher.classList.add('open');
867 }
868 });
869
870 switcher.addEventListener('keydown', function(e) {
871 if (e.key === 'Enter' || e.key === ' ') {
872 e.preventDefault();
873 switcher.click();
874 } else if (e.key === 'Escape') {
875 switcher.classList.remove('open');
876 }
877 });
878
879 document.addEventListener('click', function() {
880 switcher.classList.remove('open');
881 });
882 }
883
884 function initSearch() {
885 var input = document.getElementById('searchInput');
886 var results = document.getElementById('searchResults');
887 var wrapper = document.getElementById('searchWrapper');
888 var searchBtn = wrapper ? wrapper.querySelector('.search-btn') : null;
889 if (!input || !results || !wrapper) return;
890
891 if (searchBtn) {
892 searchBtn.addEventListener('click', function() {
893 input.focus();
894 });
895 }
896
897 var searchIndex = null;
898 var debounceTimer = null;
899 var activeIndex = -1;
900 var currentMatches = [];
901
902 function getBasePath() {
903 var path = window.location.pathname;
904 var segments = path.split('/').filter(Boolean);
905 if (segments.length > 0 && segments[0].length === 2) {
906 return '/' + segments[0];
907 }
908 return '';
909 }
910
911 function escapeHtml(text) {
912 var div = document.createElement('div');
913 div.appendChild(document.createTextNode(text));
914 return div.innerHTML;
915 }
916
917 function loadSearchIndex() {
918 if (searchIndex) return Promise.resolve(searchIndex);
919 var basePath = getBasePath();
920 var indexPath = basePath + '/search-index.json';
921 return fetch(indexPath)
922 .then(function(res) {
923 if (!res.ok) throw new Error('not found');
924 return res.json();
925 })
926 .then(function(data) {
927 searchIndex = data;
928 return data;
929 })
930 .catch(function() {
931 return null;
932 });
933 }
934
935 function showResults(items) {
936 currentMatches = items;
937 activeIndex = -1;
938 if (!items || items.length === 0) {
939 results.innerHTML = '<div class="search-empty">No results found</div>';
940 } else {
941 results.innerHTML = items.map(function(doc, idx) {
942 var tagsHtml = '';
943 if (doc.tags && doc.tags.length > 0) {
944 tagsHtml = '<div class="search-result-item-tags">' +
945 doc.tags.map(function(t) {
946 return '<span class="search-result-tag">' + escapeHtml(t) + '</span>';
947 }).join('') + '</div>';
948 }
949 return '<a href="' + escapeHtml(doc.url) + '" class="search-result-item" data-index="' + idx + '">' +
950 '<div class="search-result-item-title">' + escapeHtml(doc.title) + '</div>' +
951 (doc.description ? '<div class="search-result-item-summary">' + escapeHtml(doc.description) + '</div>' : '') +
952 tagsHtml + '</a>';
953 }).join('');
954 }
955 results.classList.add('visible');
956 }
957
958 function hideResults() {
959 results.classList.remove('visible');
960 currentMatches = [];
961 activeIndex = -1;
962 }
963
964 function updateActive() {
965 var items = results.querySelectorAll('.search-result-item');
966 items.forEach(function(el, idx) {
967 el.setAttribute('data-active', idx === activeIndex ? 'true' : 'false');
968 });
969 }
970
971 function performSearch(query) {
972 loadSearchIndex().then(function(index) {
973 if (!index || !index.documents) {
974 results.innerHTML = '<div class="search-error">Search index not available</div>';
975 results.classList.add('visible');
976 return;
977 }
978 var q = query.toLowerCase();
979 var matches = index.documents.filter(function(doc) {
980 var title = (doc.title || '').toLowerCase();
981 var desc = (doc.description || '').toLowerCase();
982 var tags = (doc.tags || []).join(' ').toLowerCase();
983 if (title.indexOf(q) !== -1) return true;
984 if (desc.indexOf(q) !== -1) return true;
985 if (tags.indexOf(q) !== -1) return true;
986 return false;
987 }).slice(0, 8);
988 showResults(matches);
989 });
990 }
991
992 input.addEventListener('focus', function() {
993 loadSearchIndex();
994 if (input.value.trim()) {
995 performSearch(input.value.trim());
996 }
997 });
998
999 input.addEventListener('input', function() {
1000 clearTimeout(debounceTimer);
1001 var query = input.value.trim();
1002 if (query.length === 0) {
1003 hideResults();
1004 return;
1005 }
1006 debounceTimer = setTimeout(function() {
1007 performSearch(query);
1008 }, 200);
1009 });
1010
1011 input.addEventListener('keydown', function(e) {
1012 var items = results.querySelectorAll('.search-result-item');
1013 if (e.key === 'ArrowDown') {
1014 e.preventDefault();
1015 if (items.length === 0) return;
1016 activeIndex = activeIndex < items.length - 1 ? activeIndex + 1 : 0;
1017 updateActive();
1018 } else if (e.key === 'ArrowUp') {
1019 e.preventDefault();
1020 if (items.length === 0) return;
1021 activeIndex = activeIndex > 0 ? activeIndex - 1 : items.length - 1;
1022 updateActive();
1023 } else if (e.key === 'Enter') {
1024 e.preventDefault();
1025 if (activeIndex >= 0 && currentMatches[activeIndex]) {
1026 window.location.href = currentMatches[activeIndex].url;
1027 }
1028 } else if (e.key === 'Escape') {
1029 hideResults();
1030 input.blur();
1031 }
1032 });
1033
1034 document.addEventListener('click', function(e) {
1035 if (!wrapper.contains(e.target)) {
1036 hideResults();
1037 }
1038 });
1039 }
1040
1041 if (document.readyState === 'loading') {
1042 document.addEventListener('DOMContentLoaded', function() {
1043 initThemeToggle();
1044 initLangSwitcher();
1045 initSearch();
1046 });
1047 } else {
1048 initThemeToggle();
1049 initLangSwitcher();
1050 initSearch();
1051 }
1052})();
1053"#;
1054
1055#[cfg(test)]
1056mod tests {
1057 use tempfile::TempDir;
1058
1059 use super::*;
1060
1061 #[test]
1062 fn test_generate_static_assets() {
1063 let temp_dir = TempDir::new().unwrap();
1064 let output_dir = temp_dir.path();
1065
1066 generate_static_assets(output_dir).unwrap();
1067
1068 let css_path = output_dir.join("assets/style.css");
1070 assert!(css_path.exists());
1071 let css_content = std::fs::read_to_string(&css_path).unwrap();
1072 assert!(css_content.contains(":root"));
1073 assert!(css_content.contains("--color-primary"));
1074
1075 let js_path = output_dir.join("assets/main.js");
1077 assert!(js_path.exists());
1078 let js_content = std::fs::read_to_string(&js_path).unwrap();
1079 assert!(js_content.contains("theme-toggle"));
1080 assert!(js_content.contains("searchIndex"));
1081 }
1082}