Skip to main content

command_stream/fy/
engine.rs

1//! Recursive rule engine for translating a meta-language links network.
2//!
3//! Uses meta-language's query/capture/template model. The evaluator adds named
4//! rendering modes, conditional sections, variadic separators and indentation
5//! shared with the npm 0.46.0 implementation. Rust meta-language 1.0.0 now has
6//! recursive rendering too; the historical gaps and current status are recorded
7//! in `js/docs/meta-language-gaps.md`.
8
9use std::collections::HashMap;
10
11use meta_language::link_network::{LinkId, LinkNetwork, LinkType};
12use meta_language::translation_rules::{TranslationRule, TranslationRuleSet};
13
14/// How a placeholder renders its captured link.
15#[derive(Clone, Default)]
16pub struct Mode {
17    /// Render the captured link in this target language instead of the current one.
18    pub language: Option<String>,
19    /// Post-process the rendered text.
20    pub transform: Option<fn(&str) -> String>,
21}
22
23/// A parsed `{...}` placeholder.
24struct Placeholder<'a> {
25    variadic: bool,
26    name: &'a str,
27    mode: Option<&'a str>,
28    separator: Option<&'a str>,
29    length: usize,
30}
31
32/// Parses `{name}`, `{name:mode}` or `{*name:mode|separator}` at the start of
33/// `source`, or the conditional opener `{?name}`.
34fn 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
78/// Parses the conditional opener `{?name}`.
79fn 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
111/// Indents every line after the first by `indent`.
112fn 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
130/// The whitespace run at the end of `output`, when `output` ends on a blank
131/// line prefix.
132fn 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
147/// Renders a links network by recursively substituting translation-rule templates.
148pub 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    /// Builds an engine for `network`, resolving each rule's query once.
161    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        // Rule selection is query-driven: each rule's `LinkQuery` is evaluated
169        // against the whole network once, and the first rule to claim a link
170        // owns it.
171        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    /// Diagnostics collected while rendering (constructs with no rule).
191    pub fn report(&self) -> Vec<String> {
192        self.diagnostics.clone()
193    }
194
195    /// Renders one link in the engine's default target language.
196    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    /// Renders one link in `language`.
202    pub fn render(&mut self, link_id: LinkId, language: &str) -> String {
203        let Some(&index) = self.rules_by_link.get(&link_id) else {
204            // Token links are leaves and render as their own text; an unclaimed
205            // *syntax* node means a construct nobody translates.
206            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    /// The source text a link captures, without applying any rule.
226    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    /// Looks a template up through the configured fallback chain.
236    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    /// Resolves a capture name to a child link id.
249    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    /// The ordered children of a link.
260    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    /// Renders `link_id` in the mode a placeholder asked for.
268    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                    // An unresolved capture renders as nothing rather than as
332                    // its own source text, so optional children disappear.
333                    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}