fluffle/
model.rs

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