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 #[serde(default)]
24 pub last_edit_from: String,
25 #[serde(default)]
27 pub modify_code: String,
28 #[serde(default)]
29 pub views: usize,
30}
31
32impl Entry {
33 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 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 #[serde(default, alias = "PAGE_TITLE")]
115 #[validate(max_length = 128)]
116 pub page_title: String,
117 #[serde(default, alias = "PAGE_DESCRIPTION")]
119 #[validate(max_length = 256)]
120 pub page_description: String,
121 #[serde(default, alias = "PAGE_ICON")]
123 #[validate(max_length = 128)]
124 pub page_icon: String,
125 #[serde(default, alias = "SHARE_TITLE")]
127 #[validate(max_length = 128)]
128 pub share_title: String,
129 #[serde(default, alias = "SHARE_DESCRIPTION")]
131 #[validate(max_length = 256)]
132 pub share_description: String,
133 #[serde(default, alias = "SHARE_IMAGE")]
135 #[validate(max_length = 128)]
136 pub share_image: String,
137 #[serde(default, alias = "OPTION_DISABLE_VIEWS")]
139 pub option_disable_views: bool,
140 #[serde(default, alias = "OPTION_DISABLE_SEARCH_ENGINE")]
142 pub option_disable_search_engine: bool,
143 #[serde(default, alias = "OPTION_VIEW_PASSWORD")]
145 pub option_view_password: String,
146 #[serde(default, alias = "OPTION_SOURCE_PASSWORD")]
151 pub option_source_password: String,
152 #[serde(default, alias = "ACCESS_RECOMMENDED_THEME")]
154 pub access_recommended_theme: RecommendedTheme,
155 #[serde(default, alias = "ACCESS_EASY_READ")]
159 #[validate(max_length = 32)]
160 pub access_easy_read: String,
161 #[serde(default, alias = "CONTAINER_PADDING")]
165 #[validate(max_length = 32)]
166 pub container_padding: String,
167 #[serde(default, alias = "CONTAINER_MOBILE_PADDING")]
171 #[validate(max_length = 32)]
172 pub container_mobile_padding: String,
173 #[serde(default, alias = "CONTAINER_MAX_WIDTH")]
177 #[validate(max_length = 16)]
178 pub container_max_width: String,
179 #[serde(default, alias = "CONTAINER_MOBILE_MAX_WIDTH")]
183 #[validate(max_length = 16)]
184 pub container_mobile_max_width: String,
185 #[serde(default, alias = "CONTAINER_INNER_FOREGROUND_COLOR")]
188 #[validate(max_length = 32)]
189 pub container_inner_foreground_color: String,
190 #[serde(default, alias = "CONTAINER_INNER_BACKGROUND")]
194 #[validate(max_length = 256)]
195 pub container_inner_background: String,
196 #[serde(default, alias = "CONTAINER_INNER_BACKGROUND_COLOR")]
200 #[validate(max_length = 32)]
201 pub container_inner_background_color: String,
202 #[serde(default, alias = "CONTAINER_INNER_BACKGROUND_IMAGE")]
206 #[validate(max_length = 128)]
207 pub container_inner_background_image: String,
208 #[serde(default, alias = "CONTAINER_INNER_BACKGROUND_IMAGE_REPEAT")]
212 #[validate(max_length = 16)]
213 pub container_inner_background_image_repeat: String,
214 #[serde(default, alias = "CONTAINER_INNER_BACKGROUND_IMAGE_POSITION")]
218 #[validate(max_length = 16)]
219 pub container_inner_background_image_position: String,
220 #[serde(default, alias = "CONTAINER_INNER_BACKGROUND_IMAGE_SIZE")]
224 #[validate(max_length = 16)]
225 pub container_inner_background_image_size: String,
226 #[serde(default, alias = "CONTAINER_OUTER_FOREGROUND_COLOR")]
228 #[validate(max_length = 32)]
229 pub container_outer_foreground_color: String,
230 #[serde(default, alias = "CONTAINER_OUTER_BACKGROUND")]
234 #[validate(max_length = 256)]
235 pub container_outer_background: String,
236 #[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_COLOR")]
240 #[validate(max_length = 32)]
241 pub container_outer_background_color: String,
242 #[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_IMAGE")]
246 #[validate(max_length = 128)]
247 pub container_outer_background_image: String,
248 #[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_IMAGE_REPEAT")]
252 #[validate(max_length = 16)]
253 pub container_outer_background_image_repeat: String,
254 #[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_IMAGE_POSITION")]
258 #[validate(max_length = 16)]
259 pub container_outer_background_image_position: String,
260 #[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_IMAGE_SIZE")]
264 #[validate(max_length = 16)]
265 pub container_outer_background_image_size: String,
266 #[serde(default, alias = "CONTAINER_BORDER")]
270 #[validate(max_length = 256)]
271 pub container_border: String,
272 #[serde(default, alias = "CONTAINER_BORDER_IMAGE")]
276 #[validate(max_length = 128)]
277 pub container_border_image: String,
278 #[serde(default, alias = "CONTAINER_BORDER_IMAGE_SLICE")]
282 #[validate(max_length = 16)]
283 pub container_border_image_slice: String,
284 #[serde(default, alias = "CONTAINER_BORDER_IMAGE_WIDTH")]
288 #[validate(max_length = 16)]
289 pub container_border_image_width: String,
290 #[serde(default, alias = "CONTAINER_BORDER_IMAGE_OUTSET")]
294 #[validate(max_length = 32)]
295 pub container_border_image_outset: String,
296 #[serde(default, alias = "CONTAINER_BORDER_IMAGE_REPEAT")]
300 #[validate(max_length = 16)]
301 pub container_border_image_repeat: String,
302 #[serde(default, alias = "CONTAINER_BORDER_COLOR")]
306 #[validate(max_length = 32)]
307 pub container_border_color: String,
308 #[serde(default, alias = "CONTAINER_BORDER_WIDTH")]
312 #[validate(max_length = 16)]
313 pub container_border_width: String,
314 #[serde(default, alias = "CONTAINER_MOBILE_BORDER_WIDTH")]
318 #[validate(max_length = 16)]
319 pub container_mobile_border_width: String,
320 #[serde(default, alias = "CONTAINER_BORDER_STYLE")]
324 #[validate(max_length = 16)]
325 pub container_border_style: String,
326 #[serde(default, alias = "CONTAINER_BORDER_RADIUS")]
330 #[validate(max_length = 16)]
331 pub container_border_radius: String,
332 #[serde(default, alias = "CONTAINER_MOBILE_BORDER_RADIUS")]
336 #[validate(max_length = 16)]
337 pub container_mobile_border_radius: String,
338 #[serde(default, alias = "CONTAINER_SHADOW")]
342 #[validate(max_length = 32)]
343 pub container_shadow: String,
344 #[serde(default, alias = "CONTAINER_FLEX")]
346 pub container_flex: bool,
347 #[serde(default, alias = "CONTAINER_FLEX_DIRECTION")]
349 #[validate(max_length = 16)]
350 pub container_flex_direction: String,
351 #[serde(default, alias = "CONTAINER_FLEX_GAP")]
355 #[validate(max_length = 16)]
356 pub container_flex_gap: String,
357 #[serde(default, alias = "CONTAINER_FLEX_WRAP")]
361 pub container_flex_wrap: bool,
362 #[serde(default, alias = "CONTAINER_FLEX_ALIGN_HORIZONTAL")]
368 #[validate(max_length = 16)]
369 pub container_flex_align_horizontal: String,
370 #[serde(default, alias = "CONTAINER_FLEX_ALIGN_VERTICAL")]
376 #[validate(max_length = 16)]
377 pub container_flex_align_vertical: String,
378 #[serde(default, alias = "CONTENT_TEXT_SHADOW")]
382 #[validate(max_length = 32)]
383 pub content_text_shadow: String,
384 #[serde(default, alias = "CONTENT_TEXT_SHADOW_COLOR")]
388 #[validate(max_length = 32)]
389 pub content_text_shadow_color: String,
390 #[serde(default, alias = "CONTENT_TEXT_SHADOW_OFFSET")]
394 #[validate(max_length = 32)]
395 pub content_text_shadow_offset: String,
396 #[serde(default, alias = "CONTENT_TEXT_SHADOW_BLUR")]
400 #[validate(max_length = 32)]
401 pub content_text_shadow_blur: String,
402 #[serde(default, alias = "CONTENT_FONT")]
404 #[validate(max_length = 128)]
405 pub content_font: String,
406 #[serde(default, alias = "CONTENT_FONT_WEIGHT")]
408 pub content_font_weight: u32,
409 #[serde(default, alias = "CONTENT_TEXT_SIZE")]
411 #[validate(max_length = 128)]
412 pub content_text_size: String,
413 #[serde(default, alias = "CONTENT_TEXT_SIZE_ARRAY")]
421 pub content_text_size_array: Vec<(String, String)>,
422 #[serde(default, alias = "CONTENT_TEXT_ALIGN")]
424 pub content_text_align: TextAlignment,
425 #[serde(default, alias = "CONTENT_TEXT_COLOR")]
427 #[validate(max_length = 128)]
428 pub content_text_color: String,
429 #[serde(default, alias = "CONTENT_LINK_COLOR")]
431 #[validate(max_length = 128)]
432 pub content_link_color: String,
433 #[serde(default, alias = "CONTENT_DISABLE_PARAGRAPH_MARGIN")]
435 pub content_disable_paragraph_margin: bool,
436 #[serde(default, alias = "SAFETY_CONTENT_WARNING")]
438 #[validate(max_length = 512)]
439 pub safety_content_warning: String,
440 #[serde(default, alias = "TETRATTO_OWNER_USERNAME")]
445 #[validate(max_length = 32)]
446 pub tetratto_owner_username: String,
447 #[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 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 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 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 output.push_str(&format!("{key} {value}\n"));
826 }
827
828 output
829 }
830}
831
832#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
835pub enum QuickFlag {
836 AcceptWarning,
838 EntryHighContrast,
840}
841
842pub type QuickFlags = Vec<QuickFlag>;