1use crate::ast::shortcode::ShortcodeKind;
19use crate::resolve::ext_kind::ExtKind;
20
21pub struct ShortcodeAttrSpec {
23 pub name: &'static str,
25 pub asset_kinds: &'static [ExtKind],
29}
30
31pub struct ShortcodeCatalogEntry {
33 pub kind: ShortcodeKind,
34 pub name: &'static str,
36 pub attrs: &'static [ShortcodeAttrSpec],
37 pub asset_attr: Option<&'static str>,
41 pub canonical_template: &'static str,
45 pub authorable: bool,
47}
48
49pub fn catalog() -> Vec<ShortcodeCatalogEntry> {
51 ShortcodeKind::all().map(entry).collect()
52}
53
54fn entry(kind: ShortcodeKind) -> ShortcodeCatalogEntry {
55 let (attrs, asset_attr, canonical_template): (
56 &'static [ShortcodeAttrSpec],
57 Option<&'static str>,
58 &'static str,
59 ) = match kind {
60 ShortcodeKind::Subscribe => (
61 &[
62 ShortcodeAttrSpec { name: "button", asset_kinds: &[] },
63 ShortcodeAttrSpec { name: "placeholder", asset_kinds: &[] },
64 ],
65 None,
66 "subscribe {button=\"${1:Subscribe}\"}\n:::",
67 ),
68 ShortcodeKind::Buttons => (
69 &[],
70 None,
71 "buttons\n[${1:Get started}](${2:/})\n:::",
72 ),
73 ShortcodeKind::Gallery => (
74 &[ShortcodeAttrSpec { name: "cols", asset_kinds: &[] }],
75 None,
76 "gallery {cols=${1:3}}\n\n:::",
77 ),
78 ShortcodeKind::Hero => (
79 &[
80 ShortcodeAttrSpec {
81 name: "image",
82 asset_kinds: &[ExtKind::Image, ExtKind::Video],
83 },
84 ShortcodeAttrSpec { name: "wide", asset_kinds: &[] },
85 ],
86 Some("image"),
87 "hero {image=${1:photo.jpg}}\n# ${2:Title}\n${3:Subtitle}\n:::",
88 ),
89 ShortcodeKind::Grid => (
90 &[
91 ShortcodeAttrSpec { name: "cols", asset_kinds: &[] },
92 ShortcodeAttrSpec { name: "wide", asset_kinds: &[] },
93 ],
94 None,
95 "grid {cols=${1:2}}\n${2:cell one}\n+++\n${3:cell two}\n:::",
96 ),
97 ShortcodeKind::Recent => (
98 &[
99 ShortcodeAttrSpec { name: "count", asset_kinds: &[] },
100 ShortcodeAttrSpec { name: "since", asset_kinds: &[] },
101 ShortcodeAttrSpec { name: "last", asset_kinds: &[] },
102 ],
103 None,
104 "recent {count=${1:5}}\n${2:No posts yet.}\n:::",
105 ),
106 ShortcodeKind::Apply => (
107 &[
108 ShortcodeAttrSpec { name: "placeholder", asset_kinds: &[] },
109 ShortcodeAttrSpec { name: "button", asset_kinds: &[] },
110 ],
111 None,
112 "apply {button=\"${1:Apply}\"}\n:::",
113 ),
114 };
115 ShortcodeCatalogEntry {
116 kind,
117 name: kind.name(),
118 attrs,
119 asset_attr,
120 canonical_template,
121 authorable: kind.authorable(),
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn covers_every_kind_exactly_once() {
131 let cat = catalog();
132 assert_eq!(cat.len(), ShortcodeKind::all().count());
133 let names: std::collections::HashSet<_> = cat.iter().map(|e| e.name).collect();
134 assert_eq!(names.len(), cat.len(), "duplicate fence names");
135 }
136
137 #[test]
138 fn name_matches_serde_snake_case() {
139 for e in catalog() {
140 let serde_name = serde_json::to_string(&e.kind).expect("serialize");
141 assert_eq!(serde_name, format!("\"{}\"", e.name));
142 }
143 }
144
145 #[test]
146 fn asset_attr_names_a_declared_asset_attr() {
147 for e in catalog() {
148 if let Some(attr) = e.asset_attr {
149 let hit = e.attrs.iter().find(|a| a.name == attr);
150 let hit = hit.unwrap_or_else(|| {
151 panic!("{}: asset_attr `{attr}` is not in attrs", e.name)
152 });
153 assert!(
154 !hit.asset_kinds.is_empty(),
155 "{}: asset_attr `{attr}` declares no asset kinds",
156 e.name
157 );
158 }
159 for a in e.attrs.iter().filter(|a| !a.asset_kinds.is_empty()) {
163 assert_eq!(
164 e.asset_attr,
165 Some(a.name),
166 "{}: attr `{}` carries asset kinds but is not the asset_attr",
167 e.name,
168 a.name
169 );
170 }
171 }
172 }
173
174 #[test]
175 fn template_opens_with_the_fence_name_and_closes_the_fence() {
176 for e in catalog() {
177 assert!(
178 e.canonical_template.starts_with(e.name),
179 "{}: template must start with the fence name (inserted after `:::`)",
180 e.name
181 );
182 assert!(
183 e.canonical_template.ends_with(":::"),
184 "{}: template must close its fence",
185 e.name
186 );
187 }
188 }
189
190 #[test]
191 fn only_apply_is_hidden() {
192 for e in catalog() {
193 assert_eq!(
194 e.authorable,
195 e.kind != ShortcodeKind::Apply,
196 "{}: authorable flag drifted from the §7.1 decision",
197 e.name
198 );
199 }
200 }
201}