use crate::lang::ast::*;
use crate::lang::span::Span;
#[derive(Debug)]
pub enum Segment {
BlockById { kind: String, id: String },
BlockByKind { kind: String },
Attribute(String),
}
#[derive(Debug)]
pub enum Resolved<'a> {
Attribute { attr: &'a Attribute },
Block { block: &'a Block },
}
pub fn parse_path(path: &str) -> Result<Vec<Segment>, String> {
let mut segments = Vec::new();
for part in path.split('.') {
if part.is_empty() {
return Err("empty segment in path".to_string());
}
if let Some(hash_pos) = part.find('#') {
let block_type = &part[..hash_pos];
let block_id = &part[hash_pos + 1..];
if block_type.is_empty() || block_id.is_empty() {
return Err(format!("invalid block reference: {}", part));
}
segments.push(Segment::BlockById {
kind: block_type.to_string(),
id: block_id.to_string(),
});
} else if segments.is_empty() {
segments.push(Segment::BlockByKind {
kind: part.to_string(),
});
} else {
segments.push(Segment::Attribute(part.to_string()));
}
}
if segments.is_empty() {
return Err("empty path".to_string());
}
Ok(segments)
}
fn block_id(block: &Block) -> Option<&str> {
match &block.inline_id {
Some(InlineId::Literal(lit)) => Some(&lit.value),
_ => None,
}
}
pub fn resolve<'a>(doc: &'a Document, segments: &[Segment]) -> Result<Resolved<'a>, String> {
if segments.is_empty() {
return Err("empty path".to_string());
}
let top_body: Vec<&'a BodyItem> = doc
.items
.iter()
.filter_map(|item| match item {
DocItem::Body(bi) => Some(bi),
_ => None,
})
.collect();
resolve_in_body(&top_body, segments, 0)
}
fn resolve_in_body<'a>(
body: &[&'a BodyItem],
segments: &[Segment],
seg_idx: usize,
) -> Result<Resolved<'a>, String> {
let seg = &segments[seg_idx];
let is_last = seg_idx == segments.len() - 1;
match seg {
Segment::BlockById { kind, id } => {
let block = body
.iter()
.filter_map(|bi| match bi {
BodyItem::Block(b) => Some(b),
_ => None,
})
.find(|b| b.kind.name == *kind && block_id(b) == Some(id.as_str()))
.ok_or_else(|| format!("block {}#{} not found", kind, id))?;
if is_last {
Ok(Resolved::Block { block })
} else {
let children: Vec<&BodyItem> = block.body.iter().collect();
resolve_in_body(&children, segments, seg_idx + 1)
}
}
Segment::BlockByKind { kind } => {
if is_last {
if let Some(attr) = body.iter().find_map(|bi| match bi {
BodyItem::Attribute(a) if a.name.name == *kind => Some(a),
_ => None,
}) {
return Ok(Resolved::Attribute { attr });
}
}
let block = body
.iter()
.filter_map(|bi| match bi {
BodyItem::Block(b) => Some(b),
_ => None,
})
.find(|b| b.kind.name == *kind)
.ok_or_else(|| format!("block or attribute '{}' not found", kind))?;
if is_last {
Ok(Resolved::Block { block })
} else {
let children: Vec<&BodyItem> = block.body.iter().collect();
resolve_in_body(&children, segments, seg_idx + 1)
}
}
Segment::Attribute(name) => {
let attr = body
.iter()
.find_map(|bi| match bi {
BodyItem::Attribute(a) if a.name.name == *name => Some(a),
_ => None,
})
.ok_or_else(|| format!("attribute '{}' not found", name))?;
Ok(Resolved::Attribute { attr })
}
}
}
pub fn line_span_of(source: &str, span: Span) -> (usize, usize) {
let bytes = source.as_bytes();
let mut line_start = span.start;
while line_start > 0 && bytes[line_start - 1] != b'\n' {
line_start -= 1;
}
let mut line_end = span.end;
while line_end < bytes.len() && bytes[line_end] != b'\n' {
line_end += 1;
}
if line_end < bytes.len() {
line_end += 1; }
(line_start, line_end)
}
pub fn block_full_span(source: &str, block: &Block) -> (usize, usize) {
let start = if let Some(first_dec) = block.decorators.first() {
line_span_of(source, first_dec.span).0
} else {
line_span_of(source, block.span).0
};
let (_, end) = line_span_of(source, block.span);
(start, end)
}
pub fn attr_full_span(source: &str, attr: &Attribute) -> (usize, usize) {
let start = if let Some(first_dec) = attr.decorators.first() {
line_span_of(source, first_dec.span).0
} else {
line_span_of(source, attr.span).0
};
let (_, end) = line_span_of(source, attr.span);
(start, end)
}