1use crate::prelude::*;
7use serde::{Deserialize, Serialize};
8
9use alloc::borrow::Cow;
10use alloc::collections::BTreeMap;
11use core::cmp::Reverse;
12
13use shuyi_core::text::{Part, split_tokens};
14
15use crate::dialogue::char_problem;
16use crate::error::{ContentError, line_of};
17use crate::font::CellWidths;
18use crate::format::{AssetKind, AssetPath, DecodeCx, Format, TEXT_MAX_BYTES, text};
19use crate::lang::text_width;
20use crate::ron::parse_ron;
21
22#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
24pub struct Names {
25 pub names: BTreeMap<String, String>,
27}
28
29impl AssetKind for Names {
30 const NAME: &'static str = "names table";
31}
32
33#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
35pub struct NamesRon;
36
37impl Format for NamesRon {
38 type Asset = Names;
39
40 fn id(&self) -> &'static str {
41 "names"
42 }
43
44 fn extensions(&self) -> &'static [&'static str] {
45 &["ron"]
46 }
47
48 fn max_bytes(&self) -> usize {
49 TEXT_MAX_BYTES
50 }
51
52 fn decode(
53 &self,
54 file: &AssetPath,
55 bytes: &[u8],
56 _cx: &DecodeCx<'_>,
57 ) -> Result<Names, Vec<ContentError>> {
58 from_source(file.shown(), text(file, bytes)?)
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub struct NameToken<'a> {
65 pub id: &'a str,
67 pub capital: bool,
69}
70
71pub fn name_token(token: &str) -> Option<NameToken<'_>> {
74 if let Some(id) = token.strip_prefix("n:") {
75 return Some(NameToken { id, capital: false });
76 }
77 token
78 .strip_prefix("N:")
79 .map(|id| NameToken { id, capital: true })
80}
81
82pub fn is_name_id(id: &str) -> bool {
85 !id.is_empty()
86 && id
87 .bytes()
88 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_' || b == b'.')
89}
90
91impl Names {
92 pub fn get(&self, id: &str) -> Option<&str> {
94 self.names.get(id).map(String::as_str)
95 }
96
97 pub fn longest(&self, widths: &CellWidths) -> usize {
101 self.names
102 .values()
103 .map(|n| text_width(n, widths))
104 .max()
105 .unwrap_or(0)
106 }
107
108 fn token(&self, token: &str) -> Option<Cow<'_, str>> {
110 let t = name_token(token)?;
111 let name = self.get(t.id)?;
112 Some(if t.capital {
113 Cow::Owned(capitalise(name))
114 } else {
115 Cow::Borrowed(name)
116 })
117 }
118
119 pub fn substitute<'t>(&self, text: &'t str) -> Cow<'t, str> {
123 if !text.contains('{') {
124 return Cow::Borrowed(text);
125 }
126 let mut out = String::with_capacity(text.len());
127 for part in split_tokens(text) {
128 match part {
129 Part::Token(t) => {
130 if let Some(name) = self.token(t) {
131 out.push_str(&name);
132 } else {
133 out.push('{');
134 out.push_str(t);
135 out.push('}');
136 }
137 }
138 Part::Text(t) | Part::Unclosed(t) => out.push_str(t),
139 }
140 }
141 Cow::Owned(out)
142 }
143
144 pub fn literal_in(&self, text: &str) -> Option<(&str, &str)> {
150 Literals::of(self).find_in(text)
151 }
152}
153
154pub(crate) struct Literals<'n> {
160 forms: Vec<(&'n str, &'n str)>,
162 by_first: BTreeMap<u8, Vec<usize>>,
165}
166
167impl<'n> Literals<'n> {
168 pub(crate) fn of(names: &'n Names) -> Self {
170 let forms: Vec<(&str, &str)> = names
171 .names
172 .iter()
173 .filter_map(|(id, name)| Some((id.as_str(), literal_form(name)?)))
174 .filter(|(_, form)| !form.is_empty())
175 .collect();
176 let mut by_first: BTreeMap<u8, Vec<usize>> = BTreeMap::new();
177 for (k, (_, form)) in forms.iter().enumerate() {
178 by_first.entry(form.as_bytes()[0]).or_default().push(k);
179 }
180 Self { forms, by_first }
181 }
182
183 pub(crate) fn find_in(&self, text: &str) -> Option<(&'n str, &'n str)> {
189 let mut found: Vec<usize> = Vec::new();
190 let plain = split_tokens(text).filter_map(|part| match part {
191 Part::Text(t) | Part::Unclosed(t) => Some(t),
192 Part::Token(_) => None,
193 });
194 for part in plain {
195 let mut at_start = true;
196 for (i, c) in part.char_indices() {
197 if at_start && let Some(forms) = self.by_first.get(&part.as_bytes()[i]) {
198 found.extend(
199 forms
200 .iter()
201 .filter(|&&k| whole_word_at(part, i, self.forms[k].1)),
202 );
203 }
204 at_start = !c.is_ascii_alphanumeric();
205 }
206 }
207 found
208 .into_iter()
209 .max_by_key(|&k| (self.forms[k].1.len(), Reverse(k)))
210 .map(|k| self.forms[k])
211 }
212}
213
214fn whole_word_at(text: &str, i: usize, word: &str) -> bool {
217 text.as_bytes()[i..].starts_with(word.as_bytes())
218 && !text[i + word.len()..]
219 .chars()
220 .next()
221 .is_some_and(|c| c.is_ascii_alphanumeric())
222}
223
224pub fn literal_form(name: &str) -> Option<&str> {
229 if !name.bytes().any(|b| b.is_ascii_uppercase()) {
230 return None;
231 }
232 let form = ["the ", "a ", "an "]
233 .iter()
234 .find_map(|article| name.strip_prefix(article))
235 .unwrap_or(name);
236 Some(form)
237}
238
239fn is_latin_letter(c: char) -> bool {
242 c.is_ascii_alphabetic() || (('\u{c0}'..='\u{24f}').contains(&c) && c.is_alphabetic())
243}
244
245fn capitalise(name: &str) -> String {
248 let mut chars = name.chars();
249 match chars.next() {
250 Some(first) if is_latin_letter(first) => first.to_uppercase().chain(chars).collect(),
251 _ => name.to_owned(),
252 }
253}
254
255pub fn with_part(
260 mut names: Names,
261 story: &str,
262 file: &str,
263 source: &str,
264) -> Result<Names, Vec<ContentError>> {
265 let part = from_source(file, source)?;
266 let mut errors = Vec::new();
267 for (id, name) in part.names {
268 let mut err = |message: String| {
269 let e = ContentError::new(file, message);
270 errors.push(match line_of(source, &format!("\"{id}\":")) {
271 Some(l) => e.at(l, None),
272 None => e,
273 });
274 };
275 if names.names.contains_key(&id) {
276 err(format!("name id \"{id}\" is already in {story}"));
277 } else if let Some((other, _)) = names.names.iter().find(|(_, n)| **n == name) {
278 err(format!(
279 "\"{name}\" is the name of \"{other}\" in {story} too; names must be unique"
280 ));
281 } else {
282 names.names.insert(id, name);
283 }
284 }
285 if errors.is_empty() {
286 Ok(names)
287 } else {
288 Err(errors)
289 }
290}
291
292pub fn from_source(file: &str, source: &str) -> Result<Names, Vec<ContentError>> {
298 let names: BTreeMap<String, String> = parse_ron(file, source).map_err(|e| vec![e])?;
299 let mut errors = Vec::new();
300 let mut err = |id: &str, message: String| {
301 let e = ContentError::new(file, message);
302 errors.push(match line_of(source, &format!("\"{id}\":")) {
303 Some(l) => e.at(l, None),
304 None => e,
305 });
306 };
307 let mut first_with: BTreeMap<&str, &str> = BTreeMap::new();
308 for (id, name) in &names {
309 if !is_name_id(id) {
310 err(
311 id,
312 format!("\"{id}\" is not a valid name id; use lowercase letters, digits, _ and ."),
313 );
314 }
315 let key = format!("\"{id}\":");
316 if source
317 .lines()
318 .filter(|l| l.trim_start().starts_with(&key))
319 .count()
320 > 1
321 {
322 err(id, format!("name id \"{id}\" is listed more than once"));
323 }
324 if name.trim().is_empty() {
325 err(id, format!("name \"{id}\" is empty"));
326 }
327 if name.contains(['{', '}']) {
328 err(
329 id,
330 format!("name \"{id}\" contains a brace; names can't hold tokens"),
331 );
332 }
333 if let Some(problem) = name.chars().find_map(char_problem) {
334 err(id, format!("name \"{id}\": {problem}"));
335 }
336 if let Some(other) = first_with.insert(name, id) {
337 err(
338 id,
339 format!(
340 "\"{name}\" is the name of both \"{other}\" and \"{id}\"; names must be unique"
341 ),
342 );
343 }
344 }
345 if errors.is_empty() {
346 Ok(Names { names })
347 } else {
348 Err(errors)
349 }
350}
351
352#[cfg(test)]
353mod tests;