use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use crate::types::VersionDelimiter;
use crate::words::{at, before, collapse_chars, is_whitespace, string, sub};
pub(crate) const PH: char = '\u{e000}';
#[derive(Debug, Clone)]
pub(crate) struct Group {
pub open: VersionDelimiter,
pub inner: String,
pub raw: String,
pub pos: i64,
}
pub(crate) type Groups = Rc<HashMap<i64, Group>>;
#[derive(Debug, Clone)]
pub(crate) struct Skel {
pub text: Vec<char>,
pub pos: Vec<i64>,
pub groups: Groups,
}
fn closer_of(ch: char) -> Option<char> {
match ch {
'(' => Some(')'),
'[' => Some(']'),
'{' => Some('}'),
_ => None,
}
}
fn delimiter_of(ch: Option<char>) -> VersionDelimiter {
match ch {
Some('[') => VersionDelimiter::Square,
Some('{') => VersionDelimiter::Curly,
_ => VersionDelimiter::Paren,
}
}
pub(crate) fn scan(s: &[char], base: i64) -> (Skel, bool) {
let mut offsets: Vec<i64> = Vec::with_capacity(s.len() + 1);
let mut off = base;
for ch in s {
offsets.push(off);
off += ch.len_utf16() as i64;
}
offsets.push(off);
let offset_at = |i: usize| offsets.get(i).copied().unwrap_or(off);
let mut text: Vec<char> = Vec::with_capacity(s.len());
let mut pos: Vec<i64> = Vec::with_capacity(s.len());
let mut groups: HashMap<i64, Group> = HashMap::new();
let mut stack: Vec<char> = Vec::new();
let mut group_start = 0usize;
for (i, &ch) in s.iter().enumerate() {
if stack.is_empty() {
if let Some(closer) = closer_of(ch) {
stack.push(closer);
group_start = i;
} else {
text.push(ch);
pos.push(offset_at(i));
}
continue;
}
if let Some(closer) = closer_of(ch) {
stack.push(closer);
} else if stack.last() == Some(&ch) {
stack.pop();
if stack.is_empty() {
add_group(s, group_start, i + 1, true, &offset_at, &mut groups);
text.push(PH);
pos.push(offset_at(group_start));
}
}
}
let unbalanced = !stack.is_empty();
if unbalanced {
add_group(s, group_start, s.len(), false, &offset_at, &mut groups);
text.push(PH);
pos.push(offset_at(group_start));
}
(Skel { text, pos, groups: Rc::new(groups) }, unbalanced)
}
fn add_group(
s: &[char],
start: usize,
end: usize,
closed: bool,
offset_at: &dyn Fn(usize) -> i64,
groups: &mut HashMap<i64, Group>,
) {
let inner = sub(s, start + 1, if closed { end.saturating_sub(1) } else { end });
let pos = offset_at(start);
groups.insert(
pos,
Group {
open: delimiter_of(at(s, start)),
inner: collapse_chars(inner),
raw: string(sub(s, start, end)),
pos,
},
);
}
impl Skel {
pub fn len(&self) -> usize {
self.text.len()
}
pub fn is_empty(&self) -> bool {
self.text.is_empty()
}
}
pub(crate) fn slice_skel(s: &Skel, start: usize, end: usize) -> Skel {
let a = start.min(s.text.len());
let b = end.min(s.text.len()).max(a);
Skel {
text: sub(&s.text, a, b).to_vec(),
pos: s.pos.get(a..b.min(s.pos.len())).unwrap_or(&[]).to_vec(),
groups: Rc::clone(&s.groups),
}
}
pub(crate) fn slice_from(s: &Skel, start: usize) -> Skel {
slice_skel(s, start, s.text.len())
}
pub(crate) fn trim_skel(s: &Skel) -> Skel {
let mut a = 0;
let mut b = s.text.len();
while a < b && is_whitespace(at(&s.text, a)) {
a += 1;
}
while b > a && is_whitespace(before(&s.text, b)) {
b -= 1;
}
slice_skel(s, a, b)
}
pub(crate) fn concat_skel(a: &Skel, b: &Skel) -> Skel {
let mut text = a.text.clone();
text.extend_from_slice(&b.text);
let mut pos = a.pos.clone();
pos.extend_from_slice(&b.pos);
Skel { text, pos, groups: Rc::clone(&a.groups) }
}
pub(crate) fn group_at(s: &Skel, index: usize) -> Option<&Group> {
if at(&s.text, index) != Some(PH) {
return None;
}
s.groups.get(s.pos.get(index)?)
}
pub(crate) fn groups_of(s: &Skel) -> Vec<(usize, &Group)> {
(0..s.text.len()).filter_map(|i| group_at(s, i).map(|g| (i, g))).collect()
}
pub(crate) fn has_group(s: &Skel) -> bool {
s.text.contains(&PH)
}
pub(crate) fn render_skel(s: &Skel) -> String {
let mut out = String::new();
for (i, &ch) in s.text.iter().enumerate() {
match group_at(s, i) {
Some(g) => out.push_str(&g.raw),
None => out.push(ch),
}
}
out
}
pub(crate) fn start_pos(s: &Skel, fallback: i64) -> i64 {
s.pos.first().copied().unwrap_or(fallback)
}
pub(crate) fn remove_groups(s: &Skel, remove: &HashSet<i64>) -> Skel {
if remove.is_empty() {
return s.clone();
}
let mut text = Vec::with_capacity(s.text.len());
let mut pos = Vec::with_capacity(s.pos.len());
for (&ch, &p) in s.text.iter().zip(s.pos.iter()) {
if ch == PH && remove.contains(&p) {
continue;
}
text.push(ch);
pos.push(p);
}
Skel { text, pos, groups: Rc::clone(&s.groups) }
}
pub(crate) fn is_spaced_dash_at(text: &[char], i: usize) -> bool {
at(text, i) == Some('-') && before(text, i) == Some(' ') && at(text, i + 1) == Some(' ')
}
pub(crate) fn has_spaced_dash(text: &[char]) -> bool {
(1..text.len().saturating_sub(1)).any(|i| is_spaced_dash_at(text, i))
}
#[derive(Debug, Clone)]
pub(crate) struct Segment {
pub skel: Skel,
pub start: usize,
pub end: usize,
}
fn trimmed_segment(s: &Skel, start: usize, end: usize) -> Segment {
let mut a = start;
let mut b = start.max(end);
while a < b && is_whitespace(at(&s.text, a)) {
a += 1;
}
while b > a && is_whitespace(before(&s.text, b)) {
b -= 1;
}
Segment { skel: slice_skel(s, a, b), start: a, end: b }
}
pub(crate) fn split_at_spaced_dashes(s: &Skel, allow_leading: bool) -> Vec<Segment> {
let mut segments = Vec::new();
let t = &s.text;
let mut start = 0usize;
if allow_leading && at(t, 0) == Some('-') && at(t, 1) == Some(' ') {
segments.push(Segment { skel: slice_skel(s, 0, 0), start: 0, end: 0 });
start = 2;
}
let mut i = start.max(1);
while i + 1 < t.len() {
if is_spaced_dash_at(t, i) {
segments.push(trimmed_segment(s, start, i - 1));
start = i + 2;
}
i += 1;
}
segments.push(trimmed_segment(s, start, t.len()));
segments
}