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 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 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}