1use crate::prelude::*;
9
10use alloc::collections::BTreeSet;
11use core::fmt::Debug;
12
13use serde::Deserialize;
14use serde::de::DeserializeOwned;
15
16use crate::error::ContentError;
17use crate::font::CellWidths;
18use crate::lang::text_width;
19use crate::ron::parse_ron;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct TipLimits {
24 pub text_lines: usize,
26 pub line_chars: usize,
28 pub title_chars: usize,
30 pub how_lines: usize,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
36#[serde(deny_unknown_fields)]
37#[serde(bound = "T: DeserializeOwned")]
38pub struct Tip<T> {
39 pub id: String,
41 pub trigger: T,
43 pub title: String,
45 pub text: String,
48 #[serde(default, deserialize_with = "some_name")]
52 pub how: Option<String>,
53 #[serde(default, deserialize_with = "some_name")]
56 pub face: Option<String>,
57}
58
59fn some_name<'de, D: serde::Deserializer<'de>>(d: D) -> Result<Option<String>, D::Error> {
61 String::deserialize(d).map(Some)
62}
63
64impl<T> Tip<T> {
65 pub fn face_or<'a>(&'a self, default: &'a str) -> &'a str {
67 self.face.as_deref().unwrap_or(default)
68 }
69}
70
71#[derive(Deserialize)]
72#[serde(deny_unknown_fields)]
73#[serde(bound = "T: DeserializeOwned")]
74struct RawFile<T> {
75 tips: Vec<Tip<T>>,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct TipTable<T> {
81 pub tips: Vec<Tip<T>>,
83}
84
85impl<T> Default for TipTable<T> {
86 fn default() -> Self {
87 Self { tips: Vec::new() }
88 }
89}
90
91impl<T: PartialEq + Copy> TipTable<T> {
92 pub fn for_trigger(&self, trigger: T) -> Option<&Tip<T>> {
94 self.tips.iter().find(|t| t.trigger == trigger)
95 }
96}
97
98pub struct TipRules<'a> {
101 pub limits: TipLimits,
103 pub widths: &'a CellWidths,
106 pub placeholder: &'a dyn Fn(&str) -> bool,
108 pub face_problem: &'a dyn Fn(&str) -> Option<String>,
111 pub default_face: &'a str,
113}
114
115pub fn from_source<T: DeserializeOwned + Debug + Ord>(
120 file: &str,
121 source: &str,
122 rules: &TipRules<'_>,
123) -> Result<TipTable<T>, Vec<ContentError>> {
124 let TipLimits {
125 text_lines,
126 line_chars,
127 title_chars,
128 how_lines,
129 } = rules.limits;
130 let raw: RawFile<T> = parse_ron(file, source).map_err(|e| vec![e])?;
131 let mut errors = Vec::new();
132 let mut ids = BTreeSet::new();
133 let mut triggers = BTreeSet::new();
134 for tip in &raw.tips {
135 let mut err = |message: String| {
136 errors.push(ContentError::new(
137 file,
138 format!("tip \"{}\": {message}", tip.id),
139 ));
140 };
141 let id_ok = !tip.id.is_empty()
142 && tip
143 .id
144 .bytes()
145 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_');
146 if !id_ok {
147 err("the id must be lowercase letters, digits and `_`".into());
148 }
149 if !ids.insert(tip.id.as_str()) {
150 err("the id is used twice".into());
151 }
152 if !triggers.insert(&tip.trigger) {
153 err(format!("{:?} already has a tip", tip.trigger));
154 }
155 if tip.title.is_empty() || text_width(&tip.title, rules.widths) > title_chars {
156 err(format!("the title must be 1 to {title_chars} characters"));
157 }
158 let lines: Vec<&str> = tip.text.lines().collect();
159 if lines.is_empty() || lines.len() > text_lines {
160 err(format!("the text must have 1 to {text_lines} lines"));
161 }
162 let how: Vec<&str> = tip
163 .how
164 .as_deref()
165 .map_or(Vec::new(), |h| h.lines().collect());
166 if tip.how.is_some() && (how.is_empty() || how.len() > how_lines) {
167 err(format!("the how must have 1 to {how_lines} lines"));
168 }
169 for line in lines.into_iter().chain(how) {
170 if text_width(line, rules.widths) > line_chars {
171 err(format!(
172 "a text line is over {line_chars} characters: {line:?}"
173 ));
174 }
175 }
176 for text in core::iter::once(&tip.text).chain(&tip.how) {
177 for name in placeholders(text) {
178 if !(rules.placeholder)(name) {
179 err(format!("unknown placeholder {{{name}}}"));
180 }
181 }
182 }
183 if let Some(problem) = (rules.face_problem)(tip.face_or(rules.default_face)) {
184 err(problem);
185 }
186 }
187 if errors.is_empty() {
188 Ok(TipTable { tips: raw.tips })
189 } else {
190 Err(errors)
191 }
192}
193
194pub fn placeholders(text: &str) -> Vec<&str> {
197 let mut names = Vec::new();
198 let mut rest = text;
199 while let Some((_, after)) = rest.split_once('{') {
200 let Some((name, tail)) = after.split_once('}') else {
201 break;
202 };
203 names.push(name);
204 rest = tail;
205 }
206 names
207}
208
209#[cfg(test)]
210mod tests;