use crate::core::{Element, ElementKind, MacroDef};
use std::collections::HashMap;
#[derive(Clone, Copy)]
pub(super) struct Term {
pub(super) heeds: bool,
pub(super) line: u32,
}
pub(super) fn sync_macro_defs(els: &mut [Element], macros: &mut HashMap<String, MacroDef>) {
for el in els.iter_mut() {
if let ElementKind::Macro { def } = &el.kind {
macros.insert(def.name.clone(), def.clone());
}
for child in children_mut(el) {
sync_macro_defs(child, macros);
}
}
}
pub(super) fn children_mut(el: &mut Element) -> Vec<&mut Vec<Element>> {
match &mut el.kind {
ElementKind::If { then, else_, .. } => {
let mut v = vec![then];
if let Some(e) = else_ {
v.push(e);
}
v
}
ElementKind::List { body, else_, .. } => {
let mut v = vec![body];
if let Some(e) = else_ {
v.push(e);
}
v
}
ElementKind::Macro { def, .. } => vec![&mut def.body],
ElementKind::Trim(b)
| ElementKind::Compress(b)
| ElementKind::NoEscape(b)
| ElementKind::AutoEsc(b)
| ElementKind::NoAutoEsc(b)
| ElementKind::BlockAssign { body: b, .. }
| ElementKind::Items { body: b, .. }
| ElementKind::Sep { body: b }
| ElementKind::Transform { body: b, .. }
| ElementKind::On { body: b, .. }
| ElementKind::Call { body: Some(b), .. }
| ElementKind::Escape { body: b, .. }
| ElementKind::OutputFormat { body: b, .. }
| ElementKind::Nested { body: Some(b), .. }
| ElementKind::Global { body: Some(b), .. }
| ElementKind::Local { body: Some(b), .. } => vec![b],
ElementKind::Attempt { try_, recover, .. } => vec![try_, recover],
ElementKind::Switch { cases, default, .. } => {
let mut v = Vec::new();
for c in cases {
v.push(&mut c.body);
}
if let Some(d) = default {
v.push(d);
}
v
}
_ => Vec::new(),
}
}
pub(super) fn is_trim_element(el: &Element) -> bool {
matches!(
el.kind,
ElementKind::TrimLineStart
| ElementKind::NoTrimLineStart
| ElementKind::TrimLineEnd
| ElementKind::LeftTrimLine
)
}
pub(super) fn leaf_heeds_opening(el: &Element) -> bool {
match &el.kind {
ElementKind::Text { text, .. } | ElementKind::NoParse { text, .. } => heeds_opening(text),
ElementKind::Interpolation { .. } => true,
_ => false,
}
}
pub(super) fn is_non_outputting(el: &Element) -> bool {
matches!(
el.kind,
ElementKind::Macro { .. }
| ElementKind::Assign { .. }
| ElementKind::Assignments(_)
| ElementKind::BlockAssign { .. }
| ElementKind::Global { .. }
| ElementKind::Local { .. }
| ElementKind::Setting { .. }
| ElementKind::Import { .. }
| ElementKind::Comment { .. }
)
}
pub(super) fn is_ignorable_text(els: &[Element], j: usize, is_root: bool) -> bool {
let (empty, all_ws) = match &els[j].kind {
ElementKind::Text { text, .. } | ElementKind::NoParse { text, .. } => {
(text.is_empty(), text.chars().all(is_ws))
}
_ => return false,
};
if empty {
return true;
}
if !all_ws {
return false;
}
let prev_ok = (j == 0 && is_root) || (j > 0 && is_non_outputting(&els[j - 1]));
let next_ok =
(j + 1 == els.len() && is_root) || (j + 1 < els.len() && is_non_outputting(&els[j + 1]));
prev_ok && next_ok
}
pub(super) fn walk_prev(
els: &[Element],
i: usize,
line: u32,
parent_prev: Option<Term>,
is_root: bool,
) -> Option<Term> {
let mut j = i;
loop {
if j == 0 {
return parent_prev;
}
j -= 1;
let e = &els[j];
if is_trim_element(e) || is_ignorable_text(els, j, is_root) {
continue;
}
match last_leaf(e) {
Some(t) => {
if t.line != line {
return None;
}
if t.heeds {
return Some(t);
}
}
None => {
if e.span.line != line {
return None;
}
}
}
}
}
pub(super) fn walk_next(
els: &[Element],
i: usize,
line: u32,
parent_next: Option<Term>,
is_root: bool,
) -> Option<Term> {
let mut j = i;
loop {
j += 1;
let Some(e) = els.get(j) else {
return parent_next;
};
if is_trim_element(e) || is_ignorable_text(els, j, is_root) {
continue;
}
match first_leaf(e) {
Some(t) => {
if t.line != line {
return None;
}
if t.heeds {
return Some(t);
}
}
None => {
if e.span.line != line {
return None;
}
}
}
}
}
pub(super) fn first_leaf(el: &Element) -> Option<Term> {
match &el.kind {
ElementKind::Text { text, .. } => Some(Term {
heeds: heeds_trailing(text),
line: el.span.line,
}),
ElementKind::Interpolation { .. } => Some(Term {
heeds: true,
line: el.span.line,
}),
ElementKind::If { then, else_, .. } => {
first_leaf_slice(then).or_else(|| else_.as_deref().and_then(first_leaf_slice))
}
ElementKind::List { body, else_, .. } => {
first_leaf_slice(body).or_else(|| else_.as_deref().and_then(first_leaf_slice))
}
ElementKind::Macro { .. } => Some(Term {
heeds: false,
line: el.span.line,
}),
ElementKind::BlockAssign { .. } => Some(Term {
heeds: false,
line: el.span.line,
}),
ElementKind::Trim(b)
| ElementKind::Compress(b)
| ElementKind::NoEscape(b)
| ElementKind::AutoEsc(b)
| ElementKind::NoAutoEsc(b)
| ElementKind::Items { body: b, .. }
| ElementKind::Sep { body: b }
| ElementKind::Transform { body: b, .. }
| ElementKind::On { body: b, .. }
| ElementKind::Call { body: Some(b), .. }
| ElementKind::Escape { body: b, .. }
| ElementKind::OutputFormat { body: b, .. }
| ElementKind::Nested { body: Some(b), .. }
| ElementKind::Global { body: Some(b), .. }
| ElementKind::Local { body: Some(b), .. } => first_leaf_slice(b),
ElementKind::Attempt { try_, recover, .. } => {
first_leaf_slice(try_).or_else(|| first_leaf_slice(recover))
}
ElementKind::Switch { cases, default, .. } => {
for c in cases {
if !c.body.is_empty() {
return first_leaf(&c.body[0]);
}
}
default.as_deref().and_then(first_leaf_slice)
}
_ => Some(Term {
heeds: false,
line: el.span.line,
}),
}
}
pub(super) fn first_leaf_slice(els: &[Element]) -> Option<Term> {
els.iter()
.find(|e| !is_trim_element(e))
.and_then(first_leaf)
}
pub(super) fn last_leaf(el: &Element) -> Option<Term> {
match &el.kind {
ElementKind::Text {
text,
orig_end_line,
..
} => Some(Term {
heeds: heeds_opening(text),
line: *orig_end_line,
}),
ElementKind::Interpolation { .. } => Some(Term {
heeds: true,
line: el.span.line,
}),
ElementKind::Comment { text } | ElementKind::RawText(text) => Some(Term {
heeds: false,
line: el.span.line + newline_count(text),
}),
ElementKind::NoParse {
text,
orig_end_line,
..
} => Some(Term {
heeds: heeds_opening(text),
line: *orig_end_line,
}),
ElementKind::If { then, else_, .. } => {
last_leaf_slice(then).or_else(|| else_.as_deref().and_then(last_leaf_slice))
}
ElementKind::List { body, else_, .. } => {
last_leaf_slice(body).or_else(|| else_.as_deref().and_then(last_leaf_slice))
}
ElementKind::Macro { .. } => Some(Term {
heeds: false,
line: el.span.line,
}),
ElementKind::BlockAssign { .. } => Some(Term {
heeds: false,
line: el.span.line,
}),
ElementKind::Trim(b)
| ElementKind::Compress(b)
| ElementKind::NoEscape(b)
| ElementKind::AutoEsc(b)
| ElementKind::NoAutoEsc(b)
| ElementKind::Items { body: b, .. }
| ElementKind::Sep { body: b }
| ElementKind::Transform { body: b, .. }
| ElementKind::On { body: b, .. }
| ElementKind::Call { body: Some(b), .. }
| ElementKind::Escape { body: b, .. }
| ElementKind::OutputFormat { body: b, .. }
| ElementKind::Nested { body: Some(b), .. }
| ElementKind::Global { body: Some(b), .. }
| ElementKind::Local { body: Some(b), .. } => last_leaf_slice(b),
ElementKind::Attempt { try_, recover, .. } => {
last_leaf_slice(recover).or_else(|| last_leaf_slice(try_))
}
ElementKind::Switch { cases, default, .. } => {
if let Some(d) = default {
if !d.is_empty() {
return last_leaf(&d[d.len() - 1]);
}
}
for c in cases.iter().rev() {
if !c.body.is_empty() {
return last_leaf(&c.body[c.body.len() - 1]);
}
}
None
}
_ => Some(Term {
heeds: false,
line: el.span.line,
}),
}
}
pub(super) fn last_leaf_slice(els: &[Element]) -> Option<Term> {
els.iter()
.rev()
.find(|e| !is_trim_element(e))
.and_then(last_leaf)
}
pub(super) fn heeds_opening(text: &str) -> bool {
if text.is_empty() {
return false;
}
for c in text.chars().rev() {
if c == '\n' || c == '\r' {
return false;
}
if !is_ws(c) {
return true;
}
}
true
}
pub(super) fn heeds_trailing(text: &str) -> bool {
if text.is_empty() {
return false;
}
for c in text.chars() {
if c == '\n' || c == '\r' {
return false;
}
if !is_ws(c) {
return true;
}
}
true
}
pub(super) fn newline_count(text: &str) -> u32 {
text.chars().filter(|c| *c == '\n').count() as u32
}
pub(super) fn first_newline_end(s: &str) -> usize {
match s.find('\n') {
Some(i) => i + 1,
None => s.len(),
}
}
pub(super) fn last_newline_start(s: &str) -> usize {
match s.rfind('\n') {
Some(i) => i + 1,
None => s.len(),
}
}
pub(super) fn is_ws(c: char) -> bool {
c == ' ' || c == '\t' || c == '\r' || c == '\n'
}
pub(super) fn leading_ws_through_newline(text: &str) -> bool {
for c in text.chars() {
if c == '\n' || c == '\r' {
return true;
}
if !is_ws(c) {
return false;
}
}
false
}
pub(super) fn trailing_ws_after_newline(text: &str, begin_col: u32) -> bool {
match text.rfind(['\n', '\r']) {
Some(i) => {
let trail = &text[i + 1..];
!trail.is_empty() && trail.chars().all(is_ws)
}
None => begin_col == 1 && !text.is_empty() && text.chars().all(is_ws),
}
}