use crate::ast::shortcode::ShortcodeKind;
use crate::resolve::ext_kind::ExtKind;
pub struct ShortcodeAttrSpec {
pub name: &'static str,
pub asset_kinds: &'static [ExtKind],
}
pub struct ShortcodeCatalogEntry {
pub kind: ShortcodeKind,
pub name: &'static str,
pub attrs: &'static [ShortcodeAttrSpec],
pub asset_attr: Option<&'static str>,
pub canonical_template: &'static str,
pub authorable: bool,
}
pub fn catalog() -> Vec<ShortcodeCatalogEntry> {
ShortcodeKind::all().map(entry).collect()
}
fn entry(kind: ShortcodeKind) -> ShortcodeCatalogEntry {
let (attrs, asset_attr, canonical_template): (
&'static [ShortcodeAttrSpec],
Option<&'static str>,
&'static str,
) = match kind {
ShortcodeKind::Subscribe => (
&[
ShortcodeAttrSpec { name: "button", asset_kinds: &[] },
ShortcodeAttrSpec { name: "placeholder", asset_kinds: &[] },
],
None,
"subscribe {button=\"${1:Subscribe}\"}\n:::",
),
ShortcodeKind::Buttons => (
&[],
None,
"buttons\n[${1:Get started}](${2:/})\n:::",
),
ShortcodeKind::Gallery => (
&[ShortcodeAttrSpec { name: "cols", asset_kinds: &[] }],
None,
"gallery {cols=${1:3}}\n\n:::",
),
ShortcodeKind::Hero => (
&[
ShortcodeAttrSpec {
name: "image",
asset_kinds: &[ExtKind::Image, ExtKind::Video],
},
ShortcodeAttrSpec { name: "wide", asset_kinds: &[] },
],
Some("image"),
"hero {image=${1:photo.jpg}}\n# ${2:Title}\n${3:Subtitle}\n:::",
),
ShortcodeKind::Grid => (
&[
ShortcodeAttrSpec { name: "cols", asset_kinds: &[] },
ShortcodeAttrSpec { name: "wide", asset_kinds: &[] },
],
None,
"grid {cols=${1:2}}\n${2:cell one}\n+++\n${3:cell two}\n:::",
),
ShortcodeKind::Recent => (
&[
ShortcodeAttrSpec { name: "count", asset_kinds: &[] },
ShortcodeAttrSpec { name: "since", asset_kinds: &[] },
ShortcodeAttrSpec { name: "last", asset_kinds: &[] },
],
None,
"recent {count=${1:5}}\n${2:No posts yet.}\n:::",
),
ShortcodeKind::Apply => (
&[
ShortcodeAttrSpec { name: "placeholder", asset_kinds: &[] },
ShortcodeAttrSpec { name: "button", asset_kinds: &[] },
],
None,
"apply {button=\"${1:Apply}\"}\n:::",
),
};
ShortcodeCatalogEntry {
kind,
name: kind.name(),
attrs,
asset_attr,
canonical_template,
authorable: kind.authorable(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn covers_every_kind_exactly_once() {
let cat = catalog();
assert_eq!(cat.len(), ShortcodeKind::all().count());
let names: std::collections::HashSet<_> = cat.iter().map(|e| e.name).collect();
assert_eq!(names.len(), cat.len(), "duplicate fence names");
}
#[test]
fn name_matches_serde_snake_case() {
for e in catalog() {
let serde_name = serde_json::to_string(&e.kind).expect("serialize");
assert_eq!(serde_name, format!("\"{}\"", e.name));
}
}
#[test]
fn asset_attr_names_a_declared_asset_attr() {
for e in catalog() {
if let Some(attr) = e.asset_attr {
let hit = e.attrs.iter().find(|a| a.name == attr);
let hit = hit.unwrap_or_else(|| {
panic!("{}: asset_attr `{attr}` is not in attrs", e.name)
});
assert!(
!hit.asset_kinds.is_empty(),
"{}: asset_attr `{attr}` declares no asset kinds",
e.name
);
}
for a in e.attrs.iter().filter(|a| !a.asset_kinds.is_empty()) {
assert_eq!(
e.asset_attr,
Some(a.name),
"{}: attr `{}` carries asset kinds but is not the asset_attr",
e.name,
a.name
);
}
}
}
#[test]
fn template_opens_with_the_fence_name_and_closes_the_fence() {
for e in catalog() {
assert!(
e.canonical_template.starts_with(e.name),
"{}: template must start with the fence name (inserted after `:::`)",
e.name
);
assert!(
e.canonical_template.ends_with(":::"),
"{}: template must close its fence",
e.name
);
}
}
#[test]
fn only_apply_is_hidden() {
for e in catalog() {
assert_eq!(
e.authorable,
e.kind != ShortcodeKind::Apply,
"{}: authorable flag drifted from the §7.1 decision",
e.name
);
}
}
}