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 #[serde(default)]
18 pub last_edit_from: String,
19 #[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 #[serde(default, alias = "PAGE_TITLE")]
73 #[validate(max_length = 128)]
74 pub page_title: String,
75 #[serde(default, alias = "PAGE_DESCRIPTION")]
77 #[validate(max_length = 256)]
78 pub page_description: String,
79 #[serde(default, alias = "PAGE_ICON")]
81 #[validate(max_length = 128)]
82 pub page_icon: String,
83 #[serde(default, alias = "SHARE_TITLE")]
85 #[validate(max_length = 128)]
86 pub share_title: String,
87 #[serde(default, alias = "SHARE_DESCRIPTION")]
89 #[validate(max_length = 256)]
90 pub share_description: String,
91 #[serde(default, alias = "SHARE_IMAGE")]
93 #[validate(max_length = 128)]
94 pub share_image: String,
95 #[serde(default, alias = "OPTION_DISABLE_VIEWS")]
97 pub option_disable_views: bool,
98 #[serde(default, alias = "OPTION_DISABLE_SEARCH_ENGINE")]
100 pub option_disable_search_engine: bool,
101 #[serde(default, alias = "OPTION_VIEW_PASSWORD")]
103 pub option_view_password: String,
104 #[serde(default, alias = "OPTION_SOURCE_PASSWORD")]
109 pub option_source_password: String,
110 #[serde(default, alias = "ACCESS_RECOMMENDED_THEME")]
112 pub access_recommended_theme: RecommendedTheme,
113 #[serde(default, alias = "ACCESS_EASY_READ")]
117 #[validate(max_length = 32)]
118 pub access_easy_read: String,
119 #[serde(default, alias = "CONTAINER_PADDING")]
123 #[validate(max_length = 32)]
124 pub container_padding: String,
125 #[serde(default, alias = "CONTAINER_MAX_WIDTH")]
129 #[validate(max_length = 16)]
130 pub container_max_width: String,
131 #[serde(default, alias = "CONTAINER_INNER_FOREGROUND_COLOR")]
134 #[validate(max_length = 32)]
135 pub container_inner_foreground_color: String,
136 #[serde(default, alias = "CONTAINER_INNER_BACKGROUND")]
140 #[validate(max_length = 256)]
141 pub container_inner_background: String,
142 #[serde(default, alias = "CONTAINER_INNER_BACKGROUND_COLOR")]
146 #[validate(max_length = 32)]
147 pub container_inner_background_color: String,
148 #[serde(default, alias = "CONTAINER_INNER_BACKGROUND_IMAGE")]
152 #[validate(max_length = 128)]
153 pub container_inner_background_image: String,
154 #[serde(default, alias = "CONTAINER_INNER_BACKGROUND_IMAGE_REPEAT")]
158 #[validate(max_length = 16)]
159 pub container_inner_background_image_repeat: String,
160 #[serde(default, alias = "CONTAINER_INNER_BACKGROUND_IMAGE_POSITION")]
164 #[validate(max_length = 16)]
165 pub container_inner_background_image_position: String,
166 #[serde(default, alias = "CONTAINER_INNER_BACKGROUND_IMAGE_SIZE")]
170 #[validate(max_length = 16)]
171 pub container_inner_background_image_size: String,
172 #[serde(default, alias = "CONTAINER_OUTER_FOREGROUND_COLOR")]
174 #[validate(max_length = 32)]
175 pub container_outer_foreground_color: String,
176 #[serde(default, alias = "CONTAINER_OUTER_BACKGROUND")]
180 #[validate(max_length = 256)]
181 pub container_outer_background: String,
182 #[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_COLOR")]
186 #[validate(max_length = 32)]
187 pub container_outer_background_color: String,
188 #[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_IMAGE")]
192 #[validate(max_length = 128)]
193 pub container_outer_background_image: String,
194 #[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_IMAGE_REPEAT")]
198 #[validate(max_length = 16)]
199 pub container_outer_background_image_repeat: String,
200 #[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_IMAGE_POSITION")]
204 #[validate(max_length = 16)]
205 pub container_outer_background_image_position: String,
206 #[serde(default, alias = "CONTAINER_OUTER_BACKGROUND_IMAGE_SIZE")]
210 #[validate(max_length = 16)]
211 pub container_outer_background_image_size: String,
212 #[serde(default, alias = "CONTAINER_BORDER")]
216 #[validate(max_length = 256)]
217 pub container_border: String,
218 #[serde(default, alias = "CONTAINER_BORDER_IMAGE")]
222 #[validate(max_length = 128)]
223 pub container_border_image: String,
224 #[serde(default, alias = "CONTAINER_BORDER_IMAGE_SLICE")]
228 #[validate(max_length = 16)]
229 pub container_border_image_slice: String,
230 #[serde(default, alias = "CONTAINER_BORDER_IMAGE_WIDTH")]
234 #[validate(max_length = 16)]
235 pub container_border_image_width: String,
236 #[serde(default, alias = "CONTAINER_BORDER_IMAGE_OUTSET")]
240 #[validate(max_length = 32)]
241 pub container_border_image_outset: String,
242 #[serde(default, alias = "CONTAINER_BORDER_IMAGE_REPEAT")]
246 #[validate(max_length = 16)]
247 pub container_border_image_repeat: String,
248 #[serde(default, alias = "CONTAINER_BORDER_COLOR")]
252 #[validate(max_length = 32)]
253 pub container_border_color: String,
254 #[serde(default, alias = "CONTAINER_BORDER_WIDTH")]
258 #[validate(max_length = 16)]
259 pub container_border_width: String,
260 #[serde(default, alias = "CONTAINER_BORDER_STYLE")]
264 #[validate(max_length = 16)]
265 pub container_border_style: String,
266 #[serde(default, alias = "CONTAINER_BORDER_RADIUS")]
270 #[validate(max_length = 16)]
271 pub container_border_radius: String,
272 #[serde(default, alias = "CONTAINER_SHADOW")]
276 #[validate(max_length = 32)]
277 pub container_shadow: String,
278 #[serde(default, alias = "CONTAINER_FLEX")]
280 pub container_flex: bool,
281 #[serde(default, alias = "CONTAINER_FLEX_DIRECTION")]
283 #[validate(max_length = 16)]
284 pub container_flex_direction: String,
285 #[serde(default, alias = "CONTAINER_FLEX_GAP")]
289 #[validate(max_length = 16)]
290 pub container_flex_gap: String,
291 #[serde(default, alias = "CONTAINER_FLEX_WRAP")]
295 pub container_flex_wrap: bool,
296 #[serde(default, alias = "CONTAINER_FLEX_ALIGN_HORIZONTAL")]
302 #[validate(max_length = 16)]
303 pub container_flex_align_horizontal: String,
304 #[serde(default, alias = "CONTAINER_FLEX_ALIGN_VERTICAL")]
310 #[validate(max_length = 16)]
311 pub container_flex_align_vertical: String,
312 #[serde(default, alias = "CONTENT_TEXT_SHADOW")]
316 #[validate(max_length = 32)]
317 pub content_text_shadow: String,
318 #[serde(default, alias = "CONTENT_TEXT_SHADOW_COLOR")]
322 #[validate(max_length = 32)]
323 pub content_text_shadow_color: String,
324 #[serde(default, alias = "CONTENT_TEXT_SHADOW_OFFSET")]
328 #[validate(max_length = 32)]
329 pub content_text_shadow_offset: String,
330 #[serde(default, alias = "CONTENT_TEXT_SHADOW_BLUR")]
334 #[validate(max_length = 32)]
335 pub content_text_shadow_blur: String,
336 #[serde(default, alias = "CONTENT_FONT")]
338 #[validate(max_length = 128)]
339 pub content_font: String,
340 #[serde(default, alias = "CONTENT_FONT_WEIGHT")]
342 pub content_font_weight: u32,
343 #[serde(default, alias = "CONTENT_TEXT_SIZE")]
345 pub content_text_size: String,
346 #[serde(default, alias = "CONTENT_TEXT_SIZE_ARRAY")]
354 pub content_text_size_array: Vec<(String, String)>,
355 #[serde(default, alias = "CONTENT_TEXT_ALIGN")]
357 pub content_text_align: TextAlignment,
358 #[serde(default, alias = "CONTENT_TEXT_COLOR")]
360 pub content_text_color: String,
361 #[serde(default, alias = "CONTENT_LINK_COLOR")]
363 pub content_link_color: String,
364 #[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 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 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 output.push_str(&format!("{key} {value}\n"));
682 }
683
684 output
685 }
686}