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