use crate::PortType;
use super::ast::{Expr, TileHole, TileOptions, TilePiece};
use super::lexer::{Span, lex};
use super::parser::{for_source_from_text, parse_expression};
pub fn parse_template(
text: &str,
opts: &TileOptions,
span: Span,
) -> Result<Vec<TilePiece>, String> {
let mut p = TemplateParser {
chars: text.chars().collect(),
pos: 0,
opts,
span,
};
let pieces = p.pieces(false)?;
if p.pos < p.chars.len() {
return Err(p.err("unexpected `}` closing a block that was never opened"));
}
Ok(pieces)
}
struct TemplateParser<'a> {
chars: Vec<char>,
pos: usize,
opts: &'a TileOptions,
span: Span,
}
impl TemplateParser<'_> {
fn err(&self, msg: &str) -> String {
format!(
"tile at line {}, col {}: {msg} (template offset {})",
self.span.line, self.span.col, self.pos
)
}
fn starts_with(&self, s: &str) -> bool {
let sc: Vec<char> = s.chars().collect();
self.chars[self.pos..].starts_with(&sc)
}
fn take(&mut self, s: &str) {
self.pos += s.chars().count();
}
fn rest(&self) -> String {
self.chars[self.pos..].iter().collect()
}
fn pieces(&mut self, in_block: bool) -> Result<Vec<TilePiece>, String> {
let open = self.opts.open.clone();
let doubled = format!("{open}{open}");
let sigil = self.opts.sigil.clone();
let mut out: Vec<TilePiece> = Vec::new();
let mut static_buf = String::new();
let mut static_depth = 0i32;
let flush = |buf: &mut String, out: &mut Vec<TilePiece>| {
if !buf.is_empty() {
out.push(TilePiece::Static(std::mem::take(buf)));
}
};
while self.pos < self.chars.len() {
if self.starts_with(&doubled) {
self.take(&doubled);
static_buf.push_str(&open);
continue;
}
if self.starts_with(&open) {
flush(&mut static_buf, &mut out);
out.push(TilePiece::Hole(self.hole()?));
continue;
}
if self.starts_with(&sigil) {
let after: String = self.rest().chars().skip(sigil.chars().count()).collect();
if after.starts_with("for") && after[3..].starts_with(char::is_whitespace) {
flush(&mut static_buf, &mut out);
out.push(self.projection()?);
continue;
}
if after.starts_with("if") && after[2..].starts_with(char::is_whitespace) {
flush(&mut static_buf, &mut out);
out.push(self.branch()?);
continue;
}
}
let c = self.chars[self.pos];
if c == '{' {
static_depth += 1;
} else if c == '}' {
if in_block && static_depth == 0 {
break;
}
static_depth -= 1;
}
static_buf.push(c);
self.pos += 1;
}
flush(&mut static_buf, &mut out);
Ok(out)
}
fn hole(&mut self) -> Result<TileHole, String> {
let start = self.pos;
self.take(&self.opts.open.clone());
let inner = self.until_close()?;
let text = inner.trim().to_string();
if text.is_empty() {
return Err(self.err("empty hole"));
}
let (mut body, raw) = match text.strip_suffix('!') {
Some(b) => (b.trim().to_string(), true),
None => (text.clone(), false),
};
let mut format = None;
if let Some(idx) = rfind_top_level(&body, '|')
&& !body[..idx].ends_with('|')
&& body[idx + 1..]
.trim()
.chars()
.all(|c| c.is_ascii_alphanumeric() || ".<>^-+#0_".contains(c))
&& !body[idx + 1..].trim().is_empty()
{
format = Some(body[idx + 1..].trim().to_string());
body = body[..idx].trim().to_string();
}
let mut decl_type = None;
if let Some(idx) = rfind_top_level(&body, ':')
&& let Some(kw) = body.get(idx + 1..).map(str::trim)
&& PortType::from_keyword(kw).is_some()
{
decl_type = Some(kw.to_string());
body = body[..idx].trim().to_string();
}
if body.is_empty() {
return Err(self.err(&format!("hole `{text}` has no expression")));
}
let expr = parse_hole_expr(&body).map_err(|e| {
if let Some(idx) = rfind_top_level(&body, ':')
&& let Some(word) = body.get(idx + 1..).map(str::trim)
&& !word.is_empty()
&& word.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
{
return self.err(&format!(
"hole `{text}`: unknown type '{word}'; types are the port-type keywords (u64, i64, f64, str, bool, json, bytes, ...)"
));
}
self.err(&format!("hole `{text}`: {e}"))
})?;
let _ = start;
Ok(TileHole {
text,
expr,
decl_type,
format,
raw,
span: self.span,
})
}
fn until_close(&mut self) -> Result<String, String> {
let close = self.opts.close.clone();
let mut depth = 0i32;
let mut quote: Option<char> = None;
let mut buf = String::new();
while self.pos < self.chars.len() {
let c = self.chars[self.pos];
if let Some(q) = quote {
buf.push(c);
self.pos += 1;
if c == '\\' && self.pos < self.chars.len() {
buf.push(self.chars[self.pos]);
self.pos += 1;
} else if c == q {
quote = None;
}
continue;
}
if depth == 0 && self.starts_with(&close) {
self.take(&close);
return Ok(buf);
}
match c {
'"' | '\'' => quote = Some(c),
'(' | '[' | '{' => depth += 1,
')' | ']' | '}' => depth -= 1,
_ => {}
}
buf.push(c);
self.pos += 1;
}
Err(self.err(&format!("unterminated hole; expected `{close}`")))
}
fn projection(&mut self) -> Result<TilePiece, String> {
self.take(&self.opts.sigil.clone());
self.take("for");
let header = self.header_until_brace(HeaderKind::For)?;
let (source_text, sep) = split_sep(&header);
let source = for_source_from_text(source_text.trim(), self.span, true)
.map_err(|e| self.err(&format!("projection: {e}")))?;
let body = self.block()?;
Ok(TilePiece::Projection {
source,
sep,
body,
span: self.span,
})
}
fn branch(&mut self) -> Result<TilePiece, String> {
self.take(&self.opts.sigil.clone());
self.take("if");
let header = self.header_until_brace(HeaderKind::If)?;
let cond = parse_hole_expr(header.trim())
.map_err(|e| self.err(&format!("branch condition `{}`: {e}", header.trim())))?;
let then = self.block()?;
let save = self.pos;
self.skip_ws();
let else_kw = format!("{}else", self.opts.sigil);
let otherwise = if self.starts_with(&else_kw) {
self.take(&else_kw);
self.skip_ws();
Some(self.block()?)
} else {
self.pos = save;
None
};
Ok(TilePiece::Branch {
cond,
then,
otherwise,
span: self.span,
})
}
fn header_until_brace(&mut self, kind: HeaderKind) -> Result<String, String> {
let mut depth = 0i32;
let mut quote: Option<char> = None;
let mut buf = String::new();
while self.pos < self.chars.len() {
let c = self.chars[self.pos];
if let Some(q) = quote {
buf.push(c);
self.pos += 1;
if c == q {
quote = None;
}
continue;
}
if depth == 0
&& !self.opts.open.starts_with('{')
&& self.starts_with(&self.opts.open.clone())
{
return Err(self.err(
"directive has no `{` block; a hole cannot appear in a directive header",
));
}
if depth == 0 && c == '{' {
const HEADER_PUNCT: &str = "<>=!+-*/%.,()[]&|?:";
let placeholder = placeholder_len(&self.chars, self.pos).is_some_and(|len| {
let before = buf.chars().last().is_some_and(|b| HEADER_PUNCT.contains(b));
let after = self
.chars
.get(self.pos + len)
.is_some_and(|a| HEADER_PUNCT.contains(*a));
if before || after {
return true;
}
let rest: String = self.chars[self.pos + len..].iter().collect();
let rest = rest.trim_start();
if rest.is_empty()
|| rest.starts_with(&self.opts.sigil)
|| rest.starts_with(&self.opts.open)
{
return false;
}
if rest.starts_with(|c: char| HEADER_PUNCT.contains(c) && !"()[]".contains(c)) {
return true;
}
!header_complete(kind, &buf)
});
if !placeholder {
return Ok(buf);
}
}
match c {
'"' | '\'' => quote = Some(c),
'(' | '[' => depth += 1,
')' | ']' => depth -= 1,
_ => {}
}
buf.push(c);
self.pos += 1;
}
Err(self.err("directive has no `{` block"))
}
fn block(&mut self) -> Result<Vec<TilePiece>, String> {
if self.pos >= self.chars.len() || self.chars[self.pos] != '{' {
return Err(self.err("expected `{`"));
}
self.pos += 1;
let mut body = self.pieces(true)?;
if self.pos >= self.chars.len() || self.chars[self.pos] != '}' {
return Err(self.err("unterminated block; expected `}`"));
}
self.pos += 1;
trim_block(&mut body);
Ok(body)
}
fn skip_ws(&mut self) {
while self.pos < self.chars.len() && self.chars[self.pos].is_whitespace() {
self.pos += 1;
}
}
}
fn trim_block(body: &mut Vec<TilePiece>) {
if let Some(TilePiece::Static(s)) = body.first_mut() {
let t = s.trim_start().to_string();
*s = t;
}
if let Some(TilePiece::Static(s)) = body.last_mut() {
let t = s.trim_end().to_string();
*s = t;
}
body.retain(|p| !matches!(p, TilePiece::Static(s) if s.is_empty()));
}
#[derive(Clone, Copy)]
enum HeaderKind {
For,
If,
}
fn header_complete(kind: HeaderKind, header: &str) -> bool {
let h = header.trim();
if h.is_empty() {
return false;
}
match kind {
HeaderKind::If => parse_hole_expr(h).is_ok(),
HeaderKind::For => {
let dangling = h.ends_with(|c: char| "<>=!+-*/%&|,(".contains(c))
|| ["where", "order", "in", "&&", "||"]
.iter()
.any(|kw| h.ends_with(kw));
if dangling {
return false;
}
let (source, _) = split_sep(h);
for_source_from_text(source.trim(), Span { line: 0, col: 0 }, true).is_ok()
}
}
}
fn split_sep(header: &str) -> (String, Option<String>) {
let unescaped = header.replace("\\\"", "\"");
let t = unescaped.trim_end();
if let Some(q) = t.strip_suffix('"')
&& let Some(open_quote) = q.rfind('"')
&& q[..open_quote].trim_end().ends_with(" sep")
{
let sep = q[open_quote + 1..].to_string();
let head = q[..open_quote].trim_end();
let head = head[..head.len() - 3].trim_end();
return (head.to_string(), Some(sep));
}
(t.to_string(), None)
}
pub fn parse_hole_expr(text: &str) -> Result<Expr, String> {
let tokens = lex(text)?;
parse_expression(tokens)
}
fn rfind_top_level(s: &str, needle: char) -> Option<usize> {
let mut depth = 0i32;
let mut quote: Option<char> = None;
let mut found = None;
for (i, c) in s.char_indices() {
if let Some(q) = quote {
if c == q {
quote = None;
}
continue;
}
match c {
'"' | '\'' => quote = Some(c),
'(' | '[' | '{' => depth += 1,
')' | ']' | '}' => depth -= 1,
_ if depth == 0 && c == needle => found = Some(i),
_ => {}
}
}
found
}
fn placeholder_len(chars: &[char], pos: usize) -> Option<usize> {
let mut i = pos + 1;
let first = *chars.get(i)?;
if !(first.is_ascii_alphabetic() || first == '_') {
return None;
}
while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '_') {
i += 1;
}
(chars.get(i) == Some(&'}')).then_some(i + 1 - pos)
}
pub fn render_template(pieces: &[TilePiece], opts: &TileOptions) -> String {
let mut out = String::new();
for piece in pieces {
match piece {
TilePiece::Static(s) => {
out.push_str(&s.replace(&opts.open, &format!("{}{}", opts.open, opts.open)))
}
TilePiece::Hole(h) => {
out.push_str(&opts.open);
out.push_str(&h.text);
out.push_str(&opts.close);
}
TilePiece::Projection {
source, sep, body, ..
} => {
out.push_str(&opts.sigil);
out.push_str("for ");
out.push_str(&source.text);
if let Some(s) = sep {
out.push_str(&format!(" sep \"{s}\""));
}
out.push_str(" { ");
out.push_str(&render_template(body, opts));
out.push_str(" }");
}
TilePiece::Branch {
cond,
then,
otherwise,
..
} => {
out.push_str(&opts.sigil);
out.push_str("if ");
out.push_str(&super::pprint::pp_expr(cond));
out.push_str(" { ");
out.push_str(&render_template(then, opts));
out.push_str(" }");
if let Some(o) = otherwise {
out.push(' ');
out.push_str(&opts.sigil);
out.push_str("else { ");
out.push_str(&render_template(o, opts));
out.push_str(" }");
}
}
}
}
out
}