Skip to main content

brief/
shortcode.rs

1use serde::Deserialize;
2use std::collections::BTreeMap;
3
4#[derive(Clone, Debug, PartialEq)]
5pub enum ArgValue {
6    Ident(String),
7    Int(i64),
8    Str(String),
9    Array(Vec<ArgValue>),
10}
11
12impl ArgValue {
13    pub fn type_name(&self) -> &'static str {
14        match self {
15            ArgValue::Ident(_) => "ident",
16            ArgValue::Int(_) => "int",
17            ArgValue::Str(_) => "string",
18            ArgValue::Array(_) => "array",
19        }
20    }
21
22    pub fn as_str(&self) -> Option<&str> {
23        match self {
24            ArgValue::Str(s) | ArgValue::Ident(s) => Some(s.as_str()),
25            _ => None,
26        }
27    }
28}
29
30#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
31#[serde(rename_all = "snake_case")]
32pub enum ArgType {
33    String,
34    Int,
35    Ident,
36    Array,
37}
38
39#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
40pub struct ArgSpec {
41    #[serde(rename = "type")]
42    pub ty: ArgType,
43    #[serde(default)]
44    pub required: bool,
45    #[serde(default)]
46    pub position: Option<usize>,
47    #[serde(default)]
48    pub oneof: Option<Vec<String>>,
49}
50
51#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
52#[serde(rename_all = "lowercase")]
53pub enum ShortKindOpt {
54    Inline,
55    Block,
56    Both,
57}
58
59impl Default for ShortKindOpt {
60    fn default() -> Self {
61        ShortKindOpt::Inline
62    }
63}
64
65#[derive(Clone, Debug, Deserialize, Default, PartialEq, Eq)]
66pub struct Shortcode {
67    #[serde(default)]
68    pub kind: ShortKindOpt,
69    #[serde(default)]
70    pub arguments: BTreeMap<String, ArgSpec>,
71    #[serde(default)]
72    pub template_html: Option<String>,
73    #[serde(default)]
74    pub template_llm: Option<String>,
75}
76
77#[derive(Clone, Debug, Default)]
78pub struct Registry {
79    pub map: BTreeMap<String, Shortcode>,
80}
81
82impl Registry {
83    pub fn with_builtins() -> Self {
84        let mut m = BTreeMap::new();
85
86        m.insert(
87            "link".into(),
88            Shortcode {
89                kind: ShortKindOpt::Inline,
90                arguments: {
91                    let mut a = BTreeMap::new();
92                    a.insert(
93                        "url".into(),
94                        ArgSpec {
95                            ty: ArgType::String,
96                            required: true,
97                            position: Some(1),
98                            oneof: None,
99                        },
100                    );
101                    a.insert(
102                        "title".into(),
103                        ArgSpec {
104                            ty: ArgType::String,
105                            required: false,
106                            position: None,
107                            oneof: None,
108                        },
109                    );
110                    a
111                },
112                template_html: None,
113                template_llm: None,
114            },
115        );
116
117        m.insert(
118            "image".into(),
119            Shortcode {
120                kind: ShortKindOpt::Inline,
121                arguments: {
122                    let mut a = BTreeMap::new();
123                    a.insert(
124                        "src".into(),
125                        ArgSpec {
126                            ty: ArgType::String,
127                            required: true,
128                            position: None,
129                            oneof: None,
130                        },
131                    );
132                    a.insert(
133                        "alt".into(),
134                        ArgSpec {
135                            ty: ArgType::String,
136                            required: false,
137                            position: None,
138                            oneof: None,
139                        },
140                    );
141                    a
142                },
143                ..Default::default()
144            },
145        );
146
147        m.insert(
148            "kbd".into(),
149            Shortcode {
150                kind: ShortKindOpt::Inline,
151                ..Default::default()
152            },
153        );
154
155        // Inline subscript and superscript: replacements for the only inline
156        // HTML constructs Brief still wants to express. Both take their text
157        // via the `[content]` body — no arguments.
158        m.insert(
159            "sub".into(),
160            Shortcode {
161                kind: ShortKindOpt::Inline,
162                ..Default::default()
163            },
164        );
165
166        m.insert(
167            "sup".into(),
168            Shortcode {
169                kind: ShortKindOpt::Inline,
170                ..Default::default()
171            },
172        );
173
174        // Collapsible block: `@details(summary: "...") ... @end` ↔
175        // `<details><summary>...</summary>...</details>`.
176        m.insert(
177            "details".into(),
178            Shortcode {
179                kind: ShortKindOpt::Block,
180                arguments: {
181                    let mut a = BTreeMap::new();
182                    a.insert(
183                        "summary".into(),
184                        ArgSpec {
185                            ty: ArgType::String,
186                            required: true,
187                            position: None,
188                            oneof: None,
189                        },
190                    );
191                    a
192                },
193                ..Default::default()
194            },
195        );
196
197        m.insert(
198            "dl".into(),
199            Shortcode {
200                kind: ShortKindOpt::Block,
201                ..Default::default()
202            },
203        );
204
205        m.insert(
206            "t".into(),
207            Shortcode {
208                kind: ShortKindOpt::Block,
209                arguments: {
210                    let mut a = BTreeMap::new();
211                    a.insert(
212                        "align".into(),
213                        ArgSpec {
214                            ty: ArgType::Array,
215                            required: false,
216                            position: None,
217                            oneof: None,
218                        },
219                    );
220                    a
221                },
222                ..Default::default()
223            },
224        );
225
226        m.insert(
227            "code".into(),
228            Shortcode {
229                kind: ShortKindOpt::Block,
230                arguments: {
231                    let mut a = BTreeMap::new();
232                    a.insert(
233                        "lang".into(),
234                        ArgSpec {
235                            ty: ArgType::String,
236                            required: false,
237                            position: Some(1),
238                            oneof: None,
239                        },
240                    );
241                    a
242                },
243                ..Default::default()
244            },
245        );
246
247        m.insert(
248            "callout".into(),
249            Shortcode {
250                kind: ShortKindOpt::Block,
251                arguments: {
252                    let mut a = BTreeMap::new();
253                    a.insert(
254                        "kind".into(),
255                        ArgSpec {
256                            ty: ArgType::String,
257                            required: true,
258                            position: None,
259                            oneof: Some(vec![
260                                "note".into(),
261                                "tip".into(),
262                                "important".into(),
263                                "warning".into(),
264                                "caution".into(),
265                            ]),
266                        },
267                    );
268                    a
269                },
270                ..Default::default()
271            },
272        );
273
274        m.insert(
275            "math".into(),
276            Shortcode {
277                kind: ShortKindOpt::Both,
278                ..Default::default()
279            },
280        );
281
282        m.insert(
283            "footnote".into(),
284            Shortcode {
285                kind: ShortKindOpt::Inline,
286                ..Default::default()
287            },
288        );
289
290        m.insert(
291            "ref".into(),
292            Shortcode {
293                kind: ShortKindOpt::Inline,
294                arguments: {
295                    let mut a = BTreeMap::new();
296                    a.insert(
297                        "title".into(),
298                        ArgSpec {
299                            ty: ArgType::String,
300                            required: true,
301                            position: Some(1),
302                            oneof: None,
303                        },
304                    );
305                    a
306                },
307                template_html: None,
308                template_llm: None,
309            },
310        );
311
312        Registry { map: m }
313    }
314
315    pub fn get(&self, name: &str) -> Option<&Shortcode> {
316        self.map.get(name)
317    }
318
319    pub fn extend(&mut self, other: BTreeMap<String, Shortcode>) {
320        for (k, v) in other {
321            self.map.insert(k, v);
322        }
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    #[test]
331    fn ref_is_a_builtin_inline_shortcode() {
332        let reg = Registry::with_builtins();
333        let sc = reg.get("ref").expect("@ref must be a built-in");
334        assert!(matches!(sc.kind, ShortKindOpt::Inline));
335        let title = sc.arguments.get("title").expect("title arg");
336        assert!(title.required);
337        assert_eq!(title.position, Some(1));
338        assert!(matches!(title.ty, ArgType::String));
339    }
340
341    #[test]
342    fn dl_is_a_builtin_block_shortcode() {
343        let reg = Registry::with_builtins();
344        let sc = reg.get("dl").expect("@dl must be a built-in");
345        assert!(matches!(sc.kind, ShortKindOpt::Block));
346        assert!(sc.arguments.is_empty(), "@dl takes no arguments in v0.3");
347    }
348}