1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2pub enum CalloutTone {
3 Note,
4 Success,
5 Warning,
6}
7
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub enum DocBlock {
10 Heading {
11 level: u8,
12 text: String,
13 },
14 Prose(String),
15 Fixture {
16 id: String,
17 },
18 Props,
19 Controls,
20 Source,
21 Callout {
22 tone: CalloutTone,
23 title: String,
24 body: String,
25 },
26 Custom {
27 id: String,
28 payload: String,
29 },
30}
31
32impl DocBlock {
33 #[must_use]
34 pub fn heading(level: u8, text: impl Into<String>) -> Self {
35 Self::Heading {
36 level: level.clamp(1, 3),
37 text: text.into(),
38 }
39 }
40
41 #[must_use]
42 pub fn prose(text: impl Into<String>) -> Self {
43 Self::Prose(text.into())
44 }
45
46 #[must_use]
47 pub fn fixture(id: impl Into<String>) -> Self {
48 Self::Fixture { id: id.into() }
49 }
50
51 #[must_use]
52 pub const fn props() -> Self {
53 Self::Props
54 }
55
56 #[must_use]
57 pub const fn controls() -> Self {
58 Self::Controls
59 }
60
61 #[must_use]
62 pub const fn source() -> Self {
63 Self::Source
64 }
65
66 #[must_use]
67 pub fn custom(id: impl Into<String>, payload: impl Into<String>) -> Self {
68 Self::Custom {
69 id: id.into(),
70 payload: payload.into(),
71 }
72 }
73
74 #[must_use]
75 pub fn callout(tone: CalloutTone, title: impl Into<String>, body: impl Into<String>) -> Self {
76 Self::Callout {
77 tone,
78 title: title.into(),
79 body: body.into(),
80 }
81 }
82}
83
84#[derive(Clone, Debug, Default, PartialEq, Eq)]
85pub struct DocPage {
86 blocks: Vec<DocBlock>,
87}
88
89impl DocPage {
90 #[must_use]
91 pub fn new(blocks: impl IntoIterator<Item = DocBlock>) -> Self {
92 Self {
93 blocks: blocks.into_iter().collect(),
94 }
95 }
96
97 #[must_use]
98 pub fn blocks(&self) -> &[DocBlock] {
99 &self.blocks
100 }
101
102 #[must_use]
103 pub fn is_empty(&self) -> bool {
104 self.blocks.is_empty()
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn composes_typed_document_blocks_in_order() {
114 let page = DocPage::new([
115 DocBlock::heading(1, "Button"),
116 DocBlock::prose("Use for primary actions."),
117 DocBlock::props(),
118 DocBlock::controls(),
119 DocBlock::source(),
120 DocBlock::custom("project::tokens", "spacing"),
121 ]);
122
123 assert_eq!(page.blocks().len(), 6);
124 assert_eq!(
125 page.blocks()[0],
126 DocBlock::Heading {
127 level: 1,
128 text: "Button".to_owned(),
129 }
130 );
131 }
132}