1use std::collections::HashMap;
10
11use meta_language::link_network::{LinkId, LinkNetwork, LinkType};
12use meta_language::translation_rules::{TranslationRule, TranslationRuleSet};
13
14#[derive(Clone, Default)]
16pub struct Mode {
17 pub language: Option<String>,
19 pub transform: Option<fn(&str) -> String>,
21}
22
23struct Placeholder<'a> {
25 variadic: bool,
26 name: &'a str,
27 mode: Option<&'a str>,
28 separator: Option<&'a str>,
29 length: usize,
30}
31
32fn parse_placeholder(source: &str) -> Option<Placeholder<'_>> {
35 let body_end = source.find('}')?;
36 if !source.starts_with('{') {
37 return None;
38 }
39 let mut body = &source[1..body_end];
40 if body.is_empty() || body.starts_with('?') || body.starts_with('/') {
41 return None;
42 }
43
44 let variadic = body.starts_with('*');
45 if variadic {
46 body = &body[1..];
47 }
48
49 let (head, separator) = match body.find('|') {
50 Some(bar) => (&body[..bar], Some(&body[bar + 1..])),
51 None => (body, None),
52 };
53 let (name, mode) = match head.find(':') {
54 Some(colon) => (&head[..colon], Some(&head[colon + 1..])),
55 None => (head, None),
56 };
57
58 if name != "." && !is_capture_name(name) {
59 return None;
60 }
61 Some(Placeholder {
62 variadic,
63 name,
64 mode,
65 separator,
66 length: body_end + 1,
67 })
68}
69
70fn is_capture_name(name: &str) -> bool {
71 let mut characters = name.chars();
72 matches!(characters.next(), Some(first) if first.is_ascii_alphabetic() || first == '_')
73 && characters.all(|character| {
74 character.is_ascii_alphanumeric() || character == '_' || character == '-'
75 })
76}
77
78fn parse_conditional(source: &str) -> Option<(&str, usize)> {
80 let body_end = source.find('}')?;
81 let body = source.strip_prefix("{?")?;
82 let name = &body[..body_end - 2];
83 if !is_capture_name(name) {
84 return None;
85 }
86 Some((name, body_end + 1))
87}
88
89fn decode_separator(separator: Option<&str>) -> String {
90 let Some(separator) = separator else {
91 return String::new();
92 };
93 let mut decoded = String::new();
94 let mut characters = separator.chars();
95 while let Some(character) = characters.next() {
96 if character != '\\' {
97 decoded.push(character);
98 continue;
99 }
100 match characters.next() {
101 Some('n') => decoded.push('\n'),
102 Some('t') => decoded.push('\t'),
103 Some('s') => decoded.push(' '),
104 Some(other) => decoded.push(other),
105 None => decoded.push('\\'),
106 }
107 }
108 decoded
109}
110
111fn indent_continuation(value: &str, indent: &str) -> String {
113 if indent.is_empty() || !value.contains('\n') {
114 return value.to_string();
115 }
116 value
117 .split('\n')
118 .enumerate()
119 .map(|(position, line)| {
120 if position == 0 || line.is_empty() {
121 line.to_string()
122 } else {
123 format!("{indent}{line}")
124 }
125 })
126 .collect::<Vec<_>>()
127 .join("\n")
128}
129
130fn current_indent(output: &str) -> &str {
133 let line = match output.rfind('\n') {
134 Some(newline) => &output[newline + 1..],
135 None => output,
136 };
137 if line
138 .chars()
139 .all(|character| character == ' ' || character == '\t')
140 {
141 line
142 } else {
143 ""
144 }
145}
146
147pub struct RuleEngine<'a> {
149 pub preserve_layout: bool,
150 network: &'a LinkNetwork,
151 rule_set: &'a TranslationRuleSet,
152 language: String,
153 fallbacks: HashMap<String, Vec<String>>,
154 modes: HashMap<String, Mode>,
155 rules_by_link: HashMap<LinkId, usize>,
156 diagnostics: Vec<String>,
157}
158
159impl<'a> RuleEngine<'a> {
160 pub fn new(
162 network: &'a LinkNetwork,
163 rule_set: &'a TranslationRuleSet,
164 language: &str,
165 fallbacks: HashMap<String, Vec<String>>,
166 modes: HashMap<String, Mode>,
167 ) -> Self {
168 let mut rules_by_link = HashMap::new();
172 for (index, rule) in rule_set.rules().iter().enumerate() {
173 for query_match in network.query_matches(rule.query()) {
174 rules_by_link.entry(query_match.link_id()).or_insert(index);
175 }
176 }
177
178 RuleEngine {
179 preserve_layout: false,
180 network,
181 rule_set,
182 language: language.to_string(),
183 fallbacks,
184 modes,
185 rules_by_link,
186 diagnostics: Vec::new(),
187 }
188 }
189
190 pub fn report(&self) -> Vec<String> {
192 self.diagnostics.clone()
193 }
194
195 pub fn render_root(&mut self, link_id: LinkId) -> String {
197 let language = self.language.clone();
198 self.render(link_id, &language)
199 }
200
201 pub fn render(&mut self, link_id: LinkId, language: &str) -> String {
203 let Some(&index) = self.rules_by_link.get(&link_id) else {
204 if let Some(link) = self.network.link(link_id) {
207 if link.metadata().link_type() == Some(LinkType::Syntax) {
208 let term = link.metadata().term().unwrap_or("?").to_string();
209 self.diagnostics
210 .push(format!("no translation rule for `{term}`"));
211 }
212 }
213 return self.captured_text(link_id);
214 };
215
216 let rule = &self.rule_set.rules()[index];
217 let Some(template) = self.template_for(rule, language) else {
218 self.diagnostics
219 .push(format!("rule `{}` has no {language} template", rule.name()));
220 return self.captured_text(link_id);
221 };
222 self.expand(&template, index, link_id, language)
223 }
224
225 fn captured_text(&self, link_id: LinkId) -> String {
227 let language = self
228 .network
229 .link(link_id)
230 .and_then(|link| link.metadata().language())
231 .unwrap_or("Shell");
232 self.network.render_source_from(link_id, language)
233 }
234
235 fn template_for(&self, rule: &TranslationRule, language: &str) -> Option<String> {
237 if let Some(template) = rule.templates().get(language) {
238 return Some(template.source().to_string());
239 }
240 for candidate in self.fallbacks.get(language)? {
241 if let Some(template) = rule.templates().get(candidate) {
242 return Some(template.source().to_string());
243 }
244 }
245 None
246 }
247
248 fn resolve(&self, rule_index: usize, link_id: LinkId, name: &str) -> Option<LinkId> {
250 if name == "." {
251 return Some(link_id);
252 }
253 let index = *self.rule_set.rules()[rule_index]
254 .reference_captures()
255 .get(name)?;
256 self.network.link(link_id)?.references().get(index).copied()
257 }
258
259 fn children_of(&self, link_id: LinkId) -> Vec<LinkId> {
261 self.network
262 .link(link_id)
263 .map(|link| link.references().to_vec())
264 .unwrap_or_default()
265 }
266
267 fn render_mode(&mut self, link_id: LinkId, mode: Option<&str>, language: &str) -> String {
269 let Some(mode) = mode else {
270 return self.render(link_id, language);
271 };
272 let Some(definition) = self.modes.get(mode).cloned() else {
273 self.diagnostics
274 .push(format!("unknown placeholder mode `{mode}`"));
275 return self.render(link_id, language);
276 };
277 let rendered = match definition.language.as_deref() {
278 Some(language) => self.render(link_id, language),
279 None => self.captured_text(link_id),
280 };
281 match definition.transform {
282 Some(transform) => transform(&rendered),
283 None => rendered,
284 }
285 }
286
287 fn expand(
288 &mut self,
289 source: &str,
290 rule_index: usize,
291 link_id: LinkId,
292 language: &str,
293 ) -> String {
294 let mut output = String::new();
295 let mut index = 0usize;
296
297 while index < source.len() {
298 let rest = &source[index..];
299
300 if let Some(stripped) = rest.strip_prefix("{{") {
301 let _ = stripped;
302 output.push('{');
303 index += 2;
304 continue;
305 }
306 if rest.starts_with("}}") {
307 output.push('}');
308 index += 2;
309 continue;
310 }
311
312 if let Some((name, opener)) = parse_conditional(rest) {
313 let closer = format!("{{/{name}}}");
314 let end = rest.find(&closer).unwrap_or(rest.len());
315 let body = rest[opener.min(end)..end].to_string();
316 if self.resolve(rule_index, link_id, name).is_some() {
317 let expanded = self.expand(&body, rule_index, link_id, language);
318 output.push_str(&expanded);
319 }
320 index += if end == rest.len() {
321 rest.len()
322 } else {
323 end + closer.len()
324 };
325 continue;
326 }
327
328 if let Some(placeholder) = parse_placeholder(rest) {
329 let target = self.resolve(rule_index, link_id, placeholder.name);
330 let Some(target) = target else {
331 index += placeholder.length;
334 continue;
335 };
336
337 let indent = if self.preserve_layout {
338 String::new()
339 } else {
340 current_indent(&output).to_string()
341 };
342 let mode = placeholder.mode.map(str::to_string);
343 let value = if placeholder.variadic {
344 let separator = decode_separator(placeholder.separator);
345 let children = self.children_of(target);
346 let rendered: Vec<String> = children
347 .into_iter()
348 .map(|child| {
349 let text = self.render_mode(child, mode.as_deref(), language);
350 indent_continuation(&text, &indent)
351 })
352 .collect();
353 rendered.join(&separator)
354 } else {
355 let text = self.render_mode(target, mode.as_deref(), language);
356 indent_continuation(&text, &indent)
357 };
358 output.push_str(&value);
359 index += placeholder.length;
360 continue;
361 }
362
363 let character = rest.chars().next().expect("rest is non-empty");
364 output.push(character);
365 index += character.len_utf8();
366 }
367
368 output
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375
376 #[test]
377 fn decodes_separator_escapes() {
378 assert_eq!(decode_separator(Some("\\n")), "\n");
379 assert_eq!(decode_separator(Some(", ")), ", ");
380 assert_eq!(decode_separator(None), "");
381 }
382
383 #[test]
384 fn indents_continuation_lines_only() {
385 assert_eq!(indent_continuation("a\nb\n\nc", " "), "a\n b\n\n c");
386 assert_eq!(indent_continuation("a", " "), "a");
387 }
388
389 #[test]
390 fn reports_the_current_indent() {
391 assert_eq!(current_indent("x\n "), " ");
392 assert_eq!(current_indent("x\n y"), "");
393 }
394
395 #[test]
396 fn parses_placeholder_forms() {
397 let placeholder = parse_placeholder("{*items:string|, }rest").expect("placeholder");
398 assert!(placeholder.variadic);
399 assert_eq!(placeholder.name, "items");
400 assert_eq!(placeholder.mode, Some("string"));
401 assert_eq!(placeholder.separator, Some(", "));
402
403 let plain = parse_placeholder("{.}").expect("placeholder");
404 assert_eq!(plain.name, ".");
405 assert!(!plain.variadic);
406
407 assert!(parse_placeholder("{?name}").is_none());
408 assert_eq!(
409 parse_conditional("{?name}body").map(|it| it.0),
410 Some("name")
411 );
412 }
413}