1use crate::prelude::*;
7use alloc::collections::BTreeMap;
8use shuyi_assets::ContentError;
9use shuyi_assets::bundle::Bundle;
10use shuyi_assets::error::codes;
11use shuyi_assets::keymap::ActionSet;
12use shuyi_assets::palette::line_of_key;
13use shuyi_assets::text::NO_BREAK_SPACE;
14use shuyi_assets::tip::placeholders;
15
16use crate::input::help_keys::{HelpKeys, cursor_keys_name, key_name};
17
18pub const CURSOR: &str = "Cursor";
20
21pub fn key_text<A: ActionSet>(name: &str, keys: HelpKeys<'_, A>) -> Option<String> {
26 if name == CURSOR {
27 Some(cursor_keys_name(keys))
28 } else {
29 A::from_name(name).map(|action| key_name(keys, action))
30 }
31}
32use core::fmt::Display;
33
34pub fn check_key_placeholders<A: ActionSet>(
46 bundle: &Bundle,
47 path: &str,
48 text: &BTreeMap<String, String>,
49) -> Result<(), Vec<ContentError>> {
50 let source = bundle.file(path).unwrap_or_default();
51 let file = bundle.display_path(path);
52 let mut errors = Vec::new();
53 for (key, value) in text {
54 for name in placeholders(value) {
55 let names_a_key = name.chars().next().is_some_and(char::is_uppercase);
56 if !names_a_key || name == CURSOR || A::from_name(name).is_some() {
57 continue;
58 }
59 let known: Vec<&str> = A::ALL.iter().map(|a| a.name()).collect();
60 let message = format!(
61 "\"{key}\": unknown action \"{{{name}}}\"; known actions: {}, and {{{CURSOR}}} \
62 for the cursor keys",
63 known.join(", ")
64 );
65 let mut error = ContentError::new(&file, message).with_code(codes::UNKNOWN_NAME);
66 if let Some(line) = line_of_key(source, key) {
67 error = error.at(line, None);
68 }
69 errors.push(error);
70 }
71 }
72 if errors.is_empty() {
73 Ok(())
74 } else {
75 Err(errors)
76 }
77}
78
79pub fn fill_values(text: &str, args: &[(&str, &dyn Display)]) -> String {
83 fill(text, args, |_| None)
84}
85
86pub fn fill(
96 text: &str,
97 args: &[(&str, &dyn Display)],
98 lookup: impl Fn(&str) -> Option<String>,
99) -> String {
100 let mut out = String::with_capacity(text.len());
101 let mut rest = text;
102 while let Some((before, after)) = rest.split_once('{') {
103 out.push_str(before);
104 let Some((name, tail)) = after.split_once('}') else {
105 out.push('{');
107 rest = after;
108 break;
109 };
110 if let Some((_, value)) = args.iter().find(|(arg, _)| *arg == name) {
111 out.push_str(&value.to_string());
112 } else if let Some(found) = lookup(name) {
113 out.extend(
114 found
115 .chars()
116 .map(|c| if c == ' ' { NO_BREAK_SPACE } else { c }),
117 );
118 } else {
119 out.push('{');
120 out.push_str(name);
121 out.push('}');
122 }
123 rest = tail;
124 }
125 out.push_str(rest);
126 out
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
135pub enum Label {
136 Key {
140 key: String,
142 args: Vec<(String, String)>,
144 },
145 Text(String),
148}
149
150impl Label {
151 pub fn key(key: impl Into<String>) -> Self {
153 Self::Key {
154 key: key.into(),
155 args: Vec::new(),
156 }
157 }
158
159 #[must_use]
162 pub fn with_arg(mut self, name: impl Into<String>, value: impl Display) -> Self {
163 if let Self::Key { args, .. } = &mut self {
164 args.push((name.into(), value.to_string()));
165 }
166 self
167 }
168}
169
170impl From<String> for Label {
171 fn from(text: String) -> Self {
172 Self::Text(text)
173 }
174}
175
176impl From<&str> for Label {
177 fn from(text: &str) -> Self {
178 Self::Text(text.to_owned())
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 fn upper(name: &str) -> Option<String> {
187 (name == "key").then(|| "K".to_owned())
188 }
189
190 #[test]
191 fn values_come_first_then_the_lookup_and_the_rest_stays() {
192 let text = "press {key} for {count} {things} {}";
193 assert_eq!(
194 fill(text, &[("count", &3)], upper),
195 "press K for 3 {things} {}"
196 );
197 assert_eq!(
199 fill(text, &[("key", &"Z"), ("things", &"apples")], upper),
200 "press Z for {count} apples {}"
201 );
202 assert_eq!(
203 fill_values(text, &[("key", &1)]),
204 "press 1 for {count} {things} {}"
205 );
206 }
207
208 #[test]
209 fn a_filled_value_is_not_filled_again() {
210 assert_eq!(fill("{a}{b}", &[("a", &"{b}")], upper), "{b}{b}");
211 assert_eq!(fill("{a}", &[("a", &"{key}")], upper), "{key}");
212 }
213
214 #[test]
215 fn a_key_name_of_two_words_is_never_split_across_lines() {
216 use shuyi_assets::font::CellWidths;
217 let key = |name: &str| (name == "Run").then(|| "Left Shift".to_owned());
218 let filled = fill("hold {Run} to run, {who}", &[("who", &"Ada Brook")], key);
219 assert_eq!(filled, "hold Left\u{a0}Shift to run, Ada Brook");
221 for width in 10..20 {
222 let lines = crate::widgets::word_wrap(&filled, width, &CellWidths::NARROW);
223 assert!(
224 lines.iter().any(|l| l.contains("Left\u{a0}Shift")),
225 "{width}: {lines:?}"
226 );
227 }
228 }
229
230 fn checked(source: &'static str) -> Result<(), Vec<ContentError>> {
232 use shuyi_assets::bundle::Layer;
233 use shuyi_assets::keymap::UiAction;
234 let files: &'static [(&str, &[u8])] = Box::leak(Box::new([("en.ron", source.as_bytes())]));
235 let bundle = Bundle::new(vec![Layer::table("files", files)]);
236 let text: shuyi_assets::lang::English = ron::from_str(source).unwrap();
237 check_key_placeholders::<UiAction>(&bundle, "en.ron", &text.0)
238 }
239
240 #[test]
241 fn actions_the_cursor_and_values_a_screen_passes_are_placeholders() {
242 let good = "{\n \"a\": \"{Confirm} plant {seeds} {Cursor} move\",\n \"b\": \"{} {\",\n}";
243 assert_eq!(checked(good), Ok(()));
244 }
245
246 #[test]
247 fn a_misspelled_action_fails_with_its_file_line_and_the_known_actions() {
248 let bad = "{\n \"a\": \"{Confirm} ok\",\n \"title.help\": \"{Confirn} plant\",\n}";
249 let errors = checked(bad).unwrap_err();
250 assert_eq!(errors.len(), 1, "{errors:?}");
251 assert_eq!(errors[0].code, Some(codes::UNKNOWN_NAME));
252 assert_eq!(
253 errors[0].to_string(),
254 "files/en.ron:3: \"title.help\": unknown action \"{Confirn}\"; known actions: \
255 Up, Down, Left, Right, Confirm, Cancel, and {Cursor} for the cursor keys"
256 );
257 }
258
259 #[test]
260 fn an_unclosed_brace_is_plain_text() {
261 assert_eq!(fill("a {key", &[], upper), "a {key");
262 assert_eq!(fill("{key} {", &[], upper), "K {");
263 assert_eq!(fill("} {key}", &[], upper), "} K");
264 assert_eq!(fill("", &[], upper), "");
265 assert_eq!(fill("no braces", &[], upper), "no braces");
266 }
267}