use std::collections::HashMap;
use meta_language::link_network::{LinkId, LinkNetwork, LinkType};
use meta_language::translation_rules::{TranslationRule, TranslationRuleSet};
#[derive(Clone, Default)]
pub struct Mode {
pub language: Option<String>,
pub transform: Option<fn(&str) -> String>,
}
struct Placeholder<'a> {
variadic: bool,
name: &'a str,
mode: Option<&'a str>,
separator: Option<&'a str>,
length: usize,
}
fn parse_placeholder(source: &str) -> Option<Placeholder<'_>> {
let body_end = source.find('}')?;
if !source.starts_with('{') {
return None;
}
let mut body = &source[1..body_end];
if body.is_empty() || body.starts_with('?') || body.starts_with('/') {
return None;
}
let variadic = body.starts_with('*');
if variadic {
body = &body[1..];
}
let (head, separator) = match body.find('|') {
Some(bar) => (&body[..bar], Some(&body[bar + 1..])),
None => (body, None),
};
let (name, mode) = match head.find(':') {
Some(colon) => (&head[..colon], Some(&head[colon + 1..])),
None => (head, None),
};
if name != "." && !is_capture_name(name) {
return None;
}
Some(Placeholder {
variadic,
name,
mode,
separator,
length: body_end + 1,
})
}
fn is_capture_name(name: &str) -> bool {
let mut characters = name.chars();
matches!(characters.next(), Some(first) if first.is_ascii_alphabetic() || first == '_')
&& characters.all(|character| {
character.is_ascii_alphanumeric() || character == '_' || character == '-'
})
}
fn parse_conditional(source: &str) -> Option<(&str, usize)> {
let body_end = source.find('}')?;
let body = source.strip_prefix("{?")?;
let name = &body[..body_end - 2];
if !is_capture_name(name) {
return None;
}
Some((name, body_end + 1))
}
fn decode_separator(separator: Option<&str>) -> String {
let Some(separator) = separator else {
return String::new();
};
let mut decoded = String::new();
let mut characters = separator.chars();
while let Some(character) = characters.next() {
if character != '\\' {
decoded.push(character);
continue;
}
match characters.next() {
Some('n') => decoded.push('\n'),
Some('t') => decoded.push('\t'),
Some('s') => decoded.push(' '),
Some(other) => decoded.push(other),
None => decoded.push('\\'),
}
}
decoded
}
fn indent_continuation(value: &str, indent: &str) -> String {
if indent.is_empty() || !value.contains('\n') {
return value.to_string();
}
value
.split('\n')
.enumerate()
.map(|(position, line)| {
if position == 0 || line.is_empty() {
line.to_string()
} else {
format!("{indent}{line}")
}
})
.collect::<Vec<_>>()
.join("\n")
}
fn current_indent(output: &str) -> &str {
let line = match output.rfind('\n') {
Some(newline) => &output[newline + 1..],
None => output,
};
if line
.chars()
.all(|character| character == ' ' || character == '\t')
{
line
} else {
""
}
}
pub struct RuleEngine<'a> {
pub preserve_layout: bool,
network: &'a LinkNetwork,
rule_set: &'a TranslationRuleSet,
language: String,
fallbacks: HashMap<String, Vec<String>>,
modes: HashMap<String, Mode>,
rules_by_link: HashMap<LinkId, usize>,
diagnostics: Vec<String>,
}
impl<'a> RuleEngine<'a> {
pub fn new(
network: &'a LinkNetwork,
rule_set: &'a TranslationRuleSet,
language: &str,
fallbacks: HashMap<String, Vec<String>>,
modes: HashMap<String, Mode>,
) -> Self {
let mut rules_by_link = HashMap::new();
for (index, rule) in rule_set.rules().iter().enumerate() {
for query_match in network.query_matches(rule.query()) {
rules_by_link.entry(query_match.link_id()).or_insert(index);
}
}
RuleEngine {
preserve_layout: false,
network,
rule_set,
language: language.to_string(),
fallbacks,
modes,
rules_by_link,
diagnostics: Vec::new(),
}
}
pub fn report(&self) -> Vec<String> {
self.diagnostics.clone()
}
pub fn render_root(&mut self, link_id: LinkId) -> String {
let language = self.language.clone();
self.render(link_id, &language)
}
pub fn render(&mut self, link_id: LinkId, language: &str) -> String {
let Some(&index) = self.rules_by_link.get(&link_id) else {
if let Some(link) = self.network.link(link_id) {
if link.metadata().link_type() == Some(LinkType::Syntax) {
let term = link.metadata().term().unwrap_or("?").to_string();
self.diagnostics
.push(format!("no translation rule for `{term}`"));
}
}
return self.captured_text(link_id);
};
let rule = &self.rule_set.rules()[index];
let Some(template) = self.template_for(rule, language) else {
self.diagnostics
.push(format!("rule `{}` has no {language} template", rule.name()));
return self.captured_text(link_id);
};
self.expand(&template, index, link_id, language)
}
fn captured_text(&self, link_id: LinkId) -> String {
let language = self
.network
.link(link_id)
.and_then(|link| link.metadata().language())
.unwrap_or("Shell");
self.network.render_source_from(link_id, language)
}
fn template_for(&self, rule: &TranslationRule, language: &str) -> Option<String> {
if let Some(template) = rule.templates().get(language) {
return Some(template.source().to_string());
}
for candidate in self.fallbacks.get(language)? {
if let Some(template) = rule.templates().get(candidate) {
return Some(template.source().to_string());
}
}
None
}
fn resolve(&self, rule_index: usize, link_id: LinkId, name: &str) -> Option<LinkId> {
if name == "." {
return Some(link_id);
}
let index = *self.rule_set.rules()[rule_index]
.reference_captures()
.get(name)?;
self.network.link(link_id)?.references().get(index).copied()
}
fn children_of(&self, link_id: LinkId) -> Vec<LinkId> {
self.network
.link(link_id)
.map(|link| link.references().to_vec())
.unwrap_or_default()
}
fn render_mode(&mut self, link_id: LinkId, mode: Option<&str>, language: &str) -> String {
let Some(mode) = mode else {
return self.render(link_id, language);
};
let Some(definition) = self.modes.get(mode).cloned() else {
self.diagnostics
.push(format!("unknown placeholder mode `{mode}`"));
return self.render(link_id, language);
};
let rendered = match definition.language.as_deref() {
Some(language) => self.render(link_id, language),
None => self.captured_text(link_id),
};
match definition.transform {
Some(transform) => transform(&rendered),
None => rendered,
}
}
fn expand(
&mut self,
source: &str,
rule_index: usize,
link_id: LinkId,
language: &str,
) -> String {
let mut output = String::new();
let mut index = 0usize;
while index < source.len() {
let rest = &source[index..];
if let Some(stripped) = rest.strip_prefix("{{") {
let _ = stripped;
output.push('{');
index += 2;
continue;
}
if rest.starts_with("}}") {
output.push('}');
index += 2;
continue;
}
if let Some((name, opener)) = parse_conditional(rest) {
let closer = format!("{{/{name}}}");
let end = rest.find(&closer).unwrap_or(rest.len());
let body = rest[opener.min(end)..end].to_string();
if self.resolve(rule_index, link_id, name).is_some() {
let expanded = self.expand(&body, rule_index, link_id, language);
output.push_str(&expanded);
}
index += if end == rest.len() {
rest.len()
} else {
end + closer.len()
};
continue;
}
if let Some(placeholder) = parse_placeholder(rest) {
let target = self.resolve(rule_index, link_id, placeholder.name);
let Some(target) = target else {
index += placeholder.length;
continue;
};
let indent = if self.preserve_layout {
String::new()
} else {
current_indent(&output).to_string()
};
let mode = placeholder.mode.map(str::to_string);
let value = if placeholder.variadic {
let separator = decode_separator(placeholder.separator);
let children = self.children_of(target);
let rendered: Vec<String> = children
.into_iter()
.map(|child| {
let text = self.render_mode(child, mode.as_deref(), language);
indent_continuation(&text, &indent)
})
.collect();
rendered.join(&separator)
} else {
let text = self.render_mode(target, mode.as_deref(), language);
indent_continuation(&text, &indent)
};
output.push_str(&value);
index += placeholder.length;
continue;
}
let character = rest.chars().next().expect("rest is non-empty");
output.push(character);
index += character.len_utf8();
}
output
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decodes_separator_escapes() {
assert_eq!(decode_separator(Some("\\n")), "\n");
assert_eq!(decode_separator(Some(", ")), ", ");
assert_eq!(decode_separator(None), "");
}
#[test]
fn indents_continuation_lines_only() {
assert_eq!(indent_continuation("a\nb\n\nc", " "), "a\n b\n\n c");
assert_eq!(indent_continuation("a", " "), "a");
}
#[test]
fn reports_the_current_indent() {
assert_eq!(current_indent("x\n "), " ");
assert_eq!(current_indent("x\n y"), "");
}
#[test]
fn parses_placeholder_forms() {
let placeholder = parse_placeholder("{*items:string|, }rest").expect("placeholder");
assert!(placeholder.variadic);
assert_eq!(placeholder.name, "items");
assert_eq!(placeholder.mode, Some("string"));
assert_eq!(placeholder.separator, Some(", "));
let plain = parse_placeholder("{.}").expect("placeholder");
assert_eq!(plain.name, ".");
assert!(!plain.variadic);
assert!(parse_placeholder("{?name}").is_none());
assert_eq!(
parse_conditional("{?name}body").map(|it| it.0),
Some("name")
);
}
}