1use std::fmt::Write;
2
3use sofie_engine::{Children, ComponentSpec, SPECS};
4
5#[must_use]
7pub fn component_list(enabled: Option<&[String]>) -> String {
8 let mut text = String::from(
9 "Screen components, written in templates as `<Name prop=\"…\" />`. Templates are \
10 HTML besides: plain elements, `{data.path}` text, `{#if …}`/`{:else}`/`{/if}`, \
11 `{#each list as item, index}`/`{/each}`; conditions may ask `can(\"…\")`, \
12 `enabled(\"…\")` and `entitled(\"…\")`, and text may be `t(\"key\", name = …)`, \
13 `number(…)` or `money(…)` (the `i18n` feature) or `date(…)`. `explain <Name>` gives \
14 one component's props.\n",
15 );
16 if let Some(enabled) = enabled {
17 let mut features: Vec<&str> = SPECS.iter().filter_map(|spec| spec.feature).collect();
18 features.dedup();
19 let states: Vec<String> = features
20 .iter()
21 .map(|feature| format!("`{feature}` {}", state(feature, enabled)))
22 .collect();
23 let _ = writeln!(text, "In this app: {}.", states.join(", "));
24 }
25 for spec in SPECS {
26 let _ = write!(text, "\n{}", component_section(spec, enabled));
27 }
28 text
29}
30
31fn state(feature: &str, enabled: &[String]) -> &'static str {
32 if enabled.iter().any(|on| on == feature) {
33 "on"
34 } else {
35 "off"
36 }
37}
38
39pub fn component(name: &str, enabled: impl FnOnce() -> Option<Vec<String>>) -> Option<String> {
41 let spec = SPECS.iter().find(|spec| spec.name == name)?;
42 let enabled = spec.feature.and_then(|_| enabled());
43 Some(component_section(spec, enabled.as_deref()))
44}
45
46fn component_section(spec: &ComponentSpec, enabled: Option<&[String]>) -> String {
47 let mut text = format!("component {}: {}\n", spec.name, spec.summary);
48 if let Some(feature) = spec.feature {
49 let in_app = match enabled.map(|enabled| state(feature, enabled)) {
50 Some("on") => ", on in this app".to_owned(),
51 Some(_) => format!(
52 ", off in this app: `sofie feature add {feature}`, or the MCP tool `feature`, \
53 turns it on"
54 ),
55 None => String::new(),
56 };
57 let _ = writeln!(
58 text,
59 " feature: `{feature}` (sofie's feature of the same name{in_app})"
60 );
61 }
62 let _ = writeln!(text, " children: {}", children(spec.children));
63 for prop in spec.props {
64 let need = if prop.required {
65 "required"
66 } else {
67 "optional"
68 };
69 let _ = writeln!(
70 text,
71 " {} ({need}, {}): {}",
72 prop.name,
73 prop.kind.describe(),
74 prop.summary
75 );
76 }
77 text
78}
79
80fn children(children: Children) -> &'static str {
81 match children {
82 Children::None => "none; write it as `<Name … />`",
83 Children::Any => "any markup and components",
84 Children::FormParts => "`Field` and `Submit`, in the order shown",
85 Children::RowContent => "rendered in a last cell of every row, the row named by `as`",
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn a_component_names_its_props_and_what_each_takes() {
95 let table = component("Table", || unreachable!("`Table` needs no feature")).unwrap();
96
97 assert!(table.starts_with("component Table: "), "{table}");
98 assert!(
99 table.contains(" of (required, a data path such as `{data.items}`): "),
100 "{table}"
101 );
102 assert!(
103 table.contains(" children: rendered in a last cell"),
104 "{table}"
105 );
106 assert_eq!(component("Nothing", || None), None);
107 }
108
109 #[test]
110 fn the_list_holds_every_component() {
111 let list = component_list(None);
112
113 for spec in SPECS {
114 assert!(
115 list.contains(&format!("component {}: ", spec.name)),
116 "{}",
117 spec.name
118 );
119 }
120 }
121
122 #[test]
123 fn a_feature_reads_as_on_or_off_when_the_app_is_known() {
124 let enabled = || Some(vec!["engine".to_owned(), "i18n".to_owned()]);
125
126 let badge = component("Badge", enabled).unwrap();
127 let card = component("Card", enabled).unwrap();
128 let unknown = component("Card", || None).unwrap();
129
130 assert!(badge.contains("same name, on in this app)"), "{badge}");
131 assert!(
132 card.contains("off in this app: `sofie feature add layout`, or the MCP tool `feature`"),
133 "{card}"
134 );
135 assert!(
136 unknown.contains("(sofie's feature of the same name)\n"),
137 "{unknown}"
138 );
139 }
140
141 #[test]
142 fn table_documents_its_row_link() {
143 let table = component("Table", || None).unwrap();
144
145 assert!(table.contains(" link (optional, "), "{table}");
146 }
147}