use std::borrow::Cow;
use crate::grammar::ast::{ArgSlot, Ast, Label, Node, Presence};
use zpl_toolchain_diagnostics::Span;
use zpl_toolchain_spec_tables::ParserTables;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Indent {
#[default]
None,
Label,
Field,
}
#[derive(Debug, Clone, Default)]
pub struct EmitConfig {
pub indent: Indent,
}
pub fn emit_zpl(ast: &Ast, tables: Option<&ParserTables>, config: &EmitConfig) -> String {
let mut out = String::new();
for label in &ast.labels {
emit_label(&mut out, label, tables, config);
}
out
}
fn emit_label(out: &mut String, label: &Label, tables: Option<&ParserTables>, config: &EmitConfig) {
let mut in_label = false;
let mut in_field = false;
let mut cmd_prefix: char = '^';
for node in &label.nodes {
match node {
Node::Command { code, args, .. } => {
let is_xa = code == "^XA";
let is_xz = code == "^XZ";
if is_xz {
in_field = false;
in_label = false;
}
let entry = tables.and_then(|t| t.cmd_by_code(code));
let opens_field = entry.is_some_and(|ce| ce.opens_field);
let closes_field = entry.is_some_and(|ce| ce.closes_field);
push_indent(out, config, in_label, in_field);
if closes_field {
in_field = false;
}
emit_command(out, code, cmd_prefix, args, tables);
out.push('\n');
if code == "^CC"
&& let Some(arg) = args.first()
&& arg.presence == Presence::Value
&& let Some(val) = &arg.value
&& let Some(ch) = val.chars().next()
{
cmd_prefix = ch;
}
if is_xa {
in_label = true;
}
if opens_field {
in_field = true;
}
}
Node::FieldData { content, .. } => {
trim_trailing_newline(out);
out.push_str(content);
out.push('\n');
}
Node::RawData { data, .. } => {
if let Some(d) = data {
trim_trailing_newline(out);
out.push_str(d);
if !d.ends_with('\n') {
out.push('\n');
}
}
}
Node::Trivia { text, .. } => {
let trimmed = text.trim();
if trimmed.is_empty() {
continue;
}
push_indent(out, config, in_label, in_field);
out.push_str(trimmed);
out.push('\n');
}
}
}
}
fn emit_command(
out: &mut String,
code: &str,
prefix: char,
args: &[ArgSlot],
tables: Option<&ParserTables>,
) {
let display_code = remap_prefix(code, prefix);
out.push_str(&display_code);
if args.is_empty() {
return;
}
let entry = tables.and_then(|t| t.cmd_by_code(code));
let sig = entry.and_then(|e| {
e.signature_overrides
.as_ref()
.and_then(|ov| ov.get(code))
.or(e.signature.as_ref())
});
let joiner = sig.map_or(",", |s| s.joiner.as_str());
let split_rule = sig.and_then(|s| s.split_rule.as_ref());
let arg_values: Vec<&str> = args
.iter()
.map(|a| match a.presence {
Presence::Value => a.value.as_deref().unwrap_or(""),
Presence::Empty | Presence::Unset => "",
})
.collect();
let (merged, merged_len) = if let Some(rule) = split_rule {
let m = merge_split_args(&arg_values, rule.param_index, rule.char_counts.len());
let len = m.len();
(MergedArgs::Owned(m), len)
} else {
(MergedArgs::Borrowed(&arg_values), arg_values.len())
};
let merged_idx_of = |orig_i: usize| -> usize {
if let Some(rule) = split_rule {
let split_count = rule.char_counts.len();
let split_idx = rule.param_index;
if orig_i < split_idx {
orig_i
} else if orig_i < split_idx + split_count {
split_idx
} else {
orig_i - (split_count - 1)
}
} else {
orig_i
}
};
let last_value = args
.iter()
.enumerate()
.rev()
.find(|(_, a)| a.presence == Presence::Value)
.map(|(i, _)| merged_idx_of(i));
let trim_to = match last_value {
Some(idx) => (idx + 1).min(merged_len),
None => return, };
for i in 0..trim_to {
if i > 0 {
out.push_str(joiner);
}
out.push_str(merged.get(i));
}
}
enum MergedArgs<'a> {
Borrowed(&'a [&'a str]),
Owned(Vec<String>),
}
impl MergedArgs<'_> {
fn get(&self, i: usize) -> &str {
match self {
MergedArgs::Borrowed(s) => s[i],
MergedArgs::Owned(v) => &v[i],
}
}
}
fn merge_split_args(values: &[&str], param_index: usize, split_count: usize) -> Vec<String> {
let mut result = Vec::with_capacity(values.len().saturating_sub(split_count - 1));
for (i, val) in values.iter().enumerate() {
if i == param_index {
let end = (param_index + split_count).min(values.len());
result.push(values[param_index..end].concat());
} else if i > param_index && i < param_index + split_count {
continue; } else {
result.push(val.to_string());
}
}
result
}
fn push_indent(out: &mut String, config: &EmitConfig, in_label: bool, in_field: bool) {
match config.indent {
Indent::None => {}
Indent::Label => {
if in_label {
out.push_str(" ");
}
}
Indent::Field => {
if in_label {
out.push_str(" ");
}
if in_field {
out.push_str(" ");
}
}
}
}
fn trim_trailing_newline(out: &mut String) {
if out.ends_with('\n') {
out.truncate(out.len() - 1);
}
}
fn remap_prefix<'a>(code: &'a str, prefix: char) -> Cow<'a, str> {
if prefix == '^' {
return Cow::Borrowed(code);
}
if let Some(rest) = code.strip_prefix('^') {
let mut result = String::with_capacity(code.len());
result.push(prefix);
result.push_str(rest);
Cow::Owned(result)
} else {
Cow::Borrowed(code)
}
}
pub fn strip_spans(ast: &Ast) -> Ast {
let sentinel = Span::new(0, 0);
Ast {
labels: ast
.labels
.iter()
.map(|label| Label {
nodes: label
.nodes
.iter()
.map(|node| match node {
Node::Command { code, args, .. } => Node::Command {
code: code.clone(),
args: args.clone(),
span: sentinel,
},
Node::FieldData {
content,
hex_escaped,
..
} => Node::FieldData {
content: content.clone(),
hex_escaped: *hex_escaped,
span: sentinel,
},
Node::RawData { command, data, .. } => Node::RawData {
command: command.clone(),
data: data.clone(),
span: sentinel,
},
Node::Trivia { text, .. } => Node::Trivia {
text: text.clone(),
span: sentinel,
},
})
.collect(),
})
.collect(),
}
}