fluffle/
model.rs

1use crate::markdown::is_numeric;
2use serde::{Deserialize, Serialize};
3use serde_valid::Validate;
4use std::fmt::Display;
5
6#[derive(Serialize, Deserialize)]
7pub struct Entry {
8    pub slug: String,
9    pub edit_code: String,
10    pub salt: String,
11    pub created: usize,
12    pub edited: usize,
13    pub content: String,
14    #[serde(default)]
15    pub metadata: String,
16    /// The IP address of the last editor of the entry.
17    #[serde(default)]
18    pub last_edit_from: String,
19    /// An edit code that can only be used to change the entry's content.
20    #[serde(default)]
21    pub modify_code: String,
22}
23
24#[derive(Serialize, Deserialize, PartialEq, Eq)]
25pub enum RecommendedTheme {
26    #[serde(alias = "none")]
27    None,
28    #[serde(alias = "light")]
29    Light,
30    #[serde(alias = "dark")]
31    Dark,
32}
33
34impl Default for RecommendedTheme {
35    fn default() -> Self {
36        Self::None
37    }
38}
39
40#[derive(Serialize, Deserialize, PartialEq, Eq)]
41pub enum TextAlignment {
42    #[serde(alias = "left")]
43    Left,
44    #[serde(alias = "center")]
45    Center,
46    #[serde(alias = "right")]
47    Right,
48    #[serde(alias = "justify")]
49    Justify,
50}
51
52impl Default for TextAlignment {
53    fn default() -> Self {
54        Self::Left
55    }
56}
57
58impl Display for TextAlignment {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        f.write_str(match self {
61            Self::Left => "left",
62            Self::Center => "center",
63            Self::Right => "right",
64            Self::Justify => "justify",
65        })
66    }
67}
68
69#[derive(Serialize, Deserialize, Validate, Default)]
70pub struct EntryMetadata {
71    /// The title of the page.
72    #[serde(default, alias = "PAGE_TITLE")]
73    #[validate(max_length = 128)]
74    pub page_title: String,
75    /// The description of the page.
76    #[serde(default, alias = "PAGE_DESCRIPTION")]
77    #[validate(max_length = 256)]
78    pub page_description: String,
79    /// The favicon of the page.
80    #[serde(default, alias = "PAGE_ICON")]
81    #[validate(max_length = 128)]
82    pub page_icon: String,
83    /// The title of the page shown in external embeds.
84    #[serde(default, alias = "SHARE_TITLE")]
85    #[validate(max_length = 128)]
86    pub share_title: String,
87    /// The description of the page shown in external embeds.
88    #[serde(default, alias = "SHARE_DESCRIPTION")]
89    #[validate(max_length = 256)]
90    pub share_description: String,
91    /// The image shown in external embeds.
92    #[serde(default, alias = "SHARE_IMAGE")]
93    #[validate(max_length = 128)]
94    pub share_image: String,
95    /// If views are counted and shown for this entry.
96    #[serde(default, alias = "OPTION_DISABLE_VIEWS")]
97    pub option_disable_views: bool,
98    /// Hides this entry for search results.
99    #[serde(default, alias = "OPTION_DISABLE_SEARCH_ENGINE")]
100    pub option_disable_search_engine: bool,
101    /// The password that is required to view this entry.
102    #[serde(default, alias = "OPTION_VIEW_PASSWORD")]
103    pub option_view_password: String,
104    /// The password that is required to view the source of the entry.
105    ///
106    /// If no password is provided but a view password IS provided, the view
107    /// password will be used.
108    #[serde(default, alias = "OPTION_SOURCE_PASSWORD")]
109    pub option_source_password: String,
110    /// The theme that is automatically used when this entry is viewed.
111    #[serde(default, alias = "ACCESS_RECOMMENDED_THEME")]
112    pub access_recommended_theme: RecommendedTheme,
113    /// The slug of the easy-to-read (no metadata) version of this entry.
114    ///
115    /// Should not begin with "/".
116    #[serde(default, alias = "ACCESS_EASY_READ")]
117    #[validate(max_length = 32)]
118    pub access_easy_read: String,
119    /// The padding of the container.
120    ///
121    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/padding>
122    #[serde(default, alias = "CONTAINER_PADDING")]
123    #[validate(max_length = 32)]
124    pub container_padding: String,
125    /// The maximum width of the container.
126    ///
127    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/max-width>
128    #[serde(default, alias = "CONTAINER_MAX_WIDTH")]
129    #[validate(max_length = 16)]
130    pub container_max_width: String,
131    /// The padding of the container.
132    /// The color of the text in the inner container.
133    #[serde(default, alias = "CONTAINER_INNER_FOREGROUND_COLOR")]
134    #[validate(max_length = 32)]
135    pub container_inner_foreground_color: String,
136    /// The background of the inner container.
137    ///
138    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background>
139    #[serde(default, alias = "CONTAINER_INNER_BACKGROUND")]
140    #[validate(max_length = 256)]
141    pub container_inner_background: String,
142    /// The background of the inner container.
143    ///
144    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background-color>
145    #[serde(default, alias = "CONTAINER_INNER_BACKGROUND_COLOR")]
146    #[validate(max_length = 32)]
147    pub container_inner_background_color: String,
148    /// The background of the inner container.
149    ///
150    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background-image>
151    #[serde(default, alias = "CONTAINER_INNER_BACKGROUND_IMAGE")]
152    #[validate(max_length = 128)]
153    pub container_inner_background_image: String,
154    /// The background of the inner container.
155    ///
156    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background-repeat>
157    #[serde(default, alias = "CONTAINER_INNER_BACKGROUND_IMAGE_REPEAT")]
158    #[validate(max_length = 16)]
159    pub container_inner_background_image_repeat: String,
160    /// The background of the inner container.
161    ///
162    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background-position>
163    #[serde(default, alias = "CONTAINER_INNER_BACKGROUND_IMAGE_POSITION")]
164    #[validate(max_length = 16)]
165    pub container_inner_background_image_position: String,
166    /// The background of the inner container.
167    ///
168    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background-size>
169    #[serde(default, alias = "CONTAINER_INNER_BACKGROUND_IMAGE_SIZE")]
170    #[validate(max_length = 16)]
171    pub container_inner_background_image_size: String,
172    /// The color of the text in the outer container.
173    #[serde(default, alias = "CONTAINER_OUTER_FOREGROUND_COLOR")]
174    #[validate(max_length = 32)]
175    pub container_outer_foreground_color: String,
176    /// The background of the outer container.
177    ///
178    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background>
179    #[serde(default, alias = "CONTAINER_OUTER_BACKGROUND")]
180    #[validate(max_length = 256)]
181    pub container_outer_background: String,
182    /// The background of the outer container.
183    ///
184    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background-color>
185    #[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_COLOR")]
186    #[validate(max_length = 32)]
187    pub container_outer_background_color: String,
188    /// The background of the outer container.
189    ///
190    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background-image>
191    #[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_IMAGE")]
192    #[validate(max_length = 128)]
193    pub container_outer_background_image: String,
194    /// The background of the outer container.
195    ///
196    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background-repeat>
197    #[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_IMAGE_REPEAT")]
198    #[validate(max_length = 16)]
199    pub container_outer_background_image_repeat: String,
200    /// The background of the outer container.
201    ///
202    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background-position>
203    #[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_IMAGE_POSITION")]
204    #[validate(max_length = 16)]
205    pub container_outer_background_image_position: String,
206    /// The background of the outer container.
207    ///
208    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/background-size>
209    #[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_IMAGE_SIZE")]
210    #[validate(max_length = 16)]
211    pub container_outer_background_image_size: String,
212    /// The border around the container.
213    ///
214    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border>
215    #[serde(default, alias = "CONTAINER_BORDER")]
216    #[validate(max_length = 256)]
217    pub container_border: String,
218    /// The border around the container.
219    ///
220    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border-image>
221    #[serde(default, alias = "CONTAINER_BORDER_IMAGE")]
222    #[validate(max_length = 128)]
223    pub container_border_image: String,
224    /// The border around the container.
225    ///
226    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border-image-slice>
227    #[serde(default, alias = "CONTAINER_BORDER_IMAGE_SLICE")]
228    #[validate(max_length = 16)]
229    pub container_border_image_slice: String,
230    /// The border around the container.
231    ///
232    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border-image-width>
233    #[serde(default, alias = "CONTAINER_BORDER_IMAGE_WIDTH")]
234    #[validate(max_length = 16)]
235    pub container_border_image_width: String,
236    /// The border around the container.
237    ///
238    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border-image-outset>
239    #[serde(default, alias = "CONTAINER_BORDER_IMAGE_OUTSET")]
240    #[validate(max_length = 32)]
241    pub container_border_image_outset: String,
242    /// The border around the container.
243    ///
244    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border-image-repeat>
245    #[serde(default, alias = "CONTAINER_BORDER_IMAGE_REPEAT")]
246    #[validate(max_length = 16)]
247    pub container_border_image_repeat: String,
248    /// The border around the container.
249    ///
250    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border-color>
251    #[serde(default, alias = "CONTAINER_BORDER_COLOR")]
252    #[validate(max_length = 32)]
253    pub container_border_color: String,
254    /// The border around the container.
255    ///
256    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border-width>
257    #[serde(default, alias = "CONTAINER_BORDER_WIDTH")]
258    #[validate(max_length = 16)]
259    pub container_border_width: String,
260    /// The border around the container.
261    ///
262    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border-style>
263    #[serde(default, alias = "CONTAINER_BORDER_STYLE")]
264    #[validate(max_length = 16)]
265    pub container_border_style: String,
266    /// The border around the container.
267    ///
268    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/border-radius>
269    #[serde(default, alias = "CONTAINER_BORDER_RADIUS")]
270    #[validate(max_length = 16)]
271    pub container_border_radius: String,
272    /// The shadow around the container.
273    ///
274    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow>
275    #[serde(default, alias = "CONTAINER_SHADOW")]
276    #[validate(max_length = 32)]
277    pub container_shadow: String,
278    /// If the container is a flexbox.
279    #[serde(default, alias = "CONTAINER_FLEX")]
280    pub container_flex: bool,
281    /// The direction of the container's content (if container has flex enabled).
282    #[serde(default, alias = "CONTAINER_FLEX_DIRECTION")]
283    #[validate(max_length = 16)]
284    pub container_flex_direction: String,
285    /// The gap of the container's content (if container has flex enabled).
286    ///
287    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/gap>
288    #[serde(default, alias = "CONTAINER_FLEX_GAP")]
289    #[validate(max_length = 16)]
290    pub container_flex_gap: String,
291    /// If the container's content wraps (if container has flex enabled).
292    ///
293    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/flex-wrap>
294    #[serde(default, alias = "CONTAINER_FLEX_WRAP")]
295    pub container_flex_wrap: bool,
296    /// The alignment of the container's content horizontally (if container has flex enabled).
297    ///
298    /// Swapped to vertical when direction is `column*`.
299    ///
300    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content>
301    #[serde(default, alias = "CONTAINER_FLEX_ALIGN_HORIZONTAL")]
302    #[validate(max_length = 16)]
303    pub container_flex_align_horizontal: String,
304    /// The alignment of the container's content vertically (if container has flex enabled).
305    ///
306    /// Swapped to horizontal when direction is `column*`.
307    ///
308    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/align-items>
309    #[serde(default, alias = "CONTAINER_FLEX_ALIGN_VERTICAL")]
310    #[validate(max_length = 16)]
311    pub container_flex_align_vertical: String,
312    /// The shadow under text.
313    ///
314    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/text-shadow>
315    #[serde(default, alias = "CONTENT_TEXT_SHADOW")]
316    #[validate(max_length = 32)]
317    pub content_text_shadow: String,
318    /// The shadow under text.
319    ///
320    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/text-shadow>
321    #[serde(default, alias = "CONTENT_TEXT_SHADOW_COLOR")]
322    #[validate(max_length = 32)]
323    pub content_text_shadow_color: String,
324    /// The shadow under text.
325    ///
326    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/text-shadow>
327    #[serde(default, alias = "CONTENT_TEXT_SHADOW_OFFSET")]
328    #[validate(max_length = 32)]
329    pub content_text_shadow_offset: String,
330    /// The shadow under text.
331    ///
332    /// Syntax: <https://developer.mozilla.org/en-US/docs/Web/CSS/text-shadow>
333    #[serde(default, alias = "CONTENT_TEXT_SHADOW_BLUR")]
334    #[validate(max_length = 32)]
335    pub content_text_shadow_blur: String,
336    /// The name of a font from Google Fonts to use.
337    #[serde(default, alias = "CONTENT_FONT")]
338    #[validate(max_length = 128)]
339    pub content_font: String,
340    /// The weight to use for the body text.
341    #[serde(default, alias = "CONTENT_FONT_WEIGHT")]
342    pub content_font_weight: u32,
343    /// The text size of elements (separated by space).
344    #[serde(default, alias = "CONTENT_TEXT_SIZE")]
345    pub content_text_size: String,
346    /// The text size of elements by element tag.
347    ///
348    /// # Example
349    /// ```toml
350    /// # ...
351    /// content_text_size = [["h1", "16px"]]
352    /// ```
353    #[serde(default, alias = "CONTENT_TEXT_SIZE_ARRAY")]
354    pub content_text_size_array: Vec<(String, String)>,
355    /// The default text alignment.
356    #[serde(default, alias = "CONTENT_TEXT_ALIGN")]
357    pub content_text_align: TextAlignment,
358    /// The base text color.
359    #[serde(default, alias = "CONTENT_TEXT_COLOR")]
360    pub content_text_color: String,
361    /// The color of links.
362    #[serde(default, alias = "CONTENT_LINK_COLOR")]
363    pub content_link_color: String,
364    /// If paragraph elements have a margin below them.
365    #[serde(default, alias = "CONTENT_DISABLE_PARAGRAPH_MARGIN")]
366    pub content_disable_paragraph_margin: bool,
367}
368
369macro_rules! metadata_css {
370    ($selector:expr, $property:literal, $self:ident.$field:ident->$output:ident) => {
371        if !$self.$field.is_empty() {
372            $output.push_str(&format!(
373                "{} {{ {}: {}; }}\n",
374                $selector,
375                $property,
376                EntryMetadata::css_escape(&$self.$field)
377            ));
378        }
379    };
380
381    ($selector:expr, $property:literal, $field:ident->$output:ident) => {
382        if !$field.is_empty() {
383            $output.push_str(&format!(
384                "{} {{ {}: {}; }}\n",
385                $selector,
386                $property,
387                EntryMetadata::css_escape(&$field)
388            ));
389        }
390    };
391
392    ($selector:expr, $property:literal !important, $self:ident.$field:ident->$output:ident) => {
393        if !$self.$field.is_empty() {
394            $output.push_str(&format!(
395                "{} {{ {}: {} !important; }}\n",
396                $selector,
397                $property,
398                EntryMetadata::css_escape(&$self.$field)
399            ));
400        }
401    };
402
403    ($selector:expr, $property:literal !important, $field:ident->$output:ident) => {
404        if !$field.is_empty() {
405            $output.push_str(&format!(
406                "{} {{ {}: {} !important; }}\n",
407                $selector,
408                $property,
409                EntryMetadata::css_escape(&$field)
410            ));
411        }
412    };
413
414    ($selector:expr, $property:literal, $format:literal, $self:ident.$field:ident->$output:ident) => {
415        if !$self.$field.is_empty() {
416            $output.push_str(&format!(
417                "{} {{ {}: {}; }}\n",
418                $selector,
419                $property,
420                format!($format, EntryMetadata::css_escape(&$self.$field))
421            ));
422        }
423    };
424}
425
426macro_rules! text_size {
427    ($selector:literal, $split:ident, $idx:literal, $output:ident) => {
428        if let Some(x) = $split.get($idx) {
429            if *x != "default" && *x != "0" {
430                metadata_css!($selector, "font-size" !important, x->$output);
431            }
432        }
433    }
434}
435
436impl EntryMetadata {
437    pub fn head_tags(&self) -> String {
438        let mut output = String::new();
439
440        if !self.page_title.is_empty() {
441            output.push_str(&format!("<title>{}</title>", self.page_title));
442        }
443
444        if !self.page_description.is_empty() {
445            output.push_str(&format!(
446                "<meta name=\"description\" content=\"{}\" />",
447                self.page_description.replace("\"", "\\\"")
448            ));
449        }
450
451        if !self.page_icon.is_empty() {
452            output.push_str(&format!(
453                "<link rel=\"icon\" href=\"{}\" />",
454                self.page_icon.replace("\"", "\\\"")
455            ));
456        }
457
458        if !self.share_title.is_empty() {
459            output.push_str(&format!(
460                "<meta property=\"og:title\" content=\"{}\" /><meta property=\"twitter:title\" content=\"{}\" />",
461                self.share_title.replace("\"", "\\\""),
462                self.share_title.replace("\"", "\\\"")
463            ));
464        }
465
466        if !self.share_description.is_empty() {
467            output.push_str(&format!(
468                "<meta property=\"og:description\" content=\"{}\" /><meta property=\"twitter:description\" content=\"{}\" />",
469                self.share_description.replace("\"", "\\\""),
470                self.share_description.replace("\"", "\\\"")
471            ));
472        }
473
474        if !self.share_image.is_empty() {
475            output.push_str(&format!(
476                "<meta property=\"og:image\" content=\"{}\" /><meta property=\"twitter:image\" content=\"{}\" />",
477                self.share_image.replace("\"", "\\\""),
478                self.share_image.replace("\"", "\\\"")
479            ));
480        }
481
482        if !self.content_font.is_empty() {
483            let s: Vec<&str> = self.content_font.split(" ").collect();
484
485            output.push_str(&format!(
486                "<link href=\"https://fonts.googleapis.com/css2?family={}&display=swap\" rel=\"stylesheet\">",
487                s[0].replace("_", "+"),
488            ));
489
490            if let Some(headings) = s.get(1) {
491                output.push_str(&format!(
492                    "<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">
493                    <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>
494                    <link href=\"https://fonts.googleapis.com/css2?family={}&display=swap\" rel=\"stylesheet\">",
495                    headings.replace("_", "+"),
496                ));
497            }
498        }
499
500        if self.option_disable_search_engine {
501            output.push_str("<meta name=\"robots\" content=\"noindex\">");
502        }
503
504        output
505    }
506
507    pub fn css_escape(input: &str) -> String {
508        input.replace("}", "").replace(";", "").replace("/*", "")
509    }
510
511    pub fn css(&self) -> String {
512        let mut output = "<style>".to_string();
513
514        metadata_css!(".container", "padding", self.container_padding->output);
515        metadata_css!(".container", "max-width", self.container_max_width->output);
516        metadata_css!(".container", "color", self.container_inner_foreground_color->output);
517        metadata_css!(".container", "background", self.container_inner_background->output);
518        metadata_css!(".container", "background-color", self.container_inner_background_color->output);
519        metadata_css!(".container", "background-image", "url('{}')", self.container_inner_background_image->output);
520        metadata_css!(".container", "background-repeat", self.container_inner_background_image_repeat->output);
521        metadata_css!(".container", "background-position", self.container_inner_background_image_position->output);
522        metadata_css!(".container", "background-size", self.container_inner_background_image_size->output);
523        metadata_css!("body", "color", self.container_outer_foreground_color->output);
524        metadata_css!("body", "background", self.container_outer_background->output);
525        metadata_css!("body", "background-color", self.container_outer_background_color->output);
526        metadata_css!("body", "background-image", "url('{}')", self.container_outer_background_image->output);
527        metadata_css!("body", "background-repeat", self.container_outer_background_image_repeat->output);
528        metadata_css!("body", "background-position", self.container_outer_background_image_position->output);
529        metadata_css!("body", "background-size", self.container_outer_background_image_size->output);
530        metadata_css!(".container", "border", self.container_border->output);
531        metadata_css!(".container", "border-image", "url('{}')", self.container_border_image->output);
532        metadata_css!(".container", "border-image-slice", self.container_border_image_slice->output);
533        metadata_css!(".container", "border-image-repeat", self.container_border_image_repeat->output);
534        metadata_css!(".container", "border-image-outset", self.container_border_image_outset->output);
535        metadata_css!(".container", "border-image-width", self.container_border_image_width->output);
536        metadata_css!(".container", "border-color", self.container_border_color->output);
537        metadata_css!(".container", "border-style", self.container_border_style->output);
538        metadata_css!(".container", "border-width", self.container_border_width->output);
539        metadata_css!(".container", "border-radius", self.container_border_radius->output);
540        metadata_css!(".container", "box-shadow", self.container_shadow->output);
541        metadata_css!(".container", "text-shadow", self.content_text_shadow->output);
542        metadata_css!("*, html *", "--color-link" !important, self.content_link_color->output);
543        metadata_css!("*, html *", "--color-text" !important, self.content_text_color->output);
544
545        if self.content_text_align != TextAlignment::Left {
546            output.push_str(&format!(
547                ".container {{ text-align: {}; }}\n",
548                EntryMetadata::css_escape(&self.content_text_align.to_string())
549            ));
550        }
551
552        for (element, size) in &self.content_text_size_array {
553            if element == "*" {
554                output.push_str(&format!(
555                    ".container, .container * {{ font-size: {}; }}\n",
556                    EntryMetadata::css_escape(&size)
557                ));
558
559                continue;
560            }
561
562            output.push_str(&format!(
563                ".container {} {{ font-size: {}; }}\n",
564                element,
565                EntryMetadata::css_escape(&size)
566            ));
567        }
568
569        if !self.content_text_size.is_empty() {
570            let split: Vec<&str> = self.content_text_size.split(" ").collect();
571            text_size!("body *", split, 0, output);
572            text_size!("body p", split, 1, output);
573            text_size!("body h1", split, 2, output);
574            text_size!("body h2", split, 3, output);
575            text_size!("body h3", split, 4, output);
576            text_size!("body h4", split, 5, output);
577            text_size!("body h5", split, 6, output);
578            text_size!("body h6", split, 7, output);
579            text_size!("body li", split, 8, output);
580            text_size!("body link", split, 9, output);
581            text_size!("body blockquote", split, 10, output);
582            text_size!("body code", split, 11, output);
583        }
584
585        if !self.content_font.is_empty() {
586            let s: Vec<&str> = self.content_font.split(" ").collect();
587
588            output.push_str(&format!(
589                ".container {{ font-family: \"{}\", system-ui; }}",
590                s[0].replace("_", " ")
591            ));
592
593            if let Some(headings) = s.get(1) {
594                output.push_str(&format!(
595                    ".container h1, .container h2, .container h3, .container h4, .container h5, .container h6 {{ font-family: \"{}\", system-ui; }}",
596                    headings.replace("_", " ")
597                ));
598            }
599        }
600
601        if self.content_font_weight != 0 {
602            output.push_str(&format!(
603                ".container {{ font-weight: {}; }}",
604                self.content_font_weight
605            ));
606        }
607
608        if !self.content_text_shadow_color.is_empty() {
609            output.push_str(&format!(
610                ".container {{ text-shadow: {} {} {}; }}",
611                self.content_text_shadow_offset,
612                self.content_text_shadow_blur,
613                self.content_text_shadow_color
614            ));
615        }
616
617        if self.container_flex {
618            output.push_str(".container { display: flex; }");
619            metadata_css!(".container", "gap", self.container_flex_gap->output);
620            metadata_css!(".container", "flex-direction", self.container_flex_direction->output);
621            metadata_css!(".container", "align-items", self.container_flex_align_vertical->output);
622            metadata_css!(".container", "justify-content", self.container_flex_align_horizontal->output);
623
624            if self.container_flex_wrap {
625                output.push_str(".container { flex-wrap: wrap; }");
626            }
627        }
628
629        if self.content_disable_paragraph_margin {
630            output.push_str(".container p { margin: 0; }");
631        }
632
633        output + "</style>"
634    }
635
636    pub fn ini_to_toml(input: &str) -> String {
637        let mut output = String::new();
638
639        for line in input.split("\n") {
640            if !line.contains("=") {
641                // no equal sign, skip line (some other toml function)
642                continue;
643            }
644
645            let mut key = String::new();
646            let mut value = String::new();
647
648            let mut chars = line.chars();
649            let mut in_value = false;
650
651            while let Some(char) = chars.next() {
652                if !in_value {
653                    key.push(char);
654
655                    if char == '=' {
656                        in_value = true;
657                    }
658
659                    continue;
660                } else {
661                    value.push(char);
662                    continue;
663                }
664            }
665
666            value = value.trim().to_string();
667
668            // determine if we need to stringify
669            let is_numeric = is_numeric(&value);
670            if !is_numeric
671                && !value.starts_with("[")
672                && !value.starts_with("\"")
673                && value != "true"
674                && value != "false"
675                || value.starts_with("#")
676            {
677                value = format!("\"{value}\"");
678            }
679
680            // push line
681            output.push_str(&format!("{key} {value}\n"));
682        }
683
684        output
685    }
686}