1use std::collections::{BTreeMap, HashMap};
2
3use serde::{Deserialize, Serialize};
4
5use crate::citation::Reference;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct SurfDoc {
10 pub front_matter: Option<FrontMatter>,
12 pub blocks: Vec<Block>,
14 pub source: String,
16}
17
18#[derive(Debug, Clone, Default, Serialize, Deserialize)]
22#[serde(default)]
23pub struct FrontMatter {
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub title: Option<String>,
26
27 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
28 pub doc_type: Option<DocType>,
29
30 #[serde(rename = "format", skip_serializing_if = "Option::is_none")]
33 pub format: Option<Format>,
34
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub status: Option<DocStatus>,
37
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub scope: Option<Scope>,
40
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub tags: Option<Vec<String>>,
43
44 #[serde(skip_serializing_if = "Option::is_none")]
45 pub created: Option<String>,
46
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub updated: Option<String>,
49
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub author: Option<String>,
52
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub confidence: Option<Confidence>,
55
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub related: Option<Vec<Related>>,
58
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub version: Option<u32>,
61
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub contributors: Option<Vec<String>>,
64
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub description: Option<String>,
67
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub workspace: Option<String>,
70
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub decision: Option<String>,
73
74 #[serde(flatten)]
76 pub extra: HashMap<String, serde_yaml::Value>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct Related {
82 pub path: String,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub relationship: Option<Relationship>,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
89#[serde(rename_all = "lowercase")]
90pub enum Relationship {
91 Produces,
92 Consumes,
93 References,
94 Supersedes,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
99#[serde(rename_all = "lowercase")]
100pub enum DocType {
101 Doc,
102 Guide,
103 Conversation,
104 Plan,
105 Agent,
106 Preference,
107 Report,
108 Proposal,
109 Incident,
110 Review,
111 App,
112 Manifest,
113 Website,
115 Web,
120 Deck,
122 Slides,
124 Presentation,
127 Paper,
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
140#[serde(rename_all = "lowercase")]
141pub enum Format {
142 #[serde(alias = "IEEE", alias = "Ieee")]
144 Ieee,
145 #[serde(alias = "ACM", alias = "Acm")]
147 Acm,
148 #[serde(alias = "Article", alias = "ARTICLE")]
150 Article,
151 #[serde(alias = "MLA", alias = "Mla")]
153 Mla,
154 #[serde(alias = "APA", alias = "Apa")]
156 Apa,
157 #[serde(alias = "Chicago", alias = "CHICAGO")]
159 Chicago,
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub enum RenderProfile {
172 Document,
174 Web,
176 Presentation,
178 Paper(Format),
180 Report(Format),
182}
183
184pub fn render_profile(doc_type: Option<DocType>, format: Option<Format>) -> RenderProfile {
191 match doc_type {
192 None => RenderProfile::Document,
193 Some(dt) => match dt {
194 DocType::Website | DocType::Web => RenderProfile::Web,
195 DocType::Deck | DocType::Slides | DocType::Presentation => {
196 RenderProfile::Presentation
197 }
198 DocType::Paper => RenderProfile::Paper(format.unwrap_or(Format::Article)),
199 DocType::Report => RenderProfile::Report(format.unwrap_or(Format::Mla)),
200 DocType::Doc
201 | DocType::Guide
202 | DocType::Conversation
203 | DocType::Plan
204 | DocType::Agent
205 | DocType::Preference
206 | DocType::Proposal
207 | DocType::Incident
208 | DocType::Review
209 | DocType::App
210 | DocType::Manifest => RenderProfile::Document,
211 },
212 }
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
217#[serde(rename_all = "lowercase")]
218pub enum DocStatus {
219 Draft,
220 Active,
221 Closed,
222 Archived,
223}
224
225#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
227#[serde(rename_all = "kebab-case")]
228pub enum Scope {
229 Personal,
230 WorkspacePrivate,
231 Workspace,
232 Repo,
233 Public,
234}
235
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
238#[serde(rename_all = "lowercase")]
239pub enum Confidence {
240 Low,
241 Medium,
242 High,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
247#[serde(tag = "kind")]
248pub enum Block {
249 Unknown {
251 name: String,
252 attrs: Attrs,
253 content: String,
254 span: Span,
255 },
256 Markdown {
258 content: String,
259 span: Span,
260 },
261 Callout {
263 callout_type: CalloutType,
264 title: Option<String>,
265 content: String,
266 span: Span,
267 },
268 Data {
270 id: Option<String>,
271 format: DataFormat,
272 sortable: bool,
273 headers: Vec<String>,
274 rows: Vec<Vec<String>>,
275 raw_content: String,
276 span: Span,
277 },
278 Code {
280 lang: Option<String>,
281 file: Option<String>,
282 highlight: Vec<String>,
283 content: String,
284 span: Span,
285 },
286 Tasks {
288 items: Vec<TaskItem>,
289 span: Span,
290 },
291 Decision {
293 status: DecisionStatus,
294 date: Option<String>,
295 deciders: Vec<String>,
296 content: String,
297 span: Span,
298 },
299 Metric {
301 label: String,
302 value: String,
303 trend: Option<Trend>,
304 unit: Option<String>,
305 span: Span,
306 },
307 Summary {
309 content: String,
310 span: Span,
311 },
312 Cite {
316 reference: Reference,
318 span: Span,
319 },
320 Bibliography {
324 style: Option<Format>,
326 span: Span,
327 },
328 Figure {
330 src: String,
331 caption: Option<String>,
332 alt: Option<String>,
333 width: Option<String>,
334 span: Span,
335 },
336 Diagram {
340 diagram_type: String,
342 title: Option<String>,
343 content: String,
344 span: Span,
345 },
346 Tabs {
348 tabs: Vec<TabPanel>,
349 span: Span,
350 },
351 Columns {
353 columns: Vec<ColumnContent>,
354 span: Span,
355 },
356 Quote {
358 content: String,
359 attribution: Option<String>,
360 cite: Option<String>,
361 span: Span,
362 },
363 Cta {
365 label: String,
366 href: String,
367 primary: bool,
368 icon: Option<String>,
369 span: Span,
370 },
371 Nav {
373 items: Vec<NavItem>,
374 logo: Option<String>,
375 #[serde(default)]
379 groups: Vec<NavGroup>,
380 #[serde(default)]
382 brand: Option<String>,
383 #[serde(default)]
385 brand_reg: bool,
386 #[serde(default)]
388 cta: Option<NavItem>,
389 #[serde(default)]
394 drawer: bool,
395 #[serde(default)]
398 minimal: bool,
399 span: Span,
400 },
401 HeroImage {
403 src: String,
404 alt: Option<String>,
405 span: Span,
406 },
407 Testimonial {
409 content: String,
410 author: Option<String>,
411 role: Option<String>,
412 company: Option<String>,
413 span: Span,
414 },
415 Style {
417 properties: Vec<StyleProperty>,
418 span: Span,
419 },
420 Faq {
422 items: Vec<FaqItem>,
423 span: Span,
424 },
425 PricingTable {
427 headers: Vec<String>,
428 rows: Vec<Vec<String>>,
429 span: Span,
430 },
431 Site {
433 domain: Option<String>,
434 properties: Vec<StyleProperty>,
435 span: Span,
436 },
437 Page {
439 route: String,
440 layout: Option<String>,
441 title: Option<String>,
442 sidebar: bool,
443 content: String,
445 children: Vec<Block>,
447 span: Span,
448 },
449 Deck {
456 properties: Vec<StyleProperty>,
457 span: Span,
458 },
459 Slide {
466 layout: Option<SlideLayout>,
467 kicker: Option<String>,
468 notes: Option<String>,
469 content: String,
471 children: Vec<Block>,
472 span: Span,
473 },
474 Embed {
476 src: String,
477 embed_type: Option<EmbedType>,
478 width: Option<String>,
479 height: Option<String>,
480 title: Option<String>,
481 span: Span,
482 },
483 Form {
485 fields: Vec<FormField>,
486 submit_label: Option<String>,
487 action: Option<String>,
490 method: Option<String>,
492 honeypot: bool,
494 span: Span,
495 },
496 Banner {
498 headline: Option<String>,
499 subtitle: Option<String>,
500 buttons: Vec<HeroButton>,
501 id: Option<String>,
503 content: String,
504 span: Span,
505 },
506 ProductGrid {
508 groups: Vec<ProductGroup>,
509 tiles: bool,
513 span: Span,
514 },
515 PostGrid {
517 title: Option<String>,
518 subtitle: Option<String>,
519 items: Vec<PostItem>,
520 span: Span,
521 },
522 Gate {
524 title: Option<String>,
525 subtitle: Option<String>,
526 action: String,
528 field_label: Option<String>,
529 submit_label: Option<String>,
530 error: Option<String>,
531 span: Span,
532 },
533 Gallery {
535 items: Vec<GalleryItem>,
536 columns: Option<u32>,
537 span: Span,
538 },
539 Footer {
541 sections: Vec<FooterSection>,
542 copyright: Option<String>,
543 social: Vec<SocialLink>,
544 #[serde(default)]
546 brand: Option<String>,
547 #[serde(default)]
549 brand_reg: bool,
550 #[serde(default)]
552 brand_logo: Option<String>,
553 #[serde(default)]
555 tagline: Option<String>,
556 span: Span,
557 },
558 Details {
560 title: Option<String>,
561 open: bool,
562 content: String,
563 span: Span,
564 },
565 Divider {
567 label: Option<String>,
568 span: Span,
569 },
570 Hero {
572 headline: Option<String>,
573 subtitle: Option<String>,
574 badge: Option<String>,
575 align: String,
576 image: Option<String>,
577 image_alt: Option<String>,
579 layout: Option<String>,
583 transparent: bool,
586 buttons: Vec<HeroButton>,
587 content: String,
588 span: Span,
589 },
590 Features {
592 cards: Vec<FeatureCard>,
593 cols: Option<u32>,
594 span: Span,
595 },
596 Steps {
598 steps: Vec<StepItem>,
599 span: Span,
600 },
601 Stats {
603 items: Vec<StatItem>,
604 span: Span,
605 },
606 Comparison {
608 headers: Vec<String>,
609 rows: Vec<Vec<String>>,
610 highlight: Option<String>,
611 span: Span,
612 },
613 Logo {
615 src: String,
616 alt: Option<String>,
617 size: Option<u32>,
618 span: Span,
619 },
620 Toc {
622 depth: u32,
623 entries: Vec<TocEntry>,
624 span: Span,
625 },
626 BeforeAfter {
628 before_items: Vec<BeforeAfterItem>,
629 after_items: Vec<BeforeAfterItem>,
630 transition: Option<String>,
631 span: Span,
632 },
633 Pipeline {
635 steps: Vec<PipelineStep>,
636 span: Span,
637 },
638 Section {
640 bg: Option<String>,
641 headline: Option<String>,
642 subtitle: Option<String>,
643 content: String,
644 children: Vec<Block>,
645 span: Span,
646 },
647 ProductCard {
649 title: String,
650 subtitle: Option<String>,
651 badge: Option<String>,
652 badge_color: Option<String>,
653 body: String,
654 features: Vec<String>,
655 cta_label: Option<String>,
656 cta_href: Option<String>,
657 span: Span,
658 },
659
660 List {
664 source: String,
665 display: ListDisplay,
666 item_template: String,
667 filters: Vec<ListFilter>,
668 sort: Option<SortSpec>,
669 preload: bool,
670 span: Span,
671 },
672 Board {
674 source: String,
675 columns: Vec<String>,
676 card_template: Option<String>,
677 preload: bool,
678 span: Span,
679 },
680 Action {
682 method: HttpMethod,
683 target: String,
684 label: String,
685 fields: Vec<FormField>,
686 confirm: Option<String>,
687 span: Span,
688 },
689 FilterBar {
691 target_selector: String,
692 fields: Vec<FilterField>,
693 span: Span,
694 },
695 Search {
697 source: String,
698 placeholder: Option<String>,
699 span: Span,
700 },
701 Dashboard {
703 source: String,
704 refresh: Option<u32>,
705 span: Span,
706 },
707 ChatInput {
709 action: String,
710 placeholder: Option<String>,
711 modes: Vec<String>,
712 span: Span,
713 },
714 Feed {
716 source: String,
717 stream: bool,
718 span: Span,
719 },
720 Store {
726 title: Option<String>,
727 currency: Option<String>,
729 items: Vec<StoreItem>,
730 span: Span,
731 },
732 Booking {
739 title: Option<String>,
740 service_label: Option<String>,
742 services: Vec<BookingService>,
743 days: Vec<BookingDay>,
744 span: Span,
745 },
746
747 Editor {
751 source: Option<String>,
752 lang: Option<String>,
753 preview: bool,
754 span: Span,
755 },
756 Chart {
758 chart_type: ChartType,
759 source: String,
760 period: Option<String>,
761 title: Option<String>,
763 data: Option<ChartData>,
767 span: Span,
768 },
769 SplitPane {
771 ratio: String,
772 span: Span,
773 },
774
775 App {
779 name: String,
780 binary: Option<String>,
781 region: Option<String>,
782 port: Option<u32>,
783 platform: Option<String>,
784 #[serde(default, skip_serializing_if = "Option::is_none")]
788 auth: Option<String>,
789 content: String,
790 children: Vec<Block>,
791 span: Span,
792 },
793 Build {
795 base: Option<String>,
796 runtime: Option<String>,
797 edition: Option<String>,
798 properties: Vec<StyleProperty>,
799 span: Span,
800 },
801 InfraDatabase {
803 name: Option<String>,
804 shared_auth: bool,
805 volume_gb: Option<u32>,
806 properties: Vec<StyleProperty>,
807 span: Span,
808 },
809 Deploy {
811 env: Option<String>,
812 app: Option<String>,
813 machines: Option<u32>,
814 memory: Option<u32>,
815 auto_stop: Option<String>,
816 min_machines: Option<u32>,
817 strategy: Option<String>,
818 properties: Vec<StyleProperty>,
819 span: Span,
820 },
821 InfraEnv {
823 tier: Option<String>,
824 entries: Vec<EnvEntry>,
825 span: Span,
826 },
827 Health {
829 path: Option<String>,
830 method: Option<String>,
831 grace: Option<String>,
832 interval: Option<String>,
833 timeout: Option<String>,
834 span: Span,
835 },
836 Concurrency {
838 concurrency_type: Option<String>,
839 hard_limit: Option<u32>,
840 soft_limit: Option<u32>,
841 force_https: bool,
842 span: Span,
843 },
844 Cicd {
846 provider: Option<String>,
847 properties: Vec<StyleProperty>,
848 span: Span,
849 },
850 Smoke {
852 script: Option<String>,
853 checks: Vec<SmokeCheck>,
854 span: Span,
855 },
856 Domains {
858 entries: Vec<DomainEntry>,
859 span: Span,
860 },
861 Crates {
863 entries: Vec<CrateEntry>,
864 span: Span,
865 },
866 DeployUrls {
868 entries: Vec<StyleProperty>,
869 span: Span,
870 },
871 Volumes {
873 entries: Vec<VolumeEntry>,
874 span: Span,
875 },
876
877 Model {
881 name: String,
882 fields: Vec<ModelField>,
883 span: Span,
884 },
885 Route {
887 method: HttpMethod,
888 path: String,
889 auth: Option<String>,
890 returns: Option<String>,
891 body: Option<String>,
892 handler: Option<String>,
893 content: String,
894 span: Span,
895 },
896 Auth {
898 provider: AuthProvider,
899 session: Option<String>,
900 roles: Vec<String>,
901 default_role: Option<String>,
902 span: Span,
903 },
904 Binding {
906 source: String,
907 target: String,
908 events: Vec<BindingEvent>,
909 span: Span,
910 },
911
912 Schema {
916 name: String,
917 fields: Vec<SchemaField>,
918 span: Span,
919 },
920 Use {
922 crates: Vec<CrateDep>,
923 span: Span,
924 },
925 AppEnv {
927 vars: Vec<EnvVar>,
928 span: Span,
929 },
930 AppDeploy {
932 region: Option<String>,
933 scale: Option<u32>,
934 domain: Option<String>,
935 memory: Option<String>,
936 properties: Vec<(String, String)>,
937 span: Span,
938 },
939
940 Row {
944 icon: String,
945 title: String,
946 description: String,
947 href: Option<String>,
948 state: RowState,
949 span: Span,
950 },
951
952 InfoCard {
955 intent: String,
956 title: String,
957 subtitle: String,
958 summary: String,
959 image: Option<String>,
960 facts: Vec<[String; 2]>,
961 steps: Vec<String>,
962 state: RowState,
963 span: Span,
964 },
965
966 AppShell {
970 layout: String,
971 children: Vec<Block>,
972 span: Span,
973 },
974 Sidebar {
976 position: String,
977 collapsible: bool,
978 width: Option<u32>,
979 children: Vec<Block>,
980 span: Span,
981 },
982 Panel {
984 position: String,
985 resizable: bool,
986 height: Option<u32>,
987 desktop_only: bool,
988 children: Vec<Block>,
989 span: Span,
990 },
991 TabBar {
993 active: Option<String>,
994 items: Vec<TabBarItem>,
995 span: Span,
996 },
997 TabContent {
999 tab: String,
1000 children: Vec<Block>,
1001 span: Span,
1002 },
1003 Toolbar {
1005 items: Vec<ToolbarItem>,
1006 span: Span,
1007 },
1008 Drawer {
1010 name: String,
1011 position: String,
1012 width: Option<u32>,
1013 trigger: Option<String>,
1014 children: Vec<Block>,
1015 span: Span,
1016 },
1017 Modal {
1019 name: String,
1020 title: Option<String>,
1021 children: Vec<Block>,
1022 span: Span,
1023 },
1024 CommandPalette {
1026 trigger: Option<String>,
1027 items: Vec<CommandItem>,
1028 span: Span,
1029 },
1030 CodeEditor {
1032 lang: Option<String>,
1033 source: Option<String>,
1034 line_numbers: bool,
1035 content: String,
1036 span: Span,
1037 },
1038 BlockEditor {
1040 source: Option<String>,
1041 span: Span,
1042 },
1043 Terminal {
1045 shell: Option<String>,
1046 cwd: Option<String>,
1047 span: Span,
1048 },
1049 NavTree {
1051 source: Option<String>,
1052 on_select: Option<String>,
1053 on_rename: Option<String>,
1054 on_delete: Option<String>,
1055 span: Span,
1056 },
1057 Badge {
1059 value: String,
1060 color: Option<String>,
1061 span: Span,
1062 },
1063 SuggestionChips {
1065 source: Option<String>,
1066 max: Option<u32>,
1067 dismissible: bool,
1068 span: Span,
1069 },
1070 ChatThread {
1072 source: Option<String>,
1073 on_action: Option<String>,
1074 span: Span,
1075 },
1076 ChatInputSimple {
1078 placeholder: Option<String>,
1079 action: Option<String>,
1080 span: Span,
1081 },
1082 Progress {
1084 source: Option<String>,
1085 steps: Vec<ProgressStep>,
1086 span: Span,
1087 },
1088 LogStream {
1090 source: Option<String>,
1091 tail: Option<u32>,
1092 span: Span,
1093 },
1094 ProblemList {
1096 source: Option<String>,
1097 span: Span,
1098 },
1099}
1100
1101#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1103#[serde(rename_all = "lowercase")]
1104pub enum RowState {
1105 Default,
1106 Loading,
1107 Empty,
1108}
1109
1110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1112pub struct TabBarItem {
1113 pub id: String,
1114 pub label: String,
1115}
1116
1117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1119pub enum ToolbarItem {
1120 Button {
1121 label: Option<String>,
1122 action: Option<String>,
1123 icon: Option<String>,
1124 style: Option<String>,
1125 disabled: bool,
1126 },
1127 Separator,
1128 Spacer,
1129 Badge {
1130 value: String,
1131 color: Option<String>,
1132 },
1133 Dropdown {
1134 label: String,
1135 options: Option<String>,
1136 action: Option<String>,
1137 },
1138 Text {
1139 value: String,
1140 editable: bool,
1141 action: Option<String>,
1142 },
1143}
1144
1145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1147pub struct CommandItem {
1148 pub label: String,
1149 pub description: Option<String>,
1150 pub action: Option<String>,
1151 pub icon: Option<String>,
1152 pub group: Option<String>,
1153}
1154
1155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1157pub struct ProgressStep {
1158 pub label: String,
1159 pub status: String,
1161}
1162
1163#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1165#[serde(rename_all = "lowercase")]
1166pub enum CalloutType {
1167 Info,
1168 Warning,
1169 Danger,
1170 Tip,
1171 Note,
1172 Success,
1173 Context,
1174}
1175
1176#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1178#[serde(rename_all = "lowercase")]
1179pub enum DataFormat {
1180 Table,
1181 Csv,
1182 Json,
1183}
1184
1185#[derive(Debug, Clone, Serialize, Deserialize)]
1187pub struct TaskItem {
1188 pub done: bool,
1189 pub text: String,
1190 pub assignee: Option<String>,
1191}
1192
1193#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1195#[serde(rename_all = "lowercase")]
1196pub enum DecisionStatus {
1197 Proposed,
1198 Accepted,
1199 Rejected,
1200 Superseded,
1201}
1202
1203#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1205#[serde(rename_all = "lowercase")]
1206pub enum Trend {
1207 Up,
1208 Down,
1209 Flat,
1210}
1211
1212#[derive(Debug, Clone, Serialize, Deserialize)]
1214pub struct TabPanel {
1215 pub label: String,
1216 pub content: String,
1217}
1218
1219#[derive(Debug, Clone, Serialize, Deserialize)]
1221pub struct ColumnContent {
1222 pub content: String,
1223}
1224
1225#[derive(Debug, Clone, Serialize, Deserialize)]
1227pub struct StyleProperty {
1228 pub key: String,
1229 pub value: String,
1230}
1231
1232#[derive(Debug, Clone, Serialize, Deserialize)]
1234pub struct FaqItem {
1235 pub question: String,
1236 pub answer: String,
1237}
1238
1239#[derive(Debug, Clone, Serialize, Deserialize)]
1241pub struct NavItem {
1242 pub label: String,
1243 pub href: String,
1244 pub icon: Option<String>,
1245 #[serde(default)]
1248 pub image: Option<String>,
1249 #[serde(default)]
1252 pub external: bool,
1253}
1254
1255#[derive(Debug, Clone, Serialize, Deserialize)]
1261pub struct NavGroup {
1262 pub label: Option<String>,
1264 pub items: Vec<NavItem>,
1265}
1266
1267#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1269#[serde(rename_all = "lowercase")]
1270pub enum EmbedType {
1271 Map,
1272 Video,
1273 Audio,
1274 Generic,
1275}
1276
1277#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1283#[serde(rename_all = "lowercase")]
1284pub enum SlideLayout {
1285 Cover,
1287 Title,
1289 Section,
1291 #[default]
1293 Bullets,
1294 Cards,
1296 Compare,
1298 Stat,
1300 Quote,
1302 Demo,
1304 Image,
1306 Two,
1308 Code,
1310 Blank,
1312}
1313
1314impl SlideLayout {
1315 pub fn from_name(s: &str) -> Option<SlideLayout> {
1317 match s.trim().to_ascii_lowercase().as_str() {
1318 "cover" => Some(SlideLayout::Cover),
1319 "title" => Some(SlideLayout::Title),
1320 "section" => Some(SlideLayout::Section),
1321 "bullets" | "default" => Some(SlideLayout::Bullets),
1322 "cards" => Some(SlideLayout::Cards),
1323 "compare" | "comparison" => Some(SlideLayout::Compare),
1324 "stat" | "stats" => Some(SlideLayout::Stat),
1325 "quote" => Some(SlideLayout::Quote),
1326 "demo" => Some(SlideLayout::Demo),
1327 "image" => Some(SlideLayout::Image),
1328 "two" | "split" | "two-column" | "two-col" | "twocolumn" => Some(SlideLayout::Two),
1329 "code" => Some(SlideLayout::Code),
1330 "blank" => Some(SlideLayout::Blank),
1331 _ => None,
1332 }
1333 }
1334
1335 pub fn css_class(self) -> &'static str {
1337 match self {
1338 SlideLayout::Cover => "cover",
1339 SlideLayout::Title => "title",
1340 SlideLayout::Section => "section",
1341 SlideLayout::Bullets => "bullets",
1342 SlideLayout::Cards => "cards",
1343 SlideLayout::Compare => "compare",
1344 SlideLayout::Stat => "stat",
1345 SlideLayout::Quote => "quote",
1346 SlideLayout::Demo => "demo",
1347 SlideLayout::Image => "image",
1348 SlideLayout::Two => "two",
1349 SlideLayout::Code => "code",
1350 SlideLayout::Blank => "blank",
1351 }
1352 }
1353}
1354
1355#[derive(Debug, Clone, Serialize, Deserialize)]
1357pub struct FormField {
1358 pub label: String,
1359 pub name: String,
1360 pub field_type: FormFieldType,
1361 pub required: bool,
1362 pub placeholder: Option<String>,
1363 pub options: Vec<String>,
1364}
1365
1366#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1368#[serde(rename_all = "lowercase")]
1369pub enum FormFieldType {
1370 Text,
1371 Email,
1372 Tel,
1373 Date,
1374 Number,
1375 Password,
1376 Select,
1377 Textarea,
1378}
1379
1380#[derive(Debug, Clone, Serialize, Deserialize)]
1382pub struct GalleryItem {
1383 pub src: String,
1384 pub caption: Option<String>,
1385 pub alt: Option<String>,
1386 pub category: Option<String>,
1387}
1388
1389#[derive(Debug, Clone, Serialize, Deserialize)]
1391pub struct FooterSection {
1392 pub heading: String,
1393 pub links: Vec<NavItem>,
1394}
1395
1396#[derive(Debug, Clone, Serialize, Deserialize)]
1398pub struct SocialLink {
1399 pub platform: String,
1400 pub href: String,
1401}
1402
1403#[derive(Debug, Clone, Serialize, Deserialize)]
1405pub struct HeroButton {
1406 pub label: String,
1407 pub href: String,
1408 pub primary: bool,
1409 #[serde(default)]
1412 pub external: bool,
1413}
1414
1415#[derive(Debug, Clone, Serialize, Deserialize)]
1417pub struct ProductGroup {
1418 pub label: Option<String>,
1420 pub items: Vec<ProductItem>,
1421 #[serde(default, skip_serializing_if = "Option::is_none")]
1424 pub cols: Option<u8>,
1425}
1426
1427#[derive(Debug, Clone, Serialize, Deserialize)]
1429pub struct ProductItem {
1430 pub name: String,
1431 pub href: String,
1432 pub emblem: Option<String>,
1434 pub tagline: Option<String>,
1435 #[serde(default, skip_serializing_if = "Option::is_none")]
1438 pub cta1_label: Option<String>,
1439 #[serde(default, skip_serializing_if = "Option::is_none")]
1440 pub cta1_href: Option<String>,
1441 #[serde(default, skip_serializing_if = "Option::is_none")]
1444 pub cta2_label: Option<String>,
1445 #[serde(default, skip_serializing_if = "Option::is_none")]
1446 pub cta2_href: Option<String>,
1447 #[serde(default, skip_serializing_if = "Option::is_none")]
1452 pub bg: Option<String>,
1453}
1454
1455#[derive(Debug, Clone, Serialize, Deserialize)]
1457pub struct PostItem {
1458 pub title: String,
1459 pub href: String,
1460 pub meta: Option<String>,
1462 pub excerpt: Option<String>,
1464 pub image: Option<String>,
1466 pub external: bool,
1468}
1469
1470#[derive(Debug, Clone, Serialize, Deserialize)]
1472pub struct FeatureCard {
1473 pub title: String,
1474 pub icon: Option<String>,
1475 pub body: String,
1476 pub link_label: Option<String>,
1477 pub link_href: Option<String>,
1478}
1479
1480#[derive(Debug, Clone, Serialize, Deserialize)]
1482pub struct StepItem {
1483 pub title: String,
1484 pub time: Option<String>,
1485 pub body: String,
1486}
1487
1488#[derive(Debug, Clone, Serialize, Deserialize)]
1490pub struct StatItem {
1491 pub value: String,
1492 pub label: String,
1493 pub color: Option<String>,
1494}
1495
1496#[derive(Debug, Clone, Serialize, Deserialize)]
1498pub struct TocEntry {
1499 pub text: String,
1500 pub id: String,
1501 pub level: u32,
1502}
1503
1504#[derive(Debug, Clone, Serialize, Deserialize)]
1506pub struct BeforeAfterItem {
1507 pub label: String,
1508 pub detail: String,
1509}
1510
1511#[derive(Debug, Clone, Serialize, Deserialize)]
1513pub struct PipelineStep {
1514 pub label: String,
1515 pub description: Option<String>,
1516}
1517
1518#[derive(Debug, Clone, Serialize, Deserialize)]
1522pub struct EnvEntry {
1523 pub name: String,
1524 pub default_value: Option<String>,
1525}
1526
1527#[derive(Debug, Clone, Serialize, Deserialize)]
1529pub struct SmokeCheck {
1530 pub method: String,
1531 pub path: String,
1532 pub expected: u16,
1533}
1534
1535#[derive(Debug, Clone, Serialize, Deserialize)]
1537pub struct DomainEntry {
1538 pub domain: String,
1539 pub description: Option<String>,
1540}
1541
1542#[derive(Debug, Clone, Serialize, Deserialize)]
1544pub struct CrateEntry {
1545 pub name: String,
1546 pub source: Option<String>,
1547 pub features: Option<String>,
1548}
1549
1550#[derive(Debug, Clone, Serialize, Deserialize)]
1552pub struct VolumeEntry {
1553 pub name: String,
1554 pub mount: String,
1555}
1556
1557#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1561#[serde(rename_all = "lowercase")]
1562pub enum ListDisplay {
1563 Card,
1564 Table,
1565 Compact,
1566}
1567
1568#[derive(Debug, Clone, Serialize, Deserialize)]
1570pub struct ListFilter {
1571 pub field: String,
1572}
1573
1574#[derive(Debug, Clone, Serialize, Deserialize)]
1576pub struct SortSpec {
1577 pub field: String,
1578 pub descending: bool,
1579}
1580
1581#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1583#[serde(rename_all = "lowercase")]
1584pub enum HttpMethod {
1585 Get,
1586 Post,
1587 Put,
1588 Patch,
1589 Delete,
1590}
1591
1592#[derive(Debug, Clone, Serialize, Deserialize)]
1594pub struct FilterField {
1595 pub label: String,
1596 pub name: String,
1597 pub options: Vec<String>,
1598}
1599
1600#[derive(Debug, Clone, Serialize, Deserialize)]
1602pub struct StoreItem {
1603 pub name: String,
1604 pub price: String,
1606 pub blurb: Option<String>,
1608 pub badge: Option<String>,
1610 pub category: Option<String>,
1612}
1613
1614#[derive(Debug, Clone, Serialize, Deserialize)]
1616pub struct BookingService {
1617 pub name: String,
1618 pub duration: Option<String>,
1620 pub price: Option<String>,
1622}
1623
1624#[derive(Debug, Clone, Serialize, Deserialize)]
1627pub struct BookingDay {
1628 pub date: String,
1630 pub slots: Vec<String>,
1632}
1633
1634#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1636#[serde(rename_all = "lowercase")]
1637pub enum ChartType {
1638 Line,
1639 Bar,
1640 Pie,
1641 Area,
1642 Scatter,
1644 Donut,
1646 StackedBar,
1648 Radar,
1650}
1651
1652#[derive(Debug, Clone, Serialize, Deserialize)]
1655pub struct ChartSeries {
1656 pub name: String,
1658 pub values: Vec<f64>,
1660}
1661
1662#[derive(Debug, Clone, Serialize, Deserialize)]
1671pub struct ChartData {
1672 pub categories: Vec<String>,
1674 pub series: Vec<ChartSeries>,
1676}
1677
1678#[derive(Debug, Clone, Serialize, Deserialize)]
1682pub struct ModelField {
1683 pub name: String,
1684 pub field_type: ModelFieldType,
1685 pub constraints: Vec<FieldConstraint>,
1686}
1687
1688#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1690#[serde(rename_all = "lowercase")]
1691pub enum ModelFieldType {
1692 Uuid,
1693 String,
1694 Int,
1695 Float,
1696 Bool,
1697 Datetime,
1698 Text,
1699 Json,
1700 Money,
1702 Image,
1704 Email,
1706 Url,
1708 Enum(Vec<String>),
1710 Ref(String),
1712}
1713
1714#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1716#[serde(rename_all = "lowercase")]
1717pub enum FieldConstraint {
1718 Primary,
1719 Auto,
1720 Required,
1721 Optional,
1722 Unique,
1723 Max(u32),
1724 Min(u32),
1725 Default(String),
1726 Index,
1728}
1729
1730#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1732#[serde(rename_all = "lowercase")]
1733pub enum AuthProvider {
1734 Email,
1735 OAuth,
1736 ApiKey,
1737 Token,
1738}
1739
1740#[derive(Debug, Clone, Serialize, Deserialize)]
1742pub struct BindingEvent {
1743 pub event: String,
1744 pub action: String,
1745}
1746
1747#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1751pub struct SchemaField {
1752 pub name: String,
1753 pub field_type: ModelFieldType,
1754 pub constraints: Vec<FieldConstraint>,
1755}
1756
1757#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1759pub struct CrateDep {
1760 pub name: String,
1761 pub version: Option<String>,
1762 pub features: Vec<String>,
1763}
1764
1765#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1767pub struct EnvVar {
1768 pub name: String,
1769 pub description: Option<String>,
1770 pub required: bool,
1771}
1772
1773#[derive(Debug, Clone, Serialize, Deserialize)]
1775pub enum InlineExt {
1776 Evidence {
1777 tier: Option<u8>,
1778 source: Option<String>,
1779 text: String,
1780 },
1781 Status {
1782 value: String,
1783 },
1784}
1785
1786pub type Attrs = BTreeMap<String, AttrValue>;
1788
1789#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1791#[serde(untagged)]
1792pub enum AttrValue {
1793 String(String),
1794 Number(f64),
1795 Bool(bool),
1796 Null,
1797}
1798
1799#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1801pub struct Span {
1802 pub start_line: usize,
1804 pub end_line: usize,
1806 pub start_offset: usize,
1808 pub end_offset: usize,
1810}
1811
1812impl Span {
1813 pub const SYNTHETIC: Span = Span {
1816 start_line: 0,
1817 end_line: 0,
1818 start_offset: 0,
1819 end_offset: 0,
1820 };
1821}
1822
1823#[cfg(test)]
1824mod doc_type_format_tests {
1825 use super::*;
1826
1827 fn parse_fm(yaml: &str) -> FrontMatter {
1828 serde_yaml::from_str::<FrontMatter>(yaml).expect("front matter should parse")
1829 }
1830
1831 #[test]
1834 fn new_doc_types_deserialize() {
1835 assert_eq!(parse_fm("type: presentation").doc_type, Some(DocType::Presentation));
1836 assert_eq!(parse_fm("type: web").doc_type, Some(DocType::Web));
1837 assert_eq!(parse_fm("type: website").doc_type, Some(DocType::Website));
1838 assert_eq!(parse_fm("type: paper").doc_type, Some(DocType::Paper));
1839 assert_eq!(parse_fm("type: report").doc_type, Some(DocType::Report));
1840 }
1841
1842 #[test]
1843 fn existing_doc_types_unchanged() {
1844 assert_eq!(parse_fm("type: deck").doc_type, Some(DocType::Deck));
1845 assert_eq!(parse_fm("type: slides").doc_type, Some(DocType::Slides));
1846 assert_eq!(parse_fm("type: doc").doc_type, Some(DocType::Doc));
1847 }
1848
1849 #[test]
1852 fn format_values_deserialize() {
1853 for (s, want) in [
1854 ("ieee", Format::Ieee),
1855 ("acm", Format::Acm),
1856 ("article", Format::Article),
1857 ("mla", Format::Mla),
1858 ("apa", Format::Apa),
1859 ("chicago", Format::Chicago),
1860 ] {
1861 assert_eq!(parse_fm(&format!("format: {s}")).format, Some(want));
1862 }
1863 }
1864
1865 #[test]
1866 fn format_aliases_are_case_insensitive() {
1867 assert_eq!(parse_fm("format: IEEE").format, Some(Format::Ieee));
1868 assert_eq!(parse_fm("format: ACM").format, Some(Format::Acm));
1869 assert_eq!(parse_fm("format: MLA").format, Some(Format::Mla));
1870 assert_eq!(parse_fm("format: APA").format, Some(Format::Apa));
1871 assert_eq!(parse_fm("format: Chicago").format, Some(Format::Chicago));
1872 assert_eq!(parse_fm("format: Article").format, Some(Format::Article));
1873 }
1874
1875 #[test]
1876 fn format_missing_defaults_to_none() {
1877 assert_eq!(parse_fm("type: paper").format, None);
1878 assert_eq!(parse_fm("title: Hello").format, None);
1879 }
1880
1881 #[test]
1884 fn every_format_maps_to_a_render_profile() {
1885 for f in [
1886 Format::Ieee,
1887 Format::Acm,
1888 Format::Article,
1889 Format::Mla,
1890 Format::Apa,
1891 Format::Chicago,
1892 ] {
1893 assert_eq!(
1895 render_profile(Some(DocType::Paper), Some(f)),
1896 RenderProfile::Paper(f)
1897 );
1898 assert_eq!(
1900 render_profile(Some(DocType::Report), Some(f)),
1901 RenderProfile::Report(f)
1902 );
1903 }
1904 }
1905
1906 #[test]
1907 fn render_profile_mapping_is_total_and_stable() {
1908 assert_eq!(render_profile(None, None), RenderProfile::Document);
1910 assert_eq!(render_profile(Some(DocType::Web), None), RenderProfile::Web);
1912 assert_eq!(render_profile(Some(DocType::Website), None), RenderProfile::Web);
1913 assert_eq!(
1915 render_profile(Some(DocType::Deck), None),
1916 RenderProfile::Presentation
1917 );
1918 assert_eq!(
1919 render_profile(Some(DocType::Slides), None),
1920 RenderProfile::Presentation
1921 );
1922 assert_eq!(
1923 render_profile(Some(DocType::Presentation), None),
1924 RenderProfile::Presentation
1925 );
1926 assert_eq!(
1928 render_profile(Some(DocType::Paper), None),
1929 RenderProfile::Paper(Format::Article)
1930 );
1931 assert_eq!(
1932 render_profile(Some(DocType::Report), None),
1933 RenderProfile::Report(Format::Mla)
1934 );
1935 for dt in [DocType::Doc, DocType::Guide, DocType::Plan, DocType::App] {
1937 assert_eq!(render_profile(Some(dt), None), RenderProfile::Document);
1938 }
1939 }
1940}