use crate::errors::{Diagnostic, GelError, Result, Severity};
use crate::exec::out::{ActionExecutor, FlatNode, FlatTree, OutputTree, RuntimeAction};
use crate::parser::ast::{
Expression, FunctionCall, GelDocument, MatchFieldList, MatchStatement, SkipStatement, Statement, WhenStatement,
};
use regex::Regex;
use smallvec::SmallVec;
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::Arc;
#[cfg(feature = "profiling")]
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(feature = "profiling")]
macro_rules! prof_inc {
($counter:ident) => {
$counter.fetch_add(1, Ordering::Relaxed);
};
}
#[cfg(not(feature = "profiling"))]
macro_rules! prof_inc {
($counter:ident) => {};
}
#[cfg(feature = "profiling")]
pub static EVAL_FIELD_LIST_CALLS: AtomicU64 = AtomicU64::new(0);
#[cfg(feature = "profiling")]
pub static EVAL_FIELD_LIST_FAST: AtomicU64 = AtomicU64::new(0);
#[cfg(feature = "profiling")]
pub static EVAL_FIELD_LIST_FALLBACK: AtomicU64 = AtomicU64::new(0);
#[cfg(feature = "profiling")]
pub static EVAL_FIELD_LIST_PREFIX_REJECT: AtomicU64 = AtomicU64::new(0);
#[cfg(feature = "profiling")]
pub static REGEX_FIND_HIT: AtomicU64 = AtomicU64::new(0);
#[cfg(feature = "profiling")]
pub static REGEX_FIND_MISS: AtomicU64 = AtomicU64::new(0);
#[cfg(feature = "profiling")]
pub static EVAL_SKIP_CALLS: AtomicU64 = AtomicU64::new(0);
#[cfg(feature = "profiling")]
pub static EVAL_SKIP_FAST: AtomicU64 = AtomicU64::new(0);
#[cfg(feature = "profiling")]
pub static EVAL_EXPR_REGEX: AtomicU64 = AtomicU64::new(0);
#[cfg(feature = "profiling")]
pub static GRAMMAR_LOOP_ITERS: AtomicU64 = AtomicU64::new(0);
type CaptureVec<'a> = SmallVec<[Cow<'a, str>; 8]>;
type NameVec = SmallVec<[Option<Arc<str>>; 8]>;
pub mod arena;
pub mod out;
pub mod streaming;
pub(crate) fn collect_inherited_statements(doc: &GelDocument, name: &str) -> Vec<Statement> {
let mut visited = std::collections::HashSet::new();
collect_inherited_inner(doc, name, &mut visited)
}
fn collect_inherited_inner(
doc: &GelDocument,
name: &str,
visited: &mut std::collections::HashSet<String>,
) -> Vec<Statement> {
let grammar = match doc.grammars.get(name) {
Some(g) => g,
None => return Vec::new(),
};
if !visited.insert(name.to_string()) {
return Vec::new();
}
let mut stmts = if let Some(parent) = &grammar.inherit {
collect_inherited_inner(doc, parent, visited)
} else {
Vec::new()
};
stmts.extend(grammar.statements.clone());
stmts
}
#[derive(Debug, Clone)]
pub struct ExecutionResult {
pub consumed: usize,
pub actions: Vec<FunctionCall>,
pub traces: Vec<String>,
pub diagnostics: Vec<Diagnostic>, pub capture_history: Vec<Vec<String>>,
pub capture_names_history: Vec<Vec<Option<Arc<str>>>>,
pub output: OutputTree,
pub flat: Option<FlatTree>,
pub error: Option<String>,
}
impl ExecutionResult {
#[allow(dead_code)]
fn new() -> Self {
Self::with_capacity_hint(0)
}
fn with_capacity_hint(input_bytes: usize) -> Self {
Self {
consumed: 0,
actions: Vec::new(),
traces: Vec::new(),
diagnostics: Vec::new(),
capture_history: Vec::new(),
capture_names_history: Vec::new(),
output: OutputTree::with_capacity_hint(input_bytes),
flat: None,
error: None,
}
}
fn trace(&mut self, severity: Severity, msg: String) {
self.traces.push(msg.clone());
self.diagnostics.push(Diagnostic {
severity,
message: msg,
span: None,
});
}
}
#[derive(Debug)]
pub struct Context<'a> {
pub doc: &'a GelDocument,
pub input: &'a str,
pub pos: usize,
pub captures: Vec<String>,
}
impl<'a> Context<'a> {
pub fn new(doc: &'a GelDocument, input: &'a str) -> Self {
Self {
doc,
input,
pos: 0,
captures: Vec::new(),
}
}
fn remaining(&self) -> &'a str {
&self.input[self.pos..]
}
fn advance(&mut self, n: usize) {
self.pos += n;
}
}
#[derive(Debug, Clone)]
pub(crate) struct TriggerAction {
pub path: String,
pub value: Option<String>,
}
pub struct Runner<'a> {
ctx: Context<'a>,
trig_before: Vec<(Regex, TriggerAction)>,
trig_after: Vec<(Regex, TriggerAction)>,
trig_on_add: Vec<(Regex, TriggerAction)>,
trig_on_leave: Vec<(Regex, TriggerAction)>,
trig_before_persist: Vec<(Regex, TriggerAction)>,
trig_after_persist: Vec<(Regex, TriggerAction)>,
trig_on_add_persist: Vec<(Regex, TriggerAction)>,
trig_on_leave_persist: Vec<(Regex, TriggerAction)>,
last_match_text: Cow<'a, str>,
last_captures: CaptureVec<'a>,
last_capture_names: NameVec,
stmt_cache: HashMap<String, Arc<Vec<Statement>>>,
}
impl<'a> Runner<'a> {
pub fn new(doc: &'a GelDocument, input: &'a str) -> Self {
let mut stmt_cache: HashMap<String, Arc<Vec<Statement>>> = HashMap::new();
for name in doc.grammars.keys().cloned().collect::<Vec<_>>() {
let stmts = collect_inherited_statements(doc, &name);
stmt_cache.insert(name, Arc::new(stmts));
}
Self {
ctx: Context::new(doc, input),
trig_before: Vec::new(),
trig_after: Vec::new(),
trig_on_add: Vec::new(),
trig_on_leave: Vec::new(),
trig_before_persist: Vec::new(),
trig_after_persist: Vec::new(),
trig_on_add_persist: Vec::new(),
trig_on_leave_persist: Vec::new(),
last_match_text: Cow::Borrowed(""),
last_captures: CaptureVec::new(),
last_capture_names: NameVec::new(),
stmt_cache,
}
}
pub fn run_grammar(mut self, name: &str) -> Result<ExecutionResult> {
if !self.ctx.doc.grammars.contains_key(name) {
return Err(GelError::runtime(format!("Grammar not found: {}", name), None));
}
let statements = self.stmt_cache.get(name).map(Arc::clone).unwrap_or_default();
let mut result = ExecutionResult::with_capacity_hint(self.ctx.input.len());
loop {
let start = self.ctx.pos;
for stmt in statements.iter() {
match stmt {
Statement::Match(m) => {
let mut scope: CaptureVec<'a> = CaptureVec::new();
let mut name_scope: NameVec = NameVec::new();
if let Some((len, acts)) = self.eval_match(m, &mut scope, &mut name_scope)? {
{
let match_text = &self.ctx.input[self.ctx.pos..self.ctx.pos + len];
let pos_before = self.ctx.pos;
if len > 0 {
self.ctx.advance(len);
}
self.last_match_text = Cow::Borrowed(match_text);
let needs_sub = Self::actions_need_substitution(acts);
let substituted_storage;
let actions_to_run: &[FunctionCall] = if needs_sub {
substituted_storage = self.substitute_actions(acts, &scope, &name_scope);
&substituted_storage
} else {
acts
};
self.last_captures = std::mem::take(&mut scope);
self.last_capture_names = std::mem::take(&mut name_scope);
self.fire_triggers("before", &mut result.output);
let (return_levels, did_next, fail_error, auto_leaves) = self.execute_runtime_actions(
actions_to_run,
&mut result.output,
&mut result.traces,
);
let total_consumed = self.ctx.pos - pos_before;
result.consumed += total_consumed;
for _ in 0..auto_leaves {
self.fire_triggers("on_leave", &mut result.output);
result.output.leave();
}
if let Some(err) = fail_error {
result.error = Some(err);
}
if return_levels > 0 {
return Ok(result);
}
if did_next {
break;
}
result.actions.extend(actions_to_run.iter().cloned());
if !self.last_captures.is_empty() {
result
.capture_history
.push(self.last_captures.iter().map(|c| c.to_string()).collect());
result.capture_names_history.push(self.last_capture_names.to_vec());
}
result.trace(Severity::Info, format!("match consumed {} chars", total_consumed));
self.fire_triggers("after", &mut result.output);
break; }
}
}
Statement::When(w) => {
let mut scope: CaptureVec<'a> = CaptureVec::new();
let mut name_scope: NameVec = NameVec::new();
if self.eval_when(w, &mut scope, &mut name_scope)? {
let needs_sub = Self::actions_need_substitution(&w.actions);
let substituted_storage;
let actions_to_run: &[FunctionCall] = if needs_sub {
substituted_storage = self.substitute_actions(&w.actions, &scope, &name_scope);
&substituted_storage
} else {
&w.actions
};
self.last_match_text = Cow::Borrowed("");
self.last_captures = scope;
self.last_capture_names = name_scope;
self.fire_triggers("before", &mut result.output);
let (return_levels, did_next, fail_error, auto_leaves) =
self.execute_runtime_actions(actions_to_run, &mut result.output, &mut result.traces);
for _ in 0..auto_leaves {
self.fire_triggers("on_leave", &mut result.output);
result.output.leave();
}
if let Some(err) = fail_error {
result.error = Some(err);
}
if return_levels > 0 {
return Ok(result);
}
if did_next {
break;
}
result.trace(Severity::Info, "when triggered".to_string());
self.fire_triggers("after", &mut result.output);
break; }
}
Statement::Skip(s) => {
if let Some(len) = self.eval_skip(s)? {
if len > 0 {
self.ctx.advance(len);
result.consumed += len;
break;
}
}
}
Statement::Action(a) => {
let pos_before_action = self.ctx.pos;
if !a.name.contains('.') && self.stmt_cache.contains_key(&*a.name) {
let (sub_consumed, remaining_levels) =
self.run_inline_grammar(&a.name, &mut result.output, &mut result.traces);
if remaining_levels > 0 {
result.consumed += self.ctx.pos - pos_before_action;
return Ok(result);
}
if sub_consumed > 0 {
result.consumed += sub_consumed;
break; }
} else {
let substituted = self.substitute_action(a, &[], &[]);
let (return_levels, did_next, fail_error, auto_leaves) = self.execute_runtime_actions(
std::slice::from_ref(&substituted),
&mut result.output,
&mut result.traces,
);
for _ in 0..auto_leaves {
self.fire_triggers("on_leave", &mut result.output);
result.output.leave();
}
if let Some(err) = fail_error {
result.error = Some(err);
}
if return_levels > 0 {
return Ok(result);
}
if did_next {
break;
}
let action_consumed = self.ctx.pos - pos_before_action;
if action_consumed > 0 {
result.consumed += action_consumed;
break; }
}
}
}
}
if self.ctx.pos >= self.ctx.input.len() {
break;
}
if self.ctx.pos == start {
result.trace(
Severity::Warning,
format!("no match found in grammar {}, pos {}", name, self.ctx.pos),
);
break;
}
}
Ok(result)
}
fn eval_match<'m>(
&mut self,
m: &'m MatchStatement,
captures_out: &mut CaptureVec<'a>,
names_out: &mut NameVec,
) -> Result<Option<(usize, &'m [FunctionCall])>> {
for alt in &m.match_list.alternatives {
if let Some((len, groups, names)) = self.eval_field_list(alt, m.case_insensitive)? {
captures_out.extend(groups);
names_out.extend(names);
return Ok(Some((len, &m.actions)));
}
}
Ok(None)
}
fn eval_when(
&mut self,
w: &WhenStatement,
captures_out: &mut CaptureVec<'a>,
names_out: &mut NameVec,
) -> Result<bool> {
for alt in &w.match_list.alternatives {
if let Some((_, groups, names)) = self.eval_field_list(alt, false)? {
captures_out.extend(groups);
names_out.extend(names);
return Ok(true);
}
}
Ok(false)
}
fn eval_skip(&mut self, s: &SkipStatement) -> Result<Option<usize>> {
prof_inc!(EVAL_SKIP_CALLS);
let rem = self.ctx.remaining();
let resolved = match &s.pattern {
Expression::Regex(r) => Some(r.as_str()),
Expression::Variable(v) => match self.resolve_variable(v) {
Some(Expression::Regex(r)) => Some(r.as_str()),
_ => None,
},
_ => None,
};
if let Some(pattern) = resolved {
let kind = self
.ctx
.doc
.pattern_indices
.get(pattern)
.and_then(|&idx| self.ctx.doc.fast_path_kinds.get(idx).copied())
.unwrap_or(crate::parser::ast::FastPathKind::None);
if kind != crate::parser::ast::FastPathKind::None {
if let Some(n) = Self::try_fast_skip_kind(kind, rem) {
prof_inc!(EVAL_SKIP_FAST);
return Ok(Some(n));
}
return Ok(None);
}
}
self.eval_expression(&s.pattern, rem, false, &[], &[])
.map(|o| o.map(|(n, _)| n))
}
pub(crate) fn try_fast_skip_kind(kind: crate::parser::ast::FastPathKind, rem: &str) -> Option<usize> {
use crate::parser::ast::FastPathKind;
let bytes = rem.as_bytes();
if bytes.is_empty() {
return None;
}
match kind {
FastPathKind::SkipToNewline => memchr::memchr(b'\n', bytes).map(|pos| pos + 1),
FastPathKind::SkipToNewlinePlus => {
if let Some(pos) = memchr::memchr(b'\n', bytes) {
if pos >= 1 {
Some(pos + 1)
} else {
None
}
} else {
None
}
}
FastPathKind::SkipToCrLf => memchr::memchr2(b'\r', b'\n', bytes).map(|pos| pos + 1),
FastPathKind::SkipToCrLfPlus => {
if let Some(pos) = memchr::memchr2(b'\r', b'\n', bytes) {
if pos >= 1 {
Some(pos + 1)
} else {
None
}
} else {
None
}
}
FastPathKind::CommentBangHash => {
if bytes[0] == b'!' || bytes[0] == b'#' {
if let Some(pos) = memchr::memchr2(b'\r', b'\n', &bytes[1..]) {
return Some(1 + pos + 1);
}
}
None
}
FastPathKind::CommentHashPlus => {
if bytes[0] == b'#' {
if let Some(pos) = memchr::memchr2(b'\r', b'\n', &bytes[1..]) {
let line_end = 1 + pos;
let trailing = bytes[line_end..]
.iter()
.take_while(|&&b| b == b'\r' || b == b'\n')
.count();
if trailing > 0 {
return Some(line_end + trailing);
}
}
}
None
}
FastPathKind::DotStarNewline => memchr::memchr(b'\n', bytes).map(|pos| pos + 1),
FastPathKind::Whitespace => {
let count = bytes.iter().take_while(|b| b.is_ascii_whitespace()).count();
if count > 0 {
Some(count)
} else {
None
}
}
FastPathKind::NonWhitespace => {
let count = bytes.iter().take_while(|b| !b.is_ascii_whitespace()).count();
if count > 0 {
Some(count)
} else {
None
}
}
FastPathKind::Digits => {
let count = bytes.iter().take_while(|b| b.is_ascii_digit()).count();
if count > 0 {
Some(count)
} else {
None
}
}
FastPathKind::WordChars => {
let count = bytes
.iter()
.take_while(|b| b.is_ascii_alphanumeric() || **b == b'_')
.count();
if count > 0 {
Some(count)
} else {
None
}
}
FastPathKind::HorizWhitespace => {
let count = bytes.iter().take_while(|&&b| b == b'\t' || b == b' ').count();
if count > 0 {
Some(count)
} else {
None
}
}
FastPathKind::CrLfPlus => {
let count = bytes.iter().take_while(|&&b| b == b'\r' || b == b'\n').count();
if count > 0 {
Some(count)
} else {
None
}
}
FastPathKind::CrLf => {
if bytes[0] == b'\r' || bytes[0] == b'\n' {
Some(1)
} else {
None
}
}
FastPathKind::NonCrLfPlus => {
if let Some(pos) = memchr::memchr2(b'\r', b'\n', bytes) {
if pos > 0 {
Some(pos)
} else {
None
}
} else if !bytes.is_empty() {
Some(bytes.len())
} else {
None
}
}
FastPathKind::NonCrLfStar => {
if let Some(pos) = memchr::memchr2(b'\r', b'\n', bytes) {
Some(pos)
} else {
Some(bytes.len())
}
}
FastPathKind::NonCrLf => {
if bytes[0] != b'\r' && bytes[0] != b'\n' {
Some(1)
} else {
None
}
}
FastPathKind::SpacesNewlines => {
let spaces = bytes.iter().take_while(|&&b| b == b' ').count();
let newlines = bytes[spaces..]
.iter()
.take_while(|&&b| b == b'\r' || b == b'\n')
.count();
if newlines > 0 {
Some(spaces + newlines)
} else {
None
}
}
FastPathKind::OptCrNl => {
if bytes[0] == b'\n' {
Some(1)
} else if bytes[0] == b'\r' && bytes.len() > 1 && bytes[1] == b'\n' {
Some(2)
} else {
None
}
}
FastPathKind::OptCrNlPlus => {
let mut pos = 0;
let mut count = 0usize;
while pos < bytes.len() {
if bytes[pos] == b'\n' {
pos += 1;
count += 1;
} else if bytes[pos] == b'\r' && pos + 1 < bytes.len() && bytes[pos + 1] == b'\n' {
pos += 2;
count += 1;
} else {
break;
}
}
if count > 0 {
Some(pos)
} else {
None
}
}
FastPathKind::SkipToOptCrNl => {
memchr::memchr(b'\n', bytes).map(|nl_pos| nl_pos + 1)
}
FastPathKind::SkipToOptCrNlPlus => {
if let Some(nl_pos) = memchr::memchr(b'\n', bytes) {
let content_end = if nl_pos > 0 && bytes[nl_pos - 1] == b'\r' {
nl_pos - 1
} else {
nl_pos
};
if content_end >= 1 {
Some(nl_pos + 1)
} else {
None
}
} else {
None
}
}
FastPathKind::CommentBangHashOpt => {
if bytes[0] == b'!' || bytes[0] == b'#' {
if let Some(nl_pos) = memchr::memchr(b'\n', &bytes[1..]) {
return Some(1 + nl_pos + 1);
}
}
None
}
FastPathKind::CommentHashPlusOpt => {
if bytes[0] == b'#' {
if let Some(nl_pos) = memchr::memchr(b'\n', &bytes[1..]) {
let mut pos = 1 + nl_pos + 1; while pos < bytes.len() {
if bytes[pos] == b'\n' {
pos += 1;
} else if bytes[pos] == b'\r' && pos + 1 < bytes.len() && bytes[pos + 1] == b'\n' {
pos += 2;
} else {
break;
}
}
return Some(pos);
}
}
None
}
FastPathKind::SpacesOptCrNlPlus => {
let spaces = bytes.iter().take_while(|&&b| b == b' ').count();
let rest = &bytes[spaces..];
let mut pos = 0;
let mut count = 0usize;
while pos < rest.len() {
if rest[pos] == b'\n' {
pos += 1;
count += 1;
} else if rest[pos] == b'\r' && pos + 1 < rest.len() && rest[pos + 1] == b'\n' {
pos += 2;
count += 1;
} else {
break;
}
}
if count > 0 {
Some(spaces + pos)
} else {
None
}
}
FastPathKind::Newline => {
if bytes[0] == b'\n' {
Some(1)
} else {
None
}
}
FastPathKind::Dot => Some(1),
FastPathKind::None => None,
}
}
pub(crate) fn try_fast_skip(pattern: &str, rem: &str) -> Option<usize> {
let kind = crate::parser::ast::classify_fast_path(pattern);
Self::try_fast_skip_kind(kind, rem)
}
#[allow(clippy::type_complexity)]
fn eval_field_list(
&mut self,
list: &MatchFieldList,
case_insensitive: bool,
) -> Result<Option<(usize, CaptureVec<'a>, NameVec)>> {
prof_inc!(EVAL_FIELD_LIST_CALLS);
if let Some(prefix) = &list.literal_prefix {
let rem = self.ctx.remaining();
let ci = case_insensitive;
let reject = if ci {
!rem.get(..prefix.len())
.map(|s| s.eq_ignore_ascii_case(prefix))
.unwrap_or(false)
} else {
!rem.starts_with(prefix.as_str())
};
if reject {
prof_inc!(EVAL_FIELD_LIST_PREFIX_REJECT);
return Ok(None);
}
}
if let Some(combined) = &list.compiled_regex {
prof_inc!(EVAL_FIELD_LIST_FAST);
let rem = self.ctx.remaining();
let window = &rem[..rem.len().min(1000)];
if let Some(m) = combined.find(window) {
if m.start() != 0 {
prof_inc!(REGEX_FIND_MISS);
return Ok(None);
}
prof_inc!(REGEX_FIND_HIT);
let consumed = m.end();
if combined.captures_len() > 1 {
if let Some(mat) = combined.captures(window) {
let mut all_captures: CaptureVec<'a> = CaptureVec::new();
let mut all_names: NameVec = NameVec::new();
for gi in 1..mat.len() {
let text: Cow<'a, str> = mat
.get(gi)
.map(|g| Cow::Borrowed(&rem[g.start()..g.end()]))
.unwrap_or(Cow::Borrowed(""));
all_captures.push(text);
all_names.push(None);
}
return Ok(Some((consumed, all_captures, all_names)));
}
} else {
let all_captures: CaptureVec<'a> = smallvec::smallvec![Cow::Borrowed(&rem[..consumed])];
let all_names: NameVec = smallvec::smallvec![None];
return Ok(Some((consumed, all_captures, all_names)));
}
}
prof_inc!(REGEX_FIND_MISS);
return Ok(None);
}
prof_inc!(EVAL_FIELD_LIST_FALLBACK);
let mut offset = 0usize;
let mut rem: &'a str = self.ctx.remaining();
let mut all_captures: CaptureVec<'a> = CaptureVec::new();
let mut all_names: NameVec = NameVec::new();
for expr in &list.expressions {
match self.eval_expression_with_groups(expr, rem, case_insensitive, &all_captures, &all_names)? {
Some((consumed, groups)) => {
if consumed > rem.len() {
return Ok(None);
}
offset += consumed;
rem = &rem[consumed..];
for (text, name) in groups {
all_captures.push(text);
all_names.push(name);
}
}
None => return Ok(None),
}
}
Ok(Some((offset, all_captures, all_names)))
}
#[allow(clippy::type_complexity)]
fn eval_expression_with_groups(
&mut self,
expr: &Expression,
rem: &'a str,
case_insensitive: bool,
captures_so_far: &[Cow<'a, str>],
names_so_far: &[Option<Arc<str>>],
) -> Result<Option<(usize, Vec<(Cow<'a, str>, Option<Arc<str>>)>)>> {
match expr {
Expression::Regex(r) => {
let idx = match self.ctx.doc.pattern_indices.get(r.as_str()) {
Some(&i) => i,
None => return Ok(None), };
let slot = idx * 2 + case_insensitive as usize;
let compiled = match self.ctx.doc.regex_cache.get(slot).and_then(|o| o.as_ref()) {
Some(rx) => rx,
None => return Ok(None),
};
let window = &rem[..rem.len().min(1000)];
prof_inc!(EVAL_EXPR_REGEX);
if let Some(m) = compiled.find(window) {
if m.start() == 0 {
let consumed = m.end();
let num_groups = compiled.captures_len();
let mut groups = Vec::with_capacity(num_groups);
if num_groups > 1 {
if let Some(mat) = compiled.captures(window) {
groups.push((Cow::Borrowed(&rem[..mat.get(0).unwrap().end()]), None));
let group_names: Vec<Option<&str>> = compiled.capture_names().collect();
for gi in 1..mat.len() {
let text: Cow<'a, str> = mat
.get(gi)
.map(|g| Cow::Borrowed(&rem[g.start()..g.end()]))
.unwrap_or(Cow::Borrowed(""));
let name = group_names.get(gi).and_then(|n| *n).map(Arc::from);
groups.push((text, name));
}
}
} else {
groups.push((Cow::Borrowed(&rem[..consumed]), None));
}
return Ok(Some((consumed, groups)));
}
}
Ok(None)
}
Expression::String(s) => {
let matched = if case_insensitive {
let end = rem.len().min(s.len());
if rem.get(..end).map(|sl| sl.eq_ignore_ascii_case(s)).unwrap_or(false) {
Some(s.len())
} else {
None
}
} else if rem.starts_with(s.as_str()) {
Some(s.len())
} else {
None
};
Ok(matched.map(|len| (len, vec![(Cow::Owned(s.clone()), None)])))
}
Expression::Number(n) => {
let s = n.to_string();
if rem.starts_with(&s) {
Ok(Some((s.len(), vec![(Cow::Owned(s), None)])))
} else {
Ok(None)
}
}
Expression::Variable(v) => {
if let Some(resolved) = self.resolve_variable(v) {
let cloned = resolved.clone();
self.eval_expression_with_groups(&cloned, rem, case_insensitive, captures_so_far, names_so_far)
} else {
Ok(None)
}
}
Expression::Capture(idx) => {
if let Some(val) = captures_so_far.get(*idx) {
let len = val.len();
if rem.len() >= len {
let slice = &rem[..len];
if (case_insensitive && slice.eq_ignore_ascii_case(val)) || slice == val.as_ref() {
return Ok(Some((len, vec![(val.clone(), None)])));
}
}
}
Ok(None)
}
Expression::CaptureName(name) => {
if let Some((i, _)) = names_so_far
.iter()
.enumerate()
.find(|(_, n)| n.as_deref() == Some(name.as_str()))
{
if let Some(val) = captures_so_far.get(i) {
let len = val.len();
if rem.len() >= len {
let slice = &rem[..len];
if (case_insensitive && slice.eq_ignore_ascii_case(val)) || slice == val.as_ref() {
return Ok(Some((len, vec![(val.clone(), None)])));
}
}
}
}
Ok(None)
}
}
}
fn resolve_variable(&self, name: &str) -> Option<&Expression> {
self.ctx.doc.defines.get(name)
}
fn eval_expression(
&mut self,
expr: &Expression,
rem: &str,
case_insensitive: bool,
groups_so_far: &[String],
names_so_far: &[Option<Arc<str>>],
) -> Result<Option<(usize, String)>> {
match expr {
Expression::String(s) => {
let matched = if case_insensitive {
let end = rem.len().min(s.len());
if rem.get(..end).map(|sl| sl.eq_ignore_ascii_case(s)).unwrap_or(false) {
Some(s.len())
} else {
None
}
} else if rem.starts_with(s.as_str()) {
Some(s.len())
} else {
None
};
Ok(matched.map(|len| (len, s.clone())))
}
Expression::Regex(r) => {
let idx = match self.ctx.doc.pattern_indices.get(r.as_str()) {
Some(&i) => i,
None => return Ok(None),
};
let slot = idx * 2 + case_insensitive as usize;
let compiled = match self.ctx.doc.regex_cache.get(slot).and_then(|o| o.as_ref()) {
Some(rx) => rx,
None => return Ok(None),
};
if let Some(m) = compiled.find(&rem[..rem.len().min(1000)]) {
if m.start() == 0 {
return Ok(Some((m.end(), rem[..m.end()].to_string())));
}
}
Ok(None)
}
Expression::Number(n) => {
let s = n.to_string();
if rem.starts_with(&s) {
Ok(Some((s.len(), s)))
} else {
Ok(None)
}
}
Expression::Variable(v) => {
if let Some(resolved) = self.resolve_variable(v) {
let cloned = resolved.clone();
self.eval_expression(&cloned, rem, case_insensitive, groups_so_far, &[])
} else {
Ok(None)
}
}
Expression::Capture(idx) => {
if let Some(val) = groups_so_far.get(*idx) {
let len = val.len();
if rem.len() >= len {
let slice = &rem[..len];
if (case_insensitive && slice.eq_ignore_ascii_case(val)) || slice == val {
return Ok(Some((len, val.clone())));
}
}
}
Ok(None)
}
Expression::CaptureName(name) => {
if let Some((i, _)) = names_so_far
.iter()
.enumerate()
.find(|(_, n)| n.as_deref() == Some(name.as_str()))
{
if let Some(val) = groups_so_far.get(i) {
let len = val.len();
if rem.len() >= len {
let slice = &rem[..len];
if (case_insensitive && slice.eq_ignore_ascii_case(val)) || slice == val {
return Ok(Some((len, val.clone())));
}
}
}
}
Ok(None)
}
}
}
fn substitute_actions(
&self,
actions: &[FunctionCall],
scope: &[Cow<'a, str>],
name_scope: &[Option<Arc<str>>],
) -> Vec<FunctionCall> {
actions
.iter()
.map(|a| self.substitute_action(a, scope, name_scope))
.collect()
}
#[inline]
fn actions_need_substitution(actions: &[FunctionCall]) -> bool {
actions.iter().any(|a| {
a.args
.iter()
.any(|arg| matches!(arg, Expression::Capture(_) | Expression::CaptureName(_)))
})
}
fn substitute_action(
&self,
action: &FunctionCall,
scope: &[Cow<'a, str>],
name_scope: &[Option<Arc<str>>],
) -> FunctionCall {
let mut new_args = Vec::with_capacity(action.args.len());
for arg in &action.args {
let replaced = match arg {
Expression::Capture(i) => scope
.get(*i)
.map(|v| Expression::String(crate::exec::out::percent_encode(v)))
.unwrap_or(Expression::String(String::new())),
Expression::CaptureName(n) => {
if let Some((i, _)) = name_scope.iter().enumerate().find(|(_, s)| s.as_deref() == Some(&**n)) {
scope
.get(i)
.map(|v| Expression::String(crate::exec::out::percent_encode(v)))
.unwrap_or(Expression::String(String::new()))
} else {
Expression::String(String::new())
}
}
_ => arg.clone(),
};
new_args.push(replaced);
}
FunctionCall {
name: action.name.clone(),
args: new_args,
}
}
fn execute_runtime_actions(
&mut self,
actions: &[FunctionCall],
tree: &mut OutputTree,
traces: &mut Vec<String>,
) -> (i32, bool, Option<String>, usize) {
let mut skip_rest = false; let mut return_levels: i32 = 0; let mut do_next = false; let mut fail_error: Option<String> = None;
let mut last_action_name: Option<&str> = None;
let mut pending_auto_leaves: usize = 0; for act in actions {
if skip_rest || return_levels > 0 || do_next {
break;
}
let current_name: &str = &act.name;
if !act.name.contains('.') && self.ctx.doc.grammars.contains_key(&*act.name) {
let (sub_consumed, _remaining_levels) = self.run_inline_grammar(&act.name, tree, traces);
if sub_consumed > 0 {
traces.push(format!("subgrammar {} consumed {} chars", act.name, sub_consumed));
}
if let Some(prev) = &last_action_name {
if *prev == "out.open" {
tree.leave();
pending_auto_leaves = pending_auto_leaves.saturating_sub(1);
}
}
if sub_consumed > 0 {
break;
}
last_action_name = Some(current_name);
continue;
}
if &*act.name == "out.create" {
if let Some(path_expr) = act.args.first() {
let path = self.expr_to_cow(path_expr);
let value = act.args.get(1).map(|v| self.expr_to_string(v));
let value_ref = value.as_deref();
tree.exec(RuntimeAction::OutCreate {
path: &path,
value: value_ref,
});
tree.exec(RuntimeAction::OutEnter { path: &path });
self.fire_triggers("on_add", tree);
tree.exec(RuntimeAction::OutLeave);
}
} else if &*act.name == "out.add" {
if let Some(path_expr) = act.args.first() {
let path = self.expr_to_cow(path_expr);
let value = act.args.get(1).map(|v| self.expr_to_string(v));
let value_ref = value.as_deref();
tree.exec(RuntimeAction::OutAdd {
path: &path,
value: value_ref,
});
tree.exec(RuntimeAction::OutEnter { path: &path });
self.fire_triggers("on_add", tree);
tree.exec(RuntimeAction::OutLeave);
}
} else if &*act.name == "out.replace" {
if let Some(path_expr) = act.args.first() {
let path = self.expr_to_cow(path_expr);
let value = act.args.get(1).map(|v| self.expr_to_string(v));
let value_ref = value.as_deref();
tree.exec(RuntimeAction::OutReplace {
path: &path,
value: value_ref,
});
tree.exec(RuntimeAction::OutEnter { path: &path });
self.fire_triggers("on_add", tree);
tree.exec(RuntimeAction::OutLeave);
}
} else if &*act.name == "out.add_attribute" {
if let (Some(path_expr), Some(name_expr), Some(val_expr)) =
(act.args.first(), act.args.get(1), act.args.get(2))
{
let path = self.expr_to_cow(path_expr);
let name = self.expr_to_cow(name_expr);
let value = self.expr_to_cow(val_expr);
tree.exec(RuntimeAction::OutAddAttribute {
path: &path,
name: &name,
value: &value,
});
tree.exec(RuntimeAction::OutEnter { path: &path });
self.fire_triggers("on_add", tree);
tree.exec(RuntimeAction::OutLeave);
}
} else if &*act.name == "out.set_root_name" {
if let Some(name_expr) = act.args.first() {
let name = self.expr_to_cow(name_expr);
tree.exec(RuntimeAction::OutSetRootName { name: &name });
}
} else if &*act.name == "out.open" {
if let Some(path_expr) = act.args.first() {
let path = self.expr_to_cow(path_expr);
tree.exec(RuntimeAction::OutOpen { path: &path });
self.fire_triggers("on_add", tree);
pending_auto_leaves += 1;
}
} else if &*act.name == "out.enter" {
if let Some(path_expr) = act.args.first() {
let path = self.expr_to_cow(path_expr);
tree.exec(RuntimeAction::OutEnter { path: &path });
self.fire_triggers("on_add", tree);
pending_auto_leaves += 1;
}
} else if &*act.name == "out.leave" {
self.fire_triggers("on_leave", tree);
tree.exec(RuntimeAction::OutLeave);
pending_auto_leaves = pending_auto_leaves.saturating_sub(1);
} else if &*act.name == "do.skip" {
skip_rest = true;
continue;
} else if &*act.name == "do.next" {
do_next = true;
continue;
} else if &*act.name == "do.return" {
let levels = act
.args
.first()
.map(|e| match e {
Expression::Number(n) => *n as i32,
Expression::String(s) => s.parse::<i32>().unwrap_or(1),
_ => 1,
})
.unwrap_or(1);
return_levels = levels.max(1);
continue;
} else if &*act.name == "do.say" {
if let Some(msg_expr) = act.args.first() {
traces.push(format!("say: {}", self.expr_to_string(msg_expr)));
}
} else if &*act.name == "do.warn" {
if let Some(msg_expr) = act.args.first() {
traces.push(format!("warn: {}", self.expr_to_string(msg_expr)));
}
} else if &*act.name == "do.fail" {
self.ctx.pos = self.ctx.input.len();
let err_msg = act
.args
.first()
.map(|e| self.expr_to_string(e))
.unwrap_or_else(|| "fail".to_string());
traces.push("fail invoked".to_string());
traces.push(format!("fail: {}", err_msg));
fail_error = Some(err_msg);
return_levels = 1;
break;
} else if act.name.starts_with("out.enqueue_") {
let raw_opt = match act.args.first() {
Some(Expression::Regex(r)) => Some(r.clone()),
Some(Expression::Variable(v)) => {
if let Some(Expression::Regex(r)) = self.resolve_variable(v) {
Some(r.clone())
} else {
None
}
}
_ => None,
};
if let Some(raw) = raw_opt {
if let Ok(rx) = Regex::new(&format!("(?s){}", raw)) {
let path = act.args.get(1).map(|p| self.expr_to_string(p)).unwrap_or_default();
let value = act.args.get(2).map(|v| self.expr_to_string(v));
let trig = TriggerAction { path, value };
match &*act.name {
"out.enqueue_before" => self.trig_before.push((rx, trig)),
"out.enqueue_after" => self.trig_after.push((rx, trig)),
"out.enqueue_on_add" => self.trig_on_add.push((rx, trig)),
"out.enqueue_on_leave" => self.trig_on_leave.push((rx, trig)),
"out.enqueue_before_persist" => self.trig_before_persist.push((rx, trig)),
"out.enqueue_after_persist" => self.trig_after_persist.push((rx, trig)),
"out.enqueue_on_add_persist" => self.trig_on_add_persist.push((rx, trig)),
"out.enqueue_on_leave_persist" => self.trig_on_leave_persist.push((rx, trig)),
_ => {}
}
}
}
} else if &*act.name == "out.clear_queue" {
self.trig_before.clear();
self.trig_after.clear();
self.trig_on_add.clear();
}
last_action_name = Some(current_name);
}
(return_levels, do_next, fail_error, pending_auto_leaves)
}
fn run_inline_grammar(&mut self, name: &str, tree: &mut OutputTree, traces: &mut Vec<String>) -> (usize, i32) {
let statements = match self.stmt_cache.get(name).map(Arc::clone) {
Some(s) => s,
None => return (0, 0),
};
let start = self.ctx.pos;
let mut return_levels: i32 = 0;
loop {
let pos_before = self.ctx.pos;
prof_inc!(GRAMMAR_LOOP_ITERS);
for stmt in statements.iter() {
match stmt {
Statement::Match(m) => {
let mut scope: CaptureVec<'a> = CaptureVec::new();
let mut name_scope: NameVec = NameVec::new();
if let Some((len, acts)) = self.eval_match(m, &mut scope, &mut name_scope).ok().flatten() {
{
let match_text = &self.ctx.input[self.ctx.pos..self.ctx.pos + len];
if len > 0 {
self.ctx.advance(len);
}
self.last_match_text = Cow::Borrowed(match_text);
let needs_sub = Self::actions_need_substitution(acts);
let substituted_storage;
let actions_to_run: &[FunctionCall] = if needs_sub {
substituted_storage = self.substitute_actions(acts, &scope, &name_scope);
&substituted_storage
} else {
acts
};
self.last_captures = scope;
self.last_capture_names = name_scope;
self.fire_triggers("before", tree);
let (ret_lvl, next, _, auto_leaves) =
self.execute_runtime_actions(actions_to_run, tree, traces);
for _ in 0..auto_leaves {
self.fire_triggers("on_leave", tree);
tree.leave();
}
if ret_lvl > 0 {
return_levels = ret_lvl;
break;
}
if next {
break;
}
self.fire_triggers("after", tree);
break; }
}
}
Statement::When(w) => {
let mut scope: CaptureVec<'a> = CaptureVec::new();
let mut name_scope: NameVec = NameVec::new();
if self.eval_when(w, &mut scope, &mut name_scope).unwrap_or(false) {
let needs_sub = Self::actions_need_substitution(&w.actions);
let substituted_storage;
let actions_to_run: &[FunctionCall] = if needs_sub {
substituted_storage = self.substitute_actions(&w.actions, &scope, &name_scope);
&substituted_storage
} else {
&w.actions
};
self.last_match_text = Cow::Borrowed("");
self.last_captures = scope;
self.last_capture_names = name_scope;
self.fire_triggers("before", tree);
let (ret_lvl, next, _, auto_leaves) =
self.execute_runtime_actions(actions_to_run, tree, traces);
for _ in 0..auto_leaves {
self.fire_triggers("on_leave", tree);
tree.leave();
}
if ret_lvl > 0 {
return_levels = ret_lvl;
break;
}
if next {
break;
}
self.fire_triggers("after", tree);
break; }
}
Statement::Skip(s) => {
if let Some(len) = self.eval_skip(s).ok().flatten() {
if len > 0 {
self.ctx.advance(len);
break;
}
}
}
Statement::Action(a) => {
let pos_before_action = self.ctx.pos;
if !a.name.contains('.') && self.stmt_cache.contains_key(&*a.name) {
let (sub_consumed, remaining) = self.run_inline_grammar(&a.name, tree, traces);
if remaining > 0 {
return_levels = remaining;
break;
}
if sub_consumed > 0 {
break;
}
} else {
let substituted = self.substitute_action(a, &[], &[]);
let (ret_lvl, next, _, auto_leaves) =
self.execute_runtime_actions(std::slice::from_ref(&substituted), tree, traces);
for _ in 0..auto_leaves {
self.fire_triggers("on_leave", tree);
tree.leave();
}
if ret_lvl > 0 {
return_levels = ret_lvl;
break;
}
if next {
break;
}
if self.ctx.pos > pos_before_action {
break;
}
}
}
}
if return_levels > 0 {
break;
}
}
if return_levels > 0 {
break;
}
if self.ctx.pos == pos_before {
break;
}
}
let remaining = (return_levels - 1).max(0);
(self.ctx.pos - start, remaining)
}
fn fire_triggers(&mut self, kind: &str, tree: &mut OutputTree) {
let (has_single, has_persist) = match kind {
"before" => (!self.trig_before.is_empty(), !self.trig_before_persist.is_empty()),
"after" => (!self.trig_after.is_empty(), !self.trig_after_persist.is_empty()),
"on_add" => (!self.trig_on_add.is_empty(), !self.trig_on_add_persist.is_empty()),
"on_leave" => (!self.trig_on_leave.is_empty(), !self.trig_on_leave_persist.is_empty()),
_ => return,
};
if !has_single && !has_persist {
return;
}
let text: &str = &self.last_match_text;
let caps: Vec<&str> = self.last_captures.iter().map(|c| c.as_ref()).collect();
let _names = &self.last_capture_names;
fn fire_list(
captures: &[&str],
names: &[Option<Arc<str>>],
list: &mut Vec<(Regex, TriggerAction)>,
text: &str,
tree: &mut OutputTree,
) {
let mut indices = Vec::new();
for (i, (rx, _)) in list.iter().enumerate() {
if rx.is_match(text) {
indices.push(i);
}
}
for idx in indices.into_iter().rev() {
let (_, trig) = list.swap_remove(idx);
let path_interp = interpolate_local(&trig.path, captures, names);
let value_interp = trig.value.as_ref().map(|v| interpolate_local(v, captures, names));
let segs = crate::exec::out::parse_path(&path_interp);
tree.add_path(&segs, value_interp);
}
}
fn fire_persist(
captures: &[&str],
names: &[Option<Arc<str>>],
list: &[(Regex, TriggerAction)],
text: &str,
tree: &mut OutputTree,
) {
for (rx, trig) in list.iter() {
if rx.is_match(text) {
let path_interp = interpolate_local(&trig.path, captures, names);
let value_interp = trig.value.as_ref().map(|v| interpolate_local(v, captures, names));
let segs = crate::exec::out::parse_path(&path_interp);
tree.add_path(&segs, value_interp);
}
}
}
let caps = ∩︀
let names = &self.last_capture_names;
if kind == "before" {
fire_list(caps, names, &mut self.trig_before, text, tree);
let persist = self.trig_before_persist.clone();
fire_persist(caps, names, &persist, text, tree);
} else if kind == "after" {
fire_list(caps, names, &mut self.trig_after, text, tree);
let persist = self.trig_after_persist.clone();
fire_persist(caps, names, &persist, text, tree);
} else if kind == "on_add" {
fire_list(caps, names, &mut self.trig_on_add, text, tree);
let persist = self.trig_on_add_persist.clone();
fire_persist(caps, names, &persist, text, tree);
} else if kind == "on_leave" {
fire_list(caps, names, &mut self.trig_on_leave, text, tree);
let persist = self.trig_on_leave_persist.clone();
fire_persist(caps, names, &persist, text, tree);
}
}
}
fn interpolate_local(s: &str, captures: &[&str], names: &[Option<Arc<str>>]) -> String {
let mut out = String::new();
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' && chars.peek() == Some(&'$') {
out.push('$');
chars.next();
} else if c == '$' {
let mut token = String::new();
while let Some(&p) = chars.peek() {
if p.is_ascii_alphanumeric() || p == '_' {
token.push(p);
chars.next();
} else {
break;
}
}
if token.is_empty() {
out.push('$');
} else if token.chars().all(|d| d.is_ascii_digit()) {
if let Ok(idx) = token.parse::<usize>() {
if let Some(val) = captures.get(idx) {
out.push_str(val);
}
}
} else if let Some((i, _)) = names
.iter()
.enumerate()
.find(|(_, n)| n.as_deref() == Some(token.as_str()))
{
if let Some(val) = captures.get(i) {
out.push_str(val);
}
}
} else {
out.push(c);
}
}
out
}
impl<'a> Runner<'a> {
fn expr_to_cow<'b>(&'b self, expr: &'b Expression) -> Cow<'b, str> {
match expr {
Expression::String(s) => {
if !s.contains('$') {
Cow::Borrowed(s.as_str())
} else {
Cow::Owned(self.expr_to_string(expr))
}
}
Expression::Number(n) => Cow::Owned(n.to_string()),
Expression::Regex(r) => Cow::Owned(format!("/{}/", r)),
Expression::Variable(v) => {
if let Some(inner) = self.resolve_variable(v) {
self.expr_to_cow(inner)
} else {
Cow::Borrowed("")
}
}
Expression::Capture(_) | Expression::CaptureName(_) => Cow::Borrowed(""),
}
}
fn expr_to_string(&self, expr: &Expression) -> String {
match expr {
Expression::String(s) => {
if !s.contains('$') {
return s.clone();
}
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' && chars.peek() == Some(&'$') {
out.push('$');
chars.next();
} else if c == '$' {
let mut token = String::new();
while let Some(&p) = chars.peek() {
if p.is_ascii_alphanumeric() || p == '_' {
token.push(p);
chars.next();
} else {
break;
}
}
if token.is_empty() {
out.push('$');
} else if token.chars().all(|d| d.is_ascii_digit()) {
if let Ok(idx) = token.parse::<usize>() {
if let Some(val) = self.last_captures.get(idx) {
out.push_str(&crate::exec::out::percent_encode(val));
}
}
} else if let Some((i, _)) = self
.last_capture_names
.iter()
.enumerate()
.find(|(_, n)| n.as_deref() == Some(token.as_str()))
{
if let Some(val) = self.last_captures.get(i) {
out.push_str(&crate::exec::out::percent_encode(val));
}
}
} else {
out.push(c);
}
}
out
}
Expression::Number(n) => n.to_string(),
Expression::Regex(r) => format!("/{}/", r),
Expression::Variable(v) => {
if let Some(inner) = self.resolve_variable(v) {
self.expr_to_string(inner)
} else {
String::new()
}
}
Expression::Capture(_) | Expression::CaptureName(_) => String::new(), }
}
}
pub fn execute(doc: &mut GelDocument, grammar: &str, input: &str) -> Result<ExecutionResult> {
doc.compile_regexes();
execute_precompiled(doc, grammar, input)
}
pub fn execute_precompiled(doc: &GelDocument, grammar: &str, input: &str) -> Result<ExecutionResult> {
let mut result = Runner::new(doc, input).run_grammar(grammar)?;
let tree = std::mem::take(&mut result.output);
result.flat = Some(tree.compact_and_flatten());
Ok(result)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuntimeFormat {
Json,
Xml,
Yaml,
None,
}
pub fn serialize_execution(result: &ExecutionResult, format: RuntimeFormat) -> String {
match format {
RuntimeFormat::Json => serialize_json(result),
RuntimeFormat::Xml => serialize_xml(result),
RuntimeFormat::Yaml => serialize_yaml(result),
RuntimeFormat::None => String::new(),
}
}
pub fn serialize_tree(result: &ExecutionResult, format: RuntimeFormat) -> String {
match format {
RuntimeFormat::Json => tree_json(result),
RuntimeFormat::Xml => tree_xml(result),
RuntimeFormat::Yaml => tree_yaml(result),
RuntimeFormat::None => String::new(),
}
}
pub fn serialize_tree_to_writer<W: std::io::Write>(
result: &ExecutionResult,
format: RuntimeFormat,
writer: &mut W,
) -> std::io::Result<()> {
match format {
RuntimeFormat::Json => tree_json_to_writer(result, writer),
RuntimeFormat::Xml => tree_xml_to_writer(result, writer),
RuntimeFormat::Yaml => tree_yaml_to_writer(result, writer),
RuntimeFormat::None => Ok(()),
}
}
trait OutputSink {
fn write_str(&mut self, s: &str) -> std::io::Result<()>;
fn write_char(&mut self, c: char) -> std::io::Result<()>;
}
impl OutputSink for String {
#[inline(always)]
fn write_str(&mut self, s: &str) -> std::io::Result<()> {
self.push_str(s);
Ok(())
}
#[inline(always)]
fn write_char(&mut self, c: char) -> std::io::Result<()> {
self.push(c);
Ok(())
}
}
struct WriterSink<W>(W);
impl<W: std::io::Write> OutputSink for WriterSink<W> {
#[inline]
fn write_str(&mut self, s: &str) -> std::io::Result<()> {
self.0.write_all(s.as_bytes())
}
#[inline]
fn write_char(&mut self, c: char) -> std::io::Result<()> {
let mut buf = [0u8; 4];
self.0.write_all(c.encode_utf8(&mut buf).as_bytes())
}
}
fn tree_json(exec: &ExecutionResult) -> String {
let flat = exec.flat.as_ref().expect("FlatTree not built");
let mut out = String::new();
tree_json_node_direct(flat, flat.root(), &mut out, 0);
out
}
fn tree_json_to_writer<W: std::io::Write>(exec: &ExecutionResult, writer: &mut W) -> std::io::Result<()> {
let flat = exec.flat.as_ref().expect("FlatTree not built");
tree_json_node_stream(flat, flat.root(), &mut WriterSink(writer), 0)
}
fn tree_json_node_direct(flat: &FlatTree, node: &FlatNode, out: &mut String, indent: usize) {
let attrs = flat.attrs_of(node);
let has_attrs = !attrs.is_empty();
let has_text = node.text.as_ref().is_some_and(|t| !t.is_empty());
let has_children = node.children_len > 0;
if !has_attrs && !has_text && !has_children {
out.push_str("{}");
return;
}
out.push('{');
let inner = indent + 1;
let mut first = true;
for (k, v) in attrs {
if !first {
out.push(',');
}
out.push('\n');
json_write_indent_direct(out, inner);
json_write_escaped_direct(out, &format!("@{k}"));
out.push_str(": ");
json_write_escaped_direct(out, v);
first = false;
}
if has_text {
if !first {
out.push(',');
}
out.push('\n');
json_write_indent_direct(out, inner);
json_write_escaped_direct(out, "#text");
out.push_str(": ");
json_write_escaped_direct(out, node.text.as_ref().unwrap());
first = false;
}
for group in flat.iter_child_groups(node) {
if !first {
out.push(',');
}
out.push('\n');
json_write_indent_direct(out, inner);
json_write_escaped_direct(out, &group[0].name);
out.push_str(": ");
if group.len() == 1 {
tree_json_node_direct(flat, &group[0], out, inner);
} else {
out.push('[');
for (i, ch) in group.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push('\n');
json_write_indent_direct(out, inner + 1);
tree_json_node_direct(flat, ch, out, inner + 1);
}
out.push('\n');
json_write_indent_direct(out, inner);
out.push(']');
}
first = false;
}
out.push('\n');
json_write_indent_direct(out, indent);
out.push('}');
}
#[inline]
fn json_write_indent_direct(out: &mut String, level: usize) {
for _ in 0..level {
out.push_str(" ");
}
}
fn json_write_escaped_direct(out: &mut String, s: &str) {
out.push('"');
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\u{0008}' => out.push_str("\\b"),
'\u{000C}' => out.push_str("\\f"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
let n = c as u32;
out.push_str("\\u00");
out.push(char::from(b"0123456789abcdef"[(n >> 4) as usize]));
out.push(char::from(b"0123456789abcdef"[(n & 0xF) as usize]));
}
c => out.push(c),
}
}
out.push('"');
}
fn tree_json_node_stream<S: OutputSink>(
flat: &FlatTree,
node: &FlatNode,
out: &mut S,
indent: usize,
) -> std::io::Result<()> {
let attrs = flat.attrs_of(node);
let has_attrs = !attrs.is_empty();
let has_text = node.text.as_ref().is_some_and(|t| !t.is_empty());
let has_children = node.children_len > 0;
if !has_attrs && !has_text && !has_children {
return out.write_str("{}");
}
out.write_char('{')?;
let inner = indent + 1;
let mut first = true;
for (k, v) in attrs {
if !first {
out.write_char(',')?;
}
out.write_char('\n')?;
json_write_indent_stream(out, inner)?;
json_write_escaped_stream(out, &format!("@{k}"))?;
out.write_str(": ")?;
json_write_escaped_stream(out, v)?;
first = false;
}
if has_text {
if !first {
out.write_char(',')?;
}
out.write_char('\n')?;
json_write_indent_stream(out, inner)?;
json_write_escaped_stream(out, "#text")?;
out.write_str(": ")?;
json_write_escaped_stream(out, node.text.as_ref().unwrap())?;
first = false;
}
for group in flat.iter_child_groups(node) {
if !first {
out.write_char(',')?;
}
out.write_char('\n')?;
json_write_indent_stream(out, inner)?;
json_write_escaped_stream(out, &group[0].name)?;
out.write_str(": ")?;
if group.len() == 1 {
tree_json_node_stream(flat, &group[0], out, inner)?;
} else {
out.write_char('[')?;
for (i, ch) in group.iter().enumerate() {
if i > 0 {
out.write_char(',')?;
}
out.write_char('\n')?;
json_write_indent_stream(out, inner + 1)?;
tree_json_node_stream(flat, ch, out, inner + 1)?;
}
out.write_char('\n')?;
json_write_indent_stream(out, inner)?;
out.write_char(']')?;
}
first = false;
}
out.write_char('\n')?;
json_write_indent_stream(out, indent)?;
out.write_char('}')
}
#[inline]
fn json_write_indent_stream<S: OutputSink>(out: &mut S, level: usize) -> std::io::Result<()> {
for _ in 0..level {
out.write_str(" ")?;
}
Ok(())
}
fn json_write_escaped_stream<S: OutputSink>(out: &mut S, s: &str) -> std::io::Result<()> {
out.write_char('"')?;
for ch in s.chars() {
match ch {
'"' => out.write_str("\\\"")?,
'\\' => out.write_str("\\\\")?,
'\u{0008}' => out.write_str("\\b")?,
'\u{000C}' => out.write_str("\\f")?,
'\n' => out.write_str("\\n")?,
'\r' => out.write_str("\\r")?,
'\t' => out.write_str("\\t")?,
c if (c as u32) < 0x20 => {
let n = c as u32;
out.write_str("\\u00")?;
out.write_char(char::from(b"0123456789abcdef"[(n >> 4) as usize]))?;
out.write_char(char::from(b"0123456789abcdef"[(n & 0xF) as usize]))?;
}
c => out.write_char(c)?,
}
}
out.write_char('"')
}
struct AposWriter<W: std::io::Write> {
inner: W,
}
impl<W: std::io::Write> std::io::Write for AposWriter<W> {
fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
write_bytes_replacing_apos(&mut self.inner, data)?;
Ok(data.len())
}
fn flush(&mut self) -> std::io::Result<()> {
self.inner.flush()
}
}
fn tree_xml(exec: &ExecutionResult) -> String {
let mut buf = Vec::new();
tree_xml_to_writer(exec, &mut buf).expect("XML write to Vec never fails");
unsafe { String::from_utf8_unchecked(buf) }
}
fn tree_xml_to_writer<W: std::io::Write>(exec: &ExecutionResult, writer: &mut W) -> std::io::Result<()> {
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
use quick_xml::Writer;
let flat = exec.flat.as_ref().expect("FlatTree not built");
let root = flat.root();
let root_name = if root.name.is_empty() || &*root.name == "." {
"xml"
} else {
&root.name
};
{
let mut w = Writer::new_with_indent(AposWriter { inner: &mut *writer }, b' ', 2);
let mut start = BytesStart::new(root_name);
for (k, v) in flat.attrs_of(root) {
start.push_attribute((&**k, v.as_str()));
}
w.write_event(Event::Start(start)).expect("root start");
if let Some(txt) = &root.text {
if !txt.is_empty() {
let escaped = xml_escape_text_content(txt);
w.write_event(Event::Text(BytesText::from_escaped(escaped)))
.expect("root text");
}
}
for group in flat.iter_child_groups(root) {
for ch in group {
tree_xml_node(flat, ch, &mut w);
}
}
w.write_event(Event::End(BytesEnd::new(root_name))).expect("root end");
}
writer.write_all(b"\n")
}
fn tree_xml_node<W: std::io::Write>(flat: &FlatTree, node: &FlatNode, writer: &mut quick_xml::Writer<W>) {
use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
let tag: &str = &node.name;
let has_text = node.text.as_ref().is_some_and(|t| !t.is_empty());
let is_empty = !has_text && node.children_len == 0;
if is_empty {
let mut elem = BytesStart::new(tag);
for (k, v) in flat.attrs_of(node) {
elem.push_attribute((&**k, v.as_str()));
}
writer.write_event(Event::Empty(elem)).expect("empty elem");
return;
}
let mut start = BytesStart::new(tag);
for (k, v) in flat.attrs_of(node) {
start.push_attribute((&**k, v.as_str()));
}
writer.write_event(Event::Start(start)).expect("elem start");
if has_text {
let txt = node.text.as_ref().unwrap();
let escaped = xml_escape_text_content(txt);
writer
.write_event(Event::Text(BytesText::from_escaped(escaped)))
.expect("text");
}
for group in flat.iter_child_groups(node) {
for ch in group {
tree_xml_node(flat, ch, writer);
}
}
writer.write_event(Event::End(BytesEnd::new(tag))).expect("elem end");
}
fn xml_escape_text_content(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'&' => out.push_str("&"),
_ => out.push(ch),
}
}
out
}
fn write_bytes_replacing_apos<W: std::io::Write>(writer: &mut W, bytes: &[u8]) -> std::io::Result<()> {
let needle = b"'";
let mut cursor = 0usize;
for pos in memchr::memmem::find_iter(bytes, needle) {
writer.write_all(&bytes[cursor..pos])?;
writer.write_all(b"'")?;
cursor = pos + needle.len();
}
writer.write_all(&bytes[cursor..])
}
fn tree_yaml(exec: &ExecutionResult) -> String {
let flat = exec.flat.as_ref().expect("FlatTree not built");
let mut out = String::new();
tree_yaml_node_direct(flat, flat.root(), &mut out, 0, false);
out.push('\n');
out
}
fn tree_yaml_to_writer<W: std::io::Write>(exec: &ExecutionResult, writer: &mut W) -> std::io::Result<()> {
let flat = exec.flat.as_ref().expect("FlatTree not built");
tree_yaml_node_stream(flat, flat.root(), &mut WriterSink(&mut *writer), 0, false)?;
writer.write_all(b"\n")
}
fn tree_yaml_node_direct(flat: &FlatTree, node: &FlatNode, out: &mut String, indent: usize, inline: bool) {
let attrs = flat.attrs_of(node);
let has_attrs = !attrs.is_empty();
let has_text = node.text.as_ref().is_some_and(|t| !t.is_empty());
let has_children = node.children_len > 0;
if !has_attrs && !has_text && !has_children {
out.push_str("{}");
return;
}
let mut entry_idx: usize = 0;
for (k, v) in attrs {
if entry_idx > 0 || !inline {
push_yaml_indent_direct(out, indent);
}
out.push('\'');
out.push('@');
out.push_str(k);
out.push('\'');
out.push_str(": ");
out.push_str(&yaml_quote_value(v));
out.push('\n');
entry_idx += 1;
}
if has_text {
if entry_idx > 0 || !inline {
push_yaml_indent_direct(out, indent);
}
out.push_str("'#text': ");
out.push_str(&yaml_quote_value(node.text.as_ref().unwrap()));
out.push('\n');
entry_idx += 1;
}
for group in flat.iter_child_groups(node) {
let name = &group[0].name;
if entry_idx > 0 || !inline {
push_yaml_indent_direct(out, indent);
}
out.push_str(&yaml_quote_key(name));
out.push(':');
if group.len() == 1 {
out.push('\n');
tree_yaml_node_direct(flat, &group[0], out, indent + 1, false);
} else {
out.push('\n');
for ch in group {
push_yaml_indent_direct(out, indent);
out.push_str("- ");
tree_yaml_node_direct(flat, ch, out, indent + 2, true);
}
}
entry_idx += 1;
}
}
#[inline]
fn push_yaml_indent_direct(out: &mut String, indent: usize) {
const SPACES: &str = " ";
let n = indent * 2;
if n <= SPACES.len() {
out.push_str(&SPACES[..n]);
} else {
for _ in 0..n {
out.push(' ');
}
}
}
fn tree_yaml_node_stream<S: OutputSink>(
flat: &FlatTree,
node: &FlatNode,
out: &mut S,
indent: usize,
inline: bool,
) -> std::io::Result<()> {
let attrs = flat.attrs_of(node);
let has_attrs = !attrs.is_empty();
let has_text = node.text.as_ref().is_some_and(|t| !t.is_empty());
let has_children = node.children_len > 0;
if !has_attrs && !has_text && !has_children {
return out.write_str("{}");
}
let mut entry_idx: usize = 0;
for (k, v) in attrs {
if entry_idx > 0 || !inline {
push_yaml_indent_stream(out, indent)?;
}
out.write_char('\'')?;
out.write_char('@')?;
out.write_str(k)?;
out.write_char('\'')?;
out.write_str(": ")?;
out.write_str(&yaml_quote_value(v))?;
out.write_char('\n')?;
entry_idx += 1;
}
if has_text {
if entry_idx > 0 || !inline {
push_yaml_indent_stream(out, indent)?;
}
out.write_str("'#text': ")?;
out.write_str(&yaml_quote_value(node.text.as_ref().unwrap()))?;
out.write_char('\n')?;
entry_idx += 1;
}
for group in flat.iter_child_groups(node) {
let name = &group[0].name;
if entry_idx > 0 || !inline {
push_yaml_indent_stream(out, indent)?;
}
out.write_str(&yaml_quote_key(name))?;
out.write_char(':')?;
if group.len() == 1 {
out.write_char('\n')?;
tree_yaml_node_stream(flat, &group[0], out, indent + 1, false)?;
} else {
out.write_char('\n')?;
for ch in group {
push_yaml_indent_stream(out, indent)?;
out.write_str("- ")?;
tree_yaml_node_stream(flat, ch, out, indent + 2, true)?;
}
}
entry_idx += 1;
}
Ok(())
}
#[inline]
fn push_yaml_indent_stream<S: OutputSink>(out: &mut S, indent: usize) -> std::io::Result<()> {
const SPACES: &str = " ";
let n = indent * 2;
if n <= SPACES.len() {
out.write_str(&SPACES[..n])
} else {
for _ in 0..n {
out.write_char(' ')?;
}
Ok(())
}
}
fn yaml_quote_key(k: &str) -> Cow<'_, str> {
if k.starts_with('@') || k.starts_with('#') {
Cow::Owned(format!("'{}'", k))
} else {
Cow::Borrowed(k)
}
}
fn yaml_quote_value(s: &str) -> Cow<'_, str> {
if s.is_empty() {
return Cow::Borrowed("''");
}
let needs_quoting = s.contains(':')
|| s.contains('#')
|| s.contains('\'')
|| s.contains('"')
|| s.contains('\n')
|| s.contains('\r')
|| s.starts_with(' ')
|| s.ends_with(' ')
|| s.starts_with('{')
|| s.starts_with('[')
|| s.starts_with('*')
|| s.starts_with('&')
|| s.starts_with('!')
|| s.starts_with('%')
|| s.starts_with('|')
|| s.starts_with('>')
|| s.starts_with('@')
|| looks_like_number(s)
|| looks_like_date(s)
|| matches!(
s,
"true"
| "false"
| "yes"
| "no"
| "null"
| "True"
| "False"
| "Yes"
| "No"
| "Null"
| "TRUE"
| "FALSE"
| "YES"
| "NO"
| "NULL"
| "on"
| "off"
| "On"
| "Off"
| "ON"
| "OFF"
);
if !needs_quoting {
return Cow::Borrowed(s);
}
if !s.contains('\'') {
Cow::Owned(format!("'{}'", s))
} else if !s.contains('"') {
Cow::Owned(format!("\"{}\"", s))
} else {
Cow::Owned(format!("'{}'", s.replace('\'', "''")))
}
}
fn looks_like_number(s: &str) -> bool {
s.parse::<f64>().is_ok()
}
fn looks_like_date(s: &str) -> bool {
s.len() == 10
&& s.as_bytes().get(4) == Some(&b'-')
&& s.as_bytes().get(7) == Some(&b'-')
&& s[..4].chars().all(|c| c.is_ascii_digit())
&& s[5..7].chars().all(|c| c.is_ascii_digit())
&& s[8..10].chars().all(|c| c.is_ascii_digit())
}
fn serialize_json(exec: &ExecutionResult) -> String {
use serde_json::{json, Map, Value};
let actions: Vec<Value> = exec
.actions
.iter()
.map(|a| {
let args: Vec<Value> = a
.args
.iter()
.map(|arg| match arg {
Expression::String(s) => Value::String(s.clone()),
Expression::Regex(r) => Value::String(format!("/{}/", r)),
Expression::Number(n) => json!(*n),
Expression::Variable(v) => Value::String(format!("${}", v)),
Expression::Capture(i) => Value::String(format!("${}", i)),
Expression::CaptureName(name) => Value::String(format!("${}", name)),
})
.collect();
json!({"name": &*a.name, "args": args})
})
.collect();
let diagnostics: Vec<Value> = exec
.diagnostics
.iter()
.map(|d| {
let mut obj = Map::new();
obj.insert("severity".into(), Value::String(d.severity.to_string()));
obj.insert("message".into(), Value::String(d.message.clone()));
if let Some(s) = &d.span {
obj.insert("line".into(), json!(s.line));
obj.insert("col".into(), json!(s.col));
obj.insert("offset".into(), json!(s.offset));
}
Value::Object(obj)
})
.collect();
let flat = exec.flat.as_ref().expect("FlatTree not built");
let output = output_node_to_value(flat, flat.root());
let result = json!({
"consumed": exec.consumed,
"actions": actions,
"traces": exec.traces,
"capture_history": exec.capture_history,
"capture_names_history": exec.capture_names_history.iter().map(|scope| scope.iter().map(|n| n.as_deref().unwrap_or("").to_string()).collect::<Vec<_>>()).collect::<Vec<_>>(),
"error": exec.error,
"diagnostics": diagnostics,
"output": output,
});
serde_json::to_string_pretty(&result).unwrap_or_default()
}
fn output_node_to_value(flat: &FlatTree, node: &FlatNode) -> serde_json::Value {
use serde_json::{Map, Value};
let mut obj = Map::new();
for (k, v) in flat.attrs_of(node) {
obj.insert(format!("@{}", k), Value::String(v.clone()));
}
if let Some(txt) = &node.text {
if !txt.is_empty() {
obj.insert("#text".into(), Value::String((**txt).clone()));
}
}
if node.children_len > 0 {
for group in flat.iter_child_groups(node) {
let name = &group[0].name;
if group.len() == 1 {
obj.insert(name.to_string(), output_node_to_value(flat, &group[0]));
} else {
let arr: Vec<Value> = group.iter().map(|ch| output_node_to_value(flat, ch)).collect();
obj.insert(name.to_string(), Value::Array(arr));
}
}
}
Value::Object(obj)
}
fn serialize_xml(exec: &ExecutionResult) -> String {
use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, Event};
use quick_xml::Writer;
use std::io::Cursor;
let mut w = Writer::new_with_indent(Cursor::new(Vec::new()), b' ', 2);
w.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))
.expect("xml decl");
let mut exec_start = BytesStart::new("execution");
exec_start.push_attribute(("consumed", exec.consumed.to_string().as_str()));
w.write_event(Event::Start(exec_start)).expect("execution start");
if !exec.actions.is_empty() {
w.write_event(Event::Start(BytesStart::new("actions")))
.expect("actions start");
for a in &exec.actions {
let mut action = BytesStart::new("action");
action.push_attribute(("name", &*a.name));
w.write_event(Event::Start(action)).expect("action start");
if !a.args.is_empty() {
w.write_event(Event::Start(BytesStart::new("args")))
.expect("args start");
for arg in &a.args {
w.write_event(Event::Start(BytesStart::new("arg"))).expect("arg start");
let text = match arg {
Expression::String(s) => s.clone(),
Expression::Regex(r) => r.clone(),
Expression::Number(n) => n.to_string(),
Expression::Variable(v) => v.clone(),
Expression::Capture(i) => format!("${}", i),
Expression::CaptureName(name) => format!("${}", name),
};
xml_write_escaped_text(&mut w, &text);
w.write_event(Event::End(BytesEnd::new("arg"))).expect("arg end");
}
w.write_event(Event::End(BytesEnd::new("args"))).expect("args end");
}
w.write_event(Event::End(BytesEnd::new("action"))).expect("action end");
}
w.write_event(Event::End(BytesEnd::new("actions")))
.expect("actions end");
}
if !exec.traces.is_empty() {
w.write_event(Event::Start(BytesStart::new("traces")))
.expect("traces start");
for t in &exec.traces {
w.write_event(Event::Start(BytesStart::new("trace")))
.expect("trace start");
xml_write_escaped_text(&mut w, t);
w.write_event(Event::End(BytesEnd::new("trace"))).expect("trace end");
}
w.write_event(Event::End(BytesEnd::new("traces"))).expect("traces end");
}
if !exec.capture_history.is_empty() {
w.write_event(Event::Start(BytesStart::new("captures")))
.expect("captures start");
for (i, scope) in exec.capture_history.iter().enumerate() {
let mut scope_start = BytesStart::new("scope");
scope_start.push_attribute(("index", i.to_string().as_str()));
w.write_event(Event::Start(scope_start)).expect("scope start");
for (j, val) in scope.iter().enumerate() {
let name = exec
.capture_names_history
.get(i)
.and_then(|nms| nms.get(j))
.map(|n| n.as_deref().unwrap_or(""))
.unwrap_or("");
let mut value_start = BytesStart::new("value");
value_start.push_attribute(("name", name));
w.write_event(Event::Start(value_start)).expect("value start");
xml_write_escaped_text(&mut w, val);
w.write_event(Event::End(BytesEnd::new("value"))).expect("value end");
}
w.write_event(Event::End(BytesEnd::new("scope"))).expect("scope end");
}
w.write_event(Event::End(BytesEnd::new("captures")))
.expect("captures end");
}
w.write_event(Event::Start(BytesStart::new("output")))
.expect("output start");
let flat = exec.flat.as_ref().expect("FlatTree not built");
xml_write_node(flat, flat.root(), &mut w);
w.write_event(Event::End(BytesEnd::new("output"))).expect("output end");
w.write_event(Event::Start(BytesStart::new("error")))
.expect("error start");
if let Some(e) = &exec.error {
xml_write_escaped_text(&mut w, e);
}
w.write_event(Event::End(BytesEnd::new("error"))).expect("error end");
w.write_event(Event::End(BytesEnd::new("execution")))
.expect("execution end");
String::from_utf8(w.into_inner().into_inner()).unwrap_or_default()
}
fn xml_write_escaped_text<W: std::io::Write>(writer: &mut quick_xml::Writer<W>, text: &str) {
use quick_xml::events::{BytesText, Event};
let escaped = quick_xml::escape::escape(text);
writer
.write_event(Event::Text(BytesText::from_escaped(escaped)))
.expect("text write");
}
fn xml_write_node<W: std::io::Write>(flat: &FlatTree, node: &FlatNode, writer: &mut quick_xml::Writer<W>) {
use quick_xml::events::{BytesEnd, BytesStart, Event};
let mut start = BytesStart::new("node");
start.push_attribute(("name", &*node.name));
writer.write_event(Event::Start(start)).expect("node start");
for (k, v) in flat.attrs_of(node) {
let mut attr_elem = BytesStart::new("attr");
attr_elem.push_attribute(("key", &**k));
attr_elem.push_attribute(("value", v.as_str()));
writer.write_event(Event::Empty(attr_elem)).expect("attr");
}
if let Some(txt) = &node.text {
xml_write_escaped_text(writer, txt);
}
for ch in flat.children_of(node) {
xml_write_node(flat, ch, writer);
}
writer.write_event(Event::End(BytesEnd::new("node"))).expect("node end");
}
fn serialize_yaml(exec: &ExecutionResult) -> String {
let mut out = String::new();
out.push_str("execution:\n consumed: ");
out.push_str(&exec.consumed.to_string());
out.push_str("\n actions:\n");
if exec.actions.is_empty() {
out.push_str(" []\n");
} else {
for a in &exec.actions {
out.push_str(" - name: ");
out.push_str(&yaml_scalar(&a.name));
if a.args.is_empty() {
out.push('\n');
} else {
out.push_str("\n args:\n");
for arg in &a.args {
out.push_str(" - ");
match arg {
Expression::String(s) => {
out.push_str(&yaml_scalar(s));
}
Expression::Regex(r) => {
out.push_str(&yaml_scalar(r));
}
Expression::Number(n) => {
out.push_str(&n.to_string());
}
Expression::Variable(v) => {
out.push_str(&yaml_scalar(v));
}
Expression::Capture(i) => {
out.push_str(&format!("\"${}\"", i));
}
Expression::CaptureName(name) => {
out.push_str(&format!("\"${}\"", name));
}
}
out.push('\n');
}
}
}
}
out.push_str(" traces:\n");
if exec.traces.is_empty() {
out.push_str(" []\n");
} else {
for t in &exec.traces {
out.push_str(" - ");
out.push_str(&yaml_scalar(t));
out.push('\n');
}
}
out.push_str(" capture_history:\n");
if exec.capture_history.is_empty() {
out.push_str(" []\n");
} else {
for scope in &exec.capture_history {
out.push_str(" - [");
for (i, val) in scope.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push_str(&yaml_scalar(val));
}
out.push_str("]\n");
}
}
out.push_str(" capture_names_history:\n");
if exec.capture_names_history.is_empty() {
out.push_str(" []\n");
} else {
for scope in &exec.capture_names_history {
out.push_str(" - [");
for (i, val) in scope.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push_str(&yaml_scalar(val.as_deref().unwrap_or("")));
}
out.push_str("]\n");
}
}
out.push_str(" error: ");
if let Some(e) = &exec.error {
out.push_str(&yaml_scalar(e));
} else {
out.push_str("null");
}
out.push_str("\n output:\n");
let flat = exec.flat.as_ref().expect("FlatTree not built");
serialize_yaml_node(flat, flat.root(), &mut out, 2);
out
}
fn yaml_scalar(s: &str) -> String {
if s.chars()
.any(|c| c.is_whitespace() || matches!(c, ':' | '-' | '#' | ',' | '[' | ']' | '{' | '}'))
{
format!("\"{}\"", s.replace('"', "\\\""))
} else {
s.to_string()
}
}
fn serialize_yaml_node(flat: &FlatTree, n: &FlatNode, out: &mut String, indent: usize) {
for _ in 0..indent {
out.push_str(" ");
}
out.push_str("- name: ");
out.push_str(&yaml_scalar(&n.name));
out.push('\n');
let n_attrs = flat.attrs_of(n);
if !n_attrs.is_empty() {
for (k, v) in n_attrs {
for _ in 0..indent {
out.push_str(" ");
}
out.push_str(" attribute: ");
out.push_str(&yaml_scalar(k));
out.push_str(": ");
out.push_str(&yaml_scalar(v));
out.push('\n');
}
}
if let Some(txt) = &n.text {
for _ in 0..indent {
out.push_str(" ");
}
out.push_str(" text: ");
out.push_str(&yaml_scalar(txt));
out.push('\n');
}
if n.children_len > 0 {
for _ in 0..indent {
out.push_str(" ");
}
out.push_str(" children:\n");
for ch in flat.children_of(n) {
serialize_yaml_node(flat, ch, out, indent + 2);
}
}
}