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