wechat-pub-rs 0.5.1

A simple, high-performance WeChat Official Account Rust SDK for uploading articles and managing drafts
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
//! Theme system for rendering markdown content to HTML.
//!
//! This module provides a comprehensive theming system that converts Markdown content
//! into beautifully styled HTML suitable for WeChat Official Account articles.
//!
//! ## Features
//!
//! - **8 Built-in Themes**: Carefully designed themes for different aesthetics
//! - **Syntax Highlighting**: 10 different code highlighting themes
//! - **CSS Variable Processing**: Dynamic theming with CSS custom properties
//! - **Template Engine**: Askama-based HTML templating
//! - **Responsive Design**: Mobile-first responsive layouts
//!
//! ## Available Themes
//!
//! | Theme | Description | Best For |
//! |-------|-------------|----------|
//! | `default` | Clean, minimal design | General content |
//! | `lapis` | Blue accents, elegant | Technical articles |
//! | `maize` | Warm yellow tones | Creative content |
//! | `orangeheart` | Orange accents | Personal blogs |
//! | `phycat` | Unique styling | Special content |
//! | `pie` | Sweet, colorful | Lifestyle content |
//! | `purple` | Purple accents | Creative writing |
//! | `rainbow` | Colorful, vibrant | Fun content |
//!
//! ## Code Highlighting Themes
//!
//! - `github` / `github-dark` - GitHub styling
//! - `atom-one-light` / `atom-one-dark` - Atom editor themes
//! - `solarized-light` / `solarized-dark` - Solarized color scheme
//! - `vscode` - VS Code default theme
//! - `monokai`, `dracula`, `xcode` - Popular editor themes
//!
//! ## Usage
//!
//! ```rust
//! use wechat_pub_rs::theme::{ThemeManager, BuiltinTheme};
//! use std::collections::HashMap;
//!
//! let theme_manager = ThemeManager::new();
//!
//! // Check available themes
//! let themes = theme_manager.available_themes();
//! println!("Available themes: {:?}", themes);
//!
//! // Render content with a theme
//! let metadata = HashMap::new();
//! let html = theme_manager.render(
//!     "# Hello World\nSome **bold** text",
//!     "lapis",
//!     "github",
//!     &metadata
//! ).unwrap();
//! ```

use crate::css_vars::CssVariableProcessor;
use crate::error::{Result, WeChatError};
use askama::Template;
use comrak::{
    ComrakOptions, ComrakPlugins, markdown_to_html_with_plugins, plugins::syntect::SyntectAdapter,
};
use std::collections::HashMap;
use tracing::warn;

// Embed all theme CSS files at compile time
const DEFAULT_CSS: &str = include_str!("../themes/default.css");
const LAPIS_CSS: &str = include_str!("../themes/lapis.css");
const MAIZE_CSS: &str = include_str!("../themes/maize.css");
const ORANGEHEART_CSS: &str = include_str!("../themes/orangeheart.css");
const PHYCAT_CSS: &str = include_str!("../themes/phycat.css");
const PIE_CSS: &str = include_str!("../themes/pie.css");
const PURPLE_CSS: &str = include_str!("../themes/purple.css");
const RAINBOW_CSS: &str = include_str!("../themes/rainbow.css");

// Embed all highlight CSS files at compile time
const ATOM_ONE_DARK_CSS: &str = include_str!("../themes/highlight/atom-one-dark.min.css");
const ATOM_ONE_LIGHT_CSS: &str = include_str!("../themes/highlight/atom-one-light.min.css");
const DRACULA_CSS: &str = include_str!("../themes/highlight/dracula.min.css");
const GITHUB_DARK_CSS: &str = include_str!("../themes/highlight/github-dark.min.css");
const GITHUB_CSS: &str = include_str!("../themes/highlight/github.min.css");
const MONOKAI_CSS: &str = include_str!("../themes/highlight/monokai.min.css");
const SOLARIZED_DARK_CSS: &str = include_str!("../themes/highlight/solarized-dark.min.css");
const SOLARIZED_LIGHT_CSS: &str = include_str!("../themes/highlight/solarized-light.min.css");
const XCODE_CSS: &str = include_str!("../themes/highlight/xcode.min.css");

/// Built-in theme options.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuiltinTheme {
    /// Simple, clean default theme
    Default,
    /// Lapis theme with blue accents
    Lapis,
    /// Maize theme with yellow tones
    Maize,
    /// Orange Heart theme with orange accents
    OrangeHeart,
    /// PhyCat theme
    PhyCat,
    /// Pie theme
    Pie,
    /// Purple theme with purple accents
    Purple,
    /// Rainbow theme with colorful elements
    Rainbow,
}

impl BuiltinTheme {
    /// Gets the theme name as a string.
    pub fn as_str(&self) -> &'static str {
        match self {
            BuiltinTheme::Default => "default",
            BuiltinTheme::Lapis => "lapis",
            BuiltinTheme::Maize => "maize",
            BuiltinTheme::OrangeHeart => "orangeheart",
            BuiltinTheme::PhyCat => "phycat",
            BuiltinTheme::Pie => "pie",
            BuiltinTheme::Purple => "purple",
            BuiltinTheme::Rainbow => "rainbow",
        }
    }

    /// Gets all available built-in themes.
    pub fn all() -> Vec<BuiltinTheme> {
        vec![
            BuiltinTheme::Default,
            BuiltinTheme::Lapis,
            BuiltinTheme::Maize,
            BuiltinTheme::OrangeHeart,
            BuiltinTheme::PhyCat,
            BuiltinTheme::Pie,
            BuiltinTheme::Purple,
            BuiltinTheme::Rainbow,
        ]
    }
}

impl std::str::FromStr for BuiltinTheme {
    type Err = WeChatError;

    fn from_str(s: &str) -> Result<Self> {
        match s.to_lowercase().as_str() {
            "default" => Ok(BuiltinTheme::Default),
            "lapis" => Ok(BuiltinTheme::Lapis),
            "maize" => Ok(BuiltinTheme::Maize),
            "orangeheart" => Ok(BuiltinTheme::OrangeHeart),
            "phycat" => Ok(BuiltinTheme::PhyCat),
            "pie" => Ok(BuiltinTheme::Pie),
            "purple" => Ok(BuiltinTheme::Purple),
            "rainbow" => Ok(BuiltinTheme::Rainbow),
            _ => Err(WeChatError::ThemeNotFound {
                theme: s.to_string(),
            }),
        }
    }
}

/// Askama template for rendering articles with themes.
#[derive(Template)]
#[template(path = "article.html")]
pub struct ArticleTemplate {
    pub title: String,
    pub description: String,
    pub author: String,
    pub content: String,
    pub theme_css: String,
    pub highlight_css: String,
}

/// Theme template containing CSS for styling.
#[derive(Debug, Clone)]
pub struct ThemeTemplate {
    /// CSS styles for the theme
    pub theme_css: String,
    /// CSS styles for the highlight theme
    pub code_css: String,
    /// Theme name
    pub name: String,
}

impl ThemeTemplate {
    /// Creates a new theme template.
    pub fn new(theme_css: String, code_css: String, name: String) -> Self {
        Self {
            theme_css,
            code_css,
            name,
        }
    }

    /// Creates a new theme template with static CSS references.
    pub fn from_static(theme_css: &'static str, code_css: &'static str, name: String) -> Self {
        Self {
            theme_css: theme_css.to_string(),
            code_css: code_css.to_string(),
            name,
        }
    }

    /// Renders content using this theme with inline styles for WeChat.
    ///
    /// This method processes CSS variables before inlining styles for better WeChat compatibility.
    pub fn render(&self, content: &str, metadata: &HashMap<String, String>) -> Result<String> {
        // Process CSS variables in both theme and highlight CSS
        let css_processor = CssVariableProcessor::new();

        let processed_theme_css =
            css_processor
                .process_css(&self.theme_css)
                .map_err(|e| WeChatError::Internal {
                    message: format!("CSS variable processing failed for theme CSS: {e}"),
                })?;

        let processed_highlight_css =
            css_processor
                .process_css(&self.code_css)
                .map_err(|e| WeChatError::Internal {
                    message: format!("CSS variable processing failed for highlight CSS: {e}"),
                })?;

        // Create Askama template with the processed CSS
        let template = ArticleTemplate {
            title: metadata.get("title").cloned().unwrap_or_default(),
            description: metadata.get("description").cloned().unwrap_or_default(),
            author: metadata.get("author").cloned().unwrap_or_default(),
            content: content.to_string(),
            theme_css: processed_theme_css,
            highlight_css: processed_highlight_css,
        };

        // Render the template to HTML
        let html_with_css = template.render().map_err(|e| WeChatError::Internal {
            message: format!("Template rendering failed: {e}"),
        })?;

        // Post-process code blocks before CSS inlining to preserve their structure
        let html_with_protected_code = self.post_process_code_blocks(html_with_css);

        // Use css-inline to convert CSS to inline styles
        let inlined_html =
            css_inline::inline(&html_with_protected_code).map_err(|e| WeChatError::Internal {
                message: format!("CSS inlining failed: {e}"),
            })?;

        // remove "\n"
        let html_without_newlines = inlined_html.replace("\n", "");
        Ok(html_without_newlines)
    }

    /// Post-process HTML to preserve code block structure for WeChat.
    /// This function handles syntax-highlighted code blocks and preserves syntax highlighting while ensuring proper line breaks.
    fn post_process_code_blocks(&self, html: String) -> String {
        use regex::Regex;

        // Use regex to find and replace pre > code blocks while preserving syntax highlighting
        let pre_code_regex =
            Regex::new(r#"(?s)(<pre[^>]*>)(<code[^>]*>)(.*?)</code></pre>"#).unwrap();

        let result = pre_code_regex.replace_all(&html, |caps: &regex::Captures| {
            let pre_tag = &caps[1];
            let code_tag = &caps[2];
            let content = &caps[3];

            // Process the content to fix newlines while preserving syntax highlighting
            let processed_content = self.process_code_content(content);

            // Create a properly formatted code block with inline styles that will survive CSS inlining
            format!("{pre_tag}{code_tag}{processed_content}</code></pre>")
        });

        result.to_string()
    }

    /// Process code content to preserve syntax highlighting while fixing newlines for WeChat.
    fn process_code_content(&self, html_content: &str) -> String {
        // Convert newlines to <br/> tags for WeChat compatibility
        // Preserve existing HTML structure (like <span> tags for syntax highlighting)
        let mut result = html_content.to_string();

        // First, handle explicit newlines at the end of spans
        result = result.replace(">\n", "><br/>");

        // Handle newlines in plain text (not inside tags)
        let mut processed = String::new();
        let mut in_tag = false;

        for ch in result.chars() {
            match ch {
                '<' => {
                    in_tag = true;
                    processed.push(ch);
                }
                '>' => {
                    in_tag = false;
                    processed.push(ch);
                }
                '\n' if !in_tag => {
                    // Convert standalone newlines to <br/> tags
                    processed.push_str("<br/>");
                }
                _ => {
                    processed.push(ch);
                }
            }
        }

        // Clean up multiple consecutive <br/> tags
        processed = processed.replace("<br/><br/>", "<br/>");

        // Handle spaces and tabs
        processed = processed.replace("  ", "&nbsp;&nbsp;");
        processed = processed.replace('\t', "&nbsp;&nbsp;&nbsp;&nbsp;");

        processed
    }
}

/// Theme manager for rendering markdown with different styles.
#[derive(Debug)]
pub struct ThemeManager {
    templates: HashMap<String, ThemeTemplate>,
    highlight_css: HashMap<String, String>,
    markdown_options: ComrakOptions<'static>,
}

impl ThemeManager {
    /// Creates a new theme manager with built-in themes.
    pub fn new() -> Self {
        let mut manager = Self {
            templates: HashMap::new(),
            highlight_css: HashMap::new(),
            markdown_options: Self::create_markdown_options(),
        };

        manager.load_builtin_themes();
        manager.load_highlight_themes();
        manager
    }

    /// Creates markdown parsing options.
    fn create_markdown_options() -> ComrakOptions<'static> {
        let mut options = ComrakOptions::default();
        options.extension.strikethrough = true;
        options.extension.table = true;
        options.extension.footnotes = true;
        options.extension.tasklist = true;
        options.parse.smart = true;
        options
    }

    /// Loads all built-in themes from embedded CSS.
    fn load_builtin_themes(&mut self) {
        for theme in BuiltinTheme::all() {
            let template = self.create_builtin_theme(theme);
            self.templates.insert(theme.as_str().to_string(), template);
        }
    }

    /// Loads all highlight themes from embedded CSS.
    fn load_highlight_themes(&mut self) {
        // Array of (theme_name, css_content) pairs for cleaner initialization
        let highlight_themes = [
            ("atom-one-dark", ATOM_ONE_DARK_CSS),
            ("atom-one-light", ATOM_ONE_LIGHT_CSS),
            ("dracula", DRACULA_CSS),
            ("github-dark", GITHUB_DARK_CSS),
            ("github", GITHUB_CSS),
            ("monokai", MONOKAI_CSS),
            ("solarized-dark", SOLARIZED_DARK_CSS),
            ("solarized-light", SOLARIZED_LIGHT_CSS),
            ("xcode", XCODE_CSS),
            ("vscode", GITHUB_CSS), // vscode as an alias for github
        ];

        for (name, css) in highlight_themes {
            self.highlight_css.insert(name.to_string(), css.to_string());
        }
    }

    /// Creates a built-in theme template from embedded CSS.
    fn create_builtin_theme(&self, theme: BuiltinTheme) -> ThemeTemplate {
        let css = self.get_embedded_theme_css(theme);
        ThemeTemplate::from_static(css, "", theme.as_str().to_string())
    }

    /// Gets embedded CSS content for built-in themes.
    fn get_embedded_theme_css(&self, theme: BuiltinTheme) -> &'static str {
        match theme {
            BuiltinTheme::Default => DEFAULT_CSS,
            BuiltinTheme::Lapis => LAPIS_CSS,
            BuiltinTheme::Maize => MAIZE_CSS,
            BuiltinTheme::OrangeHeart => ORANGEHEART_CSS,
            BuiltinTheme::PhyCat => PHYCAT_CSS,
            BuiltinTheme::Pie => PIE_CSS,
            BuiltinTheme::Purple => PURPLE_CSS,
            BuiltinTheme::Rainbow => RAINBOW_CSS,
        }
    }

    /// Renders markdown content with the specified theme and code highlight theme.
    pub fn render(
        &self,
        markdown_content: &str,
        theme_name: &str,
        code_theme: &str,
        metadata: &HashMap<String, String>,
    ) -> Result<String> {
        let template =
            self.templates
                .get(theme_name)
                .ok_or_else(|| WeChatError::ThemeNotFound {
                    theme: theme_name.to_string(),
                })?;

        // Get highlight CSS, defaulting to "vscode" if not specified or not found
        let highlight_css = self.get_highlight_css(code_theme);

        // Create syntect adapter for syntax highlighting
        // Map our CSS theme names to syntect theme names
        let syntect_theme_name = match code_theme {
            "solarized-light" => Some("Solarized (light)"),
            "solarized-dark" => Some("Solarized (dark)"),
            "monokai" => Some("Monokai"),
            "github" | "vscode" => Some("InspiredGitHub"),
            "github-dark" => Some("base16-ocean.dark"),
            "atom-one-dark" => Some("base16-ocean.dark"),
            "atom-one-light" => Some("InspiredGitHub"),
            "dracula" => Some("base16-ocean.dark"),
            "xcode" => Some("InspiredGitHub"),
            _ => None, // Use default theme
        };

        let adapter = SyntectAdapter::new(syntect_theme_name);

        // Set up comrak plugins with syntect adapter
        let mut plugins = ComrakPlugins::default();
        plugins.render.codefence_syntax_highlighter = Some(&adapter);

        // Convert markdown to HTML using comrak with syntect
        let html_content =
            markdown_to_html_with_plugins(markdown_content, &self.markdown_options, &plugins);

        // Create a new template with the highlight CSS
        let template_with_highlight = ThemeTemplate {
            theme_css: template.theme_css.clone(),
            code_css: highlight_css,
            name: template.name.clone(),
        };

        // Apply theme template
        template_with_highlight.render(&html_content, metadata)
    }

    /// Adds a custom theme.
    pub fn add_theme(&mut self, name: String, template: ThemeTemplate) {
        self.templates.insert(name, template);
    }

    /// Gets the list of available theme names.
    pub fn available_themes(&self) -> Vec<&String> {
        self.templates.keys().collect()
    }

    /// Checks if a theme exists.
    pub fn has_theme(&self, name: &str) -> bool {
        self.templates.contains_key(name)
    }

    /// Gets highlight CSS for a given theme, falling back to default if not found.
    fn get_highlight_css(&self, theme: &str) -> String {
        self.highlight_css.get(theme).cloned().unwrap_or_else(|| {
            warn!("Highlight theme '{theme}' not found, falling back to 'github'");
            self.highlight_css
                .get("github")
                .cloned()
                .unwrap_or_default()
        })
    }
}

impl Default for ThemeManager {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_builtin_theme_parsing() {
        assert_eq!(
            "default".parse::<BuiltinTheme>().unwrap(),
            BuiltinTheme::Default
        );
        assert_eq!(
            "lapis".parse::<BuiltinTheme>().unwrap(),
            BuiltinTheme::Lapis
        );
        assert_eq!(
            "maize".parse::<BuiltinTheme>().unwrap(),
            BuiltinTheme::Maize
        );
        assert_eq!(
            "orangeheart".parse::<BuiltinTheme>().unwrap(),
            BuiltinTheme::OrangeHeart
        );
        assert_eq!(
            "phycat".parse::<BuiltinTheme>().unwrap(),
            BuiltinTheme::PhyCat
        );
        assert_eq!("pie".parse::<BuiltinTheme>().unwrap(), BuiltinTheme::Pie);
        assert_eq!(
            "purple".parse::<BuiltinTheme>().unwrap(),
            BuiltinTheme::Purple
        );
        assert_eq!(
            "rainbow".parse::<BuiltinTheme>().unwrap(),
            BuiltinTheme::Rainbow
        );

        assert!("nonexistent".parse::<BuiltinTheme>().is_err());
    }

    #[test]
    fn test_theme_manager_creation() {
        let manager = ThemeManager::new();

        for theme in BuiltinTheme::all() {
            assert!(manager.has_theme(theme.as_str()));
        }

        let themes = manager.available_themes();
        assert!(themes.len() >= 4);
    }

    #[test]
    fn test_theme_rendering() {
        let manager = ThemeManager::new();
        let markdown = "# Test Title\n\nThis is a test paragraph with **bold** text.";

        let mut metadata = HashMap::new();
        metadata.insert("title".to_string(), "Test Article".to_string());
        metadata.insert("author".to_string(), "Test Author".to_string());

        let result = manager.render(markdown, "default", "vscode", &metadata);
        assert!(result.is_ok());

        let html = result.unwrap();
        assert!(html.contains("<h1"));
        assert!(html.contains("Test Title"));
        assert!(html.contains("<strong"));
        assert!(html.contains("bold"));
        assert!(html.contains("id=\"wepub\""));
    }

    #[test]
    fn test_nonexistent_theme() {
        let manager = ThemeManager::new();
        let result = manager.render("# Test", "nonexistent", "vscode", &HashMap::new());

        assert!(result.is_err());
        if let Err(WeChatError::ThemeNotFound { theme }) = result {
            assert_eq!(theme, "nonexistent");
        } else {
            panic!("Expected ThemeNotFound error");
        }
    }

    #[test]
    fn test_custom_theme() {
        let mut manager = ThemeManager::new();

        let custom_template = ThemeTemplate::new(
            "#wepub { color: red; }".to_string(),
            String::new(),
            "custom".to_string(),
        );

        manager.add_theme("custom".to_string(), custom_template);
        assert!(manager.has_theme("custom"));

        let result = manager.render("# Test", "custom", "vscode", &HashMap::new());
        assert!(result.is_ok());

        let html = result.unwrap();
        assert!(html.contains("style="));
        assert!(html.contains("Test"));
        assert!(html.contains("id=\"wepub\""));
    }

    #[test]
    fn test_highlight_theme_rendering() {
        let manager = ThemeManager::new();
        let markdown = r#"# Test

```rust
fn main() {
    println!("Hello, world!");
}
```"#;

        let mut metadata = HashMap::new();
        metadata.insert("title".to_string(), "Test Article".to_string());
        metadata.insert("author".to_string(), "Test Author".to_string());

        // Test with specific highlight theme
        let result = manager.render(markdown, "default", "solarized-light", &metadata);
        assert!(result.is_ok());

        let html = result.unwrap();
        assert!(html.contains("<h1"));
        assert!(html.contains("Test"));
        assert!(html.contains("<code"));

        // Test with default highlight theme (None)
        let result = manager.render(markdown, "default", "vscode", &metadata);
        assert!(result.is_ok());

        // Test with nonexistent highlight theme (should fallback to github)
        let result = manager.render(markdown, "default", "nonexistent", &metadata);
        assert!(result.is_ok());
    }

    #[test]
    fn test_template_css_inlining() {
        let css = "#wepub h1 { color: red; font-size: 2em; }";
        let template = ThemeTemplate::new(css.to_string(), String::new(), "theme".to_string());

        let mut metadata = HashMap::new();
        metadata.insert("title".to_string(), "My Title".to_string());
        metadata.insert("author".to_string(), "John Doe".to_string());

        let result = template.render("<h1>Content</h1>", &metadata);
        assert!(result.is_ok());

        let html = result.unwrap();
        assert!(html.contains("id=\"wepub\""));
        assert!(html.contains("<h1"));
        assert!(html.contains("Content"));
        // Check that CSS was applied as inline styles
        assert!(html.contains("style="));
    }

    #[test]
    fn test_css_variable_processing_in_theme() {
        let css_with_vars = r#"
        :root {
            --primary-color: #4870ac;
            --text-color: #40464f;
            --header-color: var(--primary-color);
        }
        #wepub { color: var(--text-color); }
        #wepub h1 { color: var(--header-color); }
        "#;

        let template =
            ThemeTemplate::new(css_with_vars.to_string(), String::new(), "test".to_string());

        let mut metadata = HashMap::new();
        metadata.insert("title".to_string(), "Test".to_string());
        metadata.insert("author".to_string(), "Test Author".to_string());

        let result = template.render("<h1>Test Header</h1><p>Test content</p>", &metadata);
        assert!(result.is_ok());

        let html = result.unwrap();

        // Verify CSS variables were processed and inlined
        assert!(!html.contains("var(")); // No var() calls should remain
        assert!(html.contains("#40464f")); // text-color should be inlined
        assert!(html.contains("#4870ac")); // header-color should be resolved and inlined
    }

    #[test]
    fn test_nested_css_variables_in_theme() {
        let css_with_nested_vars = r#"
        :root {
            --base-color: #4870ac;
            --primary-color: var(--base-color);
            --header-span-color: var(--primary-color);
            --shadow-color: #eee;
            --shadow: 3px 3px 10px var(--shadow-color);
        }
        #wepub h1 span { color: var(--header-span-color); }
        #wepub .box { box-shadow: var(--shadow); }
        "#;

        let template = ThemeTemplate::new(
            css_with_nested_vars.to_string(),
            String::new(),
            "nested".to_string(),
        );

        let mut metadata = HashMap::new();
        metadata.insert("title".to_string(), "Nested Test".to_string());

        let result = template.render("<h1><span>Nested</span></h1>", &metadata);
        assert!(result.is_ok());

        let html = result.unwrap();

        // Verify nested variables were resolved correctly
        assert!(!html.contains("var("));
        assert!(html.contains("#4870ac")); // All nested references should resolve to base color
        assert!(html.contains("3px 3px 10px #eee")); // Shadow should be fully resolved
    }

    #[test]
    fn test_real_theme_css_variable_processing() {
        // Test with actual purple theme which uses CSS variables
        let manager = ThemeManager::new();
        let markdown = "# Test Title\n\nThis is a **test** paragraph.";

        let mut metadata = HashMap::new();
        metadata.insert("title".to_string(), "Variable Test".to_string());
        metadata.insert("author".to_string(), "Test Author".to_string());

        // Test purple theme which has CSS variables
        let result = manager.render(markdown, "purple", "github", &metadata);
        assert!(result.is_ok());

        let html = result.unwrap();

        // Verify that defined CSS variables were processed
        // Note: Undefined variables like --sans-serif-font may remain for graceful degradation
        assert!(!html.contains("var(--title-color"));
        assert!(!html.contains("var(--text-color"));
        assert!(!html.contains("var(--shadow-color"));

        // Verify specific color values are present (from resolved variables)
        assert!(
            html.contains("#8064a9")
                || html.contains("color:#8064a9")
                || html.contains("color: #8064a9")
        );
        assert!(
            html.contains("#444444")
                || html.contains("color:#444444")
                || html.contains("color: #444444")
        );

        // Verify the content is properly rendered
        assert!(html.contains("Test Title"));
        assert!(html.contains("<strong"));
        assert!(html.contains("test"));
        assert!(html.contains("id=\"wepub\""));
    }

    #[test]
    fn test_all_themes_css_variable_processing() {
        // Test CSS variable processing works across all theme files
        let manager = ThemeManager::new();
        let markdown = "# Test\n\nCSS variables test.";
        let metadata = HashMap::new();

        for theme in BuiltinTheme::all() {
            let result = manager.render(markdown, theme.as_str(), "github", &metadata);
            assert!(
                result.is_ok(),
                "Theme {} should render successfully",
                theme.as_str()
            );

            let html = result.unwrap();

            // Verify basic HTML structure is preserved
            assert!(html.contains("Test"));
            assert!(html.contains("id=\"wepub\""));

            // For themes with CSS variables, verify that at least some processing occurred
            // We don't assert complete absence of var() since some undefined variables may remain
            let theme_css = get_embedded_theme_css(theme);
            let var_count_before = theme_css_var_count(theme_css);
            let var_count_after = html.matches("var(--").count();

            // At minimum, defined variables should be reduced
            if var_count_before > 0 {
                println!(
                    "Theme {}: {} vars before, {} vars after",
                    theme.as_str(),
                    var_count_before,
                    var_count_after
                );
            }
        }
    }

    #[test]
    fn test_post_process_code_blocks_issues_analysis() {
        // This test has been updated - post_process_code_blocks is now internal to ThemeTemplate
        // and properly handles syntax-highlighted code blocks using HTML parsing instead of regex

        let manager = ThemeManager::new();
        let markdown = "```rust\nfn main() {}\n```";
        let mut metadata = HashMap::new();
        metadata.insert("title".to_string(), "Test".to_string());

        let final_html = manager
            .render(markdown, "default", "github", &metadata)
            .unwrap();

        // With our fix, <pre> and <code> tags are preserved with inline styles
        assert!(final_html.contains("<pre"), "Pre tags should be preserved");
        assert!(
            final_html.contains("<code"),
            "Code tags should be preserved"
        );
        assert!(
            final_html.contains("style="),
            "Code blocks should have inline styles"
        );

        // Content is preserved (separated by syntax highlighting spans)
        assert!(
            final_html.contains("fn") && final_html.contains("main"),
            "Code content is preserved"
        );
    }

    #[test]
    fn test_post_process_code_blocks_edge_cases() {
        // This test has been updated to test the overall rendering pipeline
        // since post_process_code_blocks is now internal to ThemeTemplate
        let manager = ThemeManager::new();
        let mut metadata = HashMap::new();
        metadata.insert("title".to_string(), "Test".to_string());

        // Test various markdown code samples through the complete rendering pipeline
        let markdown_samples = [
            // Normal inline code
            "This is `inline code` text.",
            // Normal block code
            "```rust\nfn main() {\n    println!(\"Hello\");\n}\n```",
            // Code with special characters
            "```javascript\nconst regex = /<code>([^<]*)</code>/;\n```",
            // Multiple code blocks
            "First `code` and second `code`\n\n```\nblock code\n```",
        ];

        for (i, markdown_input) in markdown_samples.iter().enumerate() {
            println!("Testing Markdown sample {}: {}", i + 1, markdown_input);
            let rendered = manager
                .render(markdown_input, "default", "github", &metadata)
                .unwrap();

            println!("Rendered output length: {} chars", rendered.len());

            // Verify that code blocks have proper styling
            if markdown_input.contains("```") {
                // Block code should have inline styles
                assert!(
                    rendered.contains("style="),
                    "Sample {}: Block code should have inline styles",
                    i + 1
                );
                assert!(
                    rendered.contains("font-family"),
                    "Sample {}: Code should have font-family style",
                    i + 1
                );
            }

            if markdown_input.contains("`") && !markdown_input.contains("```") {
                // Inline code should have inline styles
                assert!(
                    rendered.contains("style="),
                    "Sample {}: Inline code should have inline styles",
                    i + 1
                );
            }
        }
    }

    #[test]
    fn test_wechat_code_block_newline_fix() {
        // Test the fix for WeChat code block newline rendering
        let manager = ThemeManager::new();
        let complex_markdown = r#"# Code Block Test

Here's a complex Rust function:

```rust
fn calculate_fibonacci(n: u32) -> u64 {
    if n <= 1 {
        return n as u64;
    }

    let mut a = 0u64;
    let mut b = 1u64;

    for _ in 2..=n {
        let temp = a + b;
        a = b;
        b = temp;
    }

    b
}
```

And some inline `code` as well."#;

        let mut metadata = HashMap::new();
        metadata.insert("title".to_string(), "Code Test".to_string());
        metadata.insert("author".to_string(), "Test Author".to_string());

        let final_html = manager
            .render(complex_markdown, "default", "github", &metadata)
            .unwrap();

        // Verify that line breaks are preserved in code blocks
        assert!(
            final_html.contains("<br>") || final_html.contains("white-space: pre"),
            "Code should have line breaks via <br> tags or white-space: pre CSS"
        );

        // Verify that code content is preserved
        assert!(
            final_html.contains("fibonacci"),
            "Code content should be preserved"
        );
        assert!(
            final_html.contains("temp"),
            "Multi-line code should be preserved"
        );

        // Verify syntax highlighting is maintained
        assert!(
            final_html.contains("span"),
            "Syntax highlighting spans should be preserved"
        );
        assert!(
            final_html.contains("style="),
            "Inline styles should be applied"
        );

        // Check that the problematic single-line rendering is fixed
        // The old issue was: "fn calculate_fibonacci(n: u32) -> u64 { if n <= 1 { return n as u64; } ..."
        // Now it should have proper line breaks
        let fibonacci_context = if let Some(start) = final_html.find("fibonacci") {
            &final_html[start..start + 500.min(final_html.len() - start)]
        } else {
            ""
        };

        // Verify that we have line breaks in the code structure
        assert!(
            fibonacci_context.contains("<br>") || fibonacci_context.contains("white-space: pre"),
            "Function should have proper line breaks, not be all on one line"
        );
    }

    #[test]
    fn test_css_inlining_effect_on_code_blocks() {
        // Test what happens to code blocks after CSS inlining
        let manager = ThemeManager::new();
        let simple_markdown = "```rust\nfn main() {\n    println!(\"Hello, world!\");\n}\n```";
        let mut metadata = HashMap::new();
        metadata.insert("title".to_string(), "Test".to_string());

        // Get HTML before CSS inlining by using comrak directly
        use comrak::{
            ComrakOptions, ComrakPlugins, markdown_to_html_with_plugins,
            plugins::syntect::SyntectAdapter,
        };

        let mut options = ComrakOptions::default();
        options.extension.strikethrough = true;
        options.extension.table = true;
        options.extension.footnotes = true;
        options.extension.tasklist = true;
        options.parse.smart = true;

        let adapter = SyntectAdapter::new(Some("InspiredGitHub"));
        let mut plugins = ComrakPlugins::default();
        plugins.render.codefence_syntax_highlighter = Some(&adapter);

        let html_before_processing =
            markdown_to_html_with_plugins(simple_markdown, &options, &plugins);
        println!("HTML before post-processing: {html_before_processing}");

        // Now test the full rendering pipeline
        let final_html = manager
            .render(simple_markdown, "default", "github", &metadata)
            .unwrap();
        println!(
            "Final HTML after CSS inlining: contains <pre>: {}",
            final_html.contains("<pre>")
        );
        println!(
            "Final HTML after CSS inlining: contains <code>: {}",
            final_html.contains("<code>")
        );

        // Find and print the actual code content in final HTML
        if let Some(code_start) = final_html.find("fn main") {
            let context_start = code_start.saturating_sub(200);
            let context_end = (code_start + 400).min(final_html.len());
            println!(
                "Code context in final HTML: {}",
                &final_html[context_start..context_end]
            );
        }

        // Test that our newline fixes are working - either <br> tags or white-space: pre CSS
        assert!(
            final_html.contains("<br>") || final_html.contains("white-space: pre"),
            "Code should have line breaks via <br> tags or white-space: pre CSS"
        );

        // Verify that code content is preserved
        assert!(
            final_html.contains("println"),
            "Code content should be preserved"
        );
        assert!(
            final_html.contains("main"),
            "Code content should be preserved"
        );

        // Verify that the code now has proper line structure (not all on one line)
        let println_context = if let Some(start) = final_html.find("println") {
            &final_html[start.saturating_sub(100)..start + 200.min(final_html.len() - start)]
        } else {
            ""
        };

        // The content should have breaks between lines - either <br> tags or actual newlines preserved
        assert!(
            println_context.contains("<br>")
                || println_context.contains("white-space: pre")
                || final_html.contains("<br>"),
            "Code should have line breaks. Context: {println_context}"
        );
    }

    // Helper function to count CSS variables in a theme
    fn theme_css_var_count(css: &str) -> usize {
        css.matches("var(--").count()
    }

    // Helper function to get embedded CSS for testing
    fn get_embedded_theme_css(theme: BuiltinTheme) -> &'static str {
        match theme {
            BuiltinTheme::Default => DEFAULT_CSS,
            BuiltinTheme::Lapis => LAPIS_CSS,
            BuiltinTheme::Maize => MAIZE_CSS,
            BuiltinTheme::OrangeHeart => ORANGEHEART_CSS,
            BuiltinTheme::PhyCat => PHYCAT_CSS,
            BuiltinTheme::Pie => PIE_CSS,
            BuiltinTheme::Purple => PURPLE_CSS,
            BuiltinTheme::Rainbow => RAINBOW_CSS,
        }
    }
}