use std::collections::HashMap;
use crate::actions::{ActionToken, TokenKind};
use crate::cst::{
BlockScalar, CommentLine, ControlBranch, ControlKind, ControlRegion, MappingEntry, Node,
OpaqueKind, OpaqueNode, OutputAction, ScalarLine, ScalarPart, ScalarParts, SequenceItem, Span,
TemplatedDocument,
};
use crate::lines::LineIndex;
use crate::yaml_scan::{structural_mapping_colon, unquote_yaml_scalar};
#[derive(Debug)]
pub(crate) struct Frame {
pub(crate) parent: Option<usize>,
pub(crate) indent: usize,
pub(crate) opened_empty: bool,
pub(crate) block: bool,
pub(crate) marked_at: Option<usize>,
}
pub(crate) fn parse_document(source: &str, tokens: Vec<ActionToken>) -> TemplatedDocument<'_> {
let lines = LineIndex::new(source);
let parser = Parser {
source,
tokens,
frames: Vec::new(),
head: None,
owners: vec![OwnerFrame::root()],
region_modes: HashMap::new(),
next_token: 0,
};
parser.run(&lines)
}
enum RegionMode {
Structured,
Consumed,
}
struct OwnerFrame {
children: Vec<Node>,
data: OwnerData,
}
impl OwnerFrame {
fn root() -> Self {
Self {
children: Vec::new(),
data: OwnerData::Root,
}
}
fn container(data: OwnerData) -> Self {
Self {
children: Vec::new(),
data,
}
}
}
enum OwnerData {
Root,
Entry(EntrySeed),
Item(ItemSeed),
Branch(BranchSeed),
}
struct EntrySeed {
frame: usize,
span: Span,
indent: usize,
key: ScalarParts,
value: Option<ScalarParts>,
block: Option<BlockSeed>,
}
struct ItemSeed {
frame: usize,
span: Span,
indent: usize,
value: Option<ScalarParts>,
block: Option<BlockSeed>,
}
struct BlockSeed {
header: Span,
body: Option<Span>,
}
struct BranchSeed {
region: usize,
builder: RegionBuilder,
header: Span,
}
struct RegionBuilder {
kind: ControlKind,
span: Span,
branches: Vec<ControlBranch>,
well_nested: bool,
}
struct Parser<'src> {
source: &'src str,
tokens: Vec<ActionToken>,
frames: Vec<Frame>,
head: Option<usize>,
owners: Vec<OwnerFrame>,
region_modes: HashMap<usize, RegionMode>,
next_token: usize,
}
impl<'src> Parser<'src> {
fn run(mut self, lines: &LineIndex) -> TemplatedDocument<'src> {
for line in 0..lines.count() {
let (ls, le) = lines.span(line);
let followed_by_newline = line + 1 < lines.count();
self.process_line(ls, le, followed_by_newline);
}
self.close_all();
let roots = self
.owners
.pop()
.map(|owner| owner.children)
.unwrap_or_default();
TemplatedDocument {
source: self.source,
roots,
document_spans: document_spans(self.source),
}
}
fn process_line(&mut self, ls: usize, le: usize, followed_by_newline: bool) {
let raw = self.source.get(ls..le).unwrap_or_default();
let replay = if followed_by_newline {
raw.strip_suffix('\r').unwrap_or(raw)
} else {
raw
};
let trimmed = replay.trim_start();
let indent = replay.len() - trimmed.len();
if trimmed.is_empty() {
return;
}
if self
.head
.and_then(|head| self.frames.get(head))
.is_some_and(|frame| frame.block && indent > frame.indent)
{
self.extend_block_body(ls, le);
self.consume_line_tokens(le, false);
return;
}
if trimmed.starts_with('#') {
let content = self.parts_for_span(ls + indent, le);
self.consume_line_tokens(le, false);
self.attach(Node::Comment(CommentLine {
span: Span::new(ls + indent, le),
content,
}));
return;
}
if trimmed.starts_with("{{") {
self.process_action_line(ls, le);
return;
}
self.process_content_line(ls, le, trimmed, indent);
}
fn process_content_line(&mut self, ls: usize, le: usize, trimmed: &str, indent: usize) {
if let Some(after_dash) = trimmed.strip_prefix('-') {
self.seq_pop(indent);
self.consume_line_tokens(le, true);
if !after_dash.is_empty() && !after_dash.starts_with(char::is_whitespace) {
let content = self.parts_for_span(ls + indent, le);
self.attach(Node::Scalar(ScalarLine {
span: Span::new(ls + indent, le),
indent,
content,
}));
return;
}
self.mark(indent, ls);
self.sequence_item_line(ls, le, after_dash, indent);
return;
}
self.pop(indent);
self.consume_line_tokens(le, true);
self.mark(indent, ls);
self.entry_line(trimmed, indent, ls + indent, le);
}
fn sequence_item_line(&mut self, ls: usize, le: usize, after_dash: &str, indent: usize) {
let nested = after_dash.trim_start();
let nested_start = ls + indent + 1 + (after_dash.len() - nested.len());
let item_block = nested.starts_with('|') || nested.starts_with('>');
let frame = self.push_frame(indent, false, item_block);
let mut seed = ItemSeed {
frame,
span: Span::new(ls + indent, le),
indent,
value: None,
block: item_block.then(|| BlockSeed {
header: Span::new(nested_start, nested_start + nested.len()),
body: None,
}),
};
if !nested.is_empty() && !item_block {
if structural_mapping_colon(nested).is_some() {
self.owners
.push(OwnerFrame::container(OwnerData::Item(seed)));
self.entry_line(nested, indent + 2, nested_start, le);
return;
}
seed.value = Some(self.parts_for_span(nested_start, nested_start + nested.len()));
}
self.owners
.push(OwnerFrame::container(OwnerData::Item(seed)));
}
fn entry_line(&mut self, text: &str, eff_indent: usize, text_start: usize, le: usize) {
let Some(colon) = structural_mapping_colon(text) else {
let content = self.parts_for_span(text_start, text_start + text.len());
self.attach(Node::Scalar(ScalarLine {
span: Span::new(text_start, le),
indent: eff_indent,
content,
}));
return;
};
let value_text = &text[colon + 1..];
let value = value_text.trim();
let block = value.starts_with('|') || value.starts_with('>');
let template_value = value.contains("{{");
let key_trimmed = text[..colon].trim_end();
let key_text = unquote_yaml_scalar(key_trimmed);
let key_invalid = key_text.is_empty() || key_text.contains("{{") || key_text.contains("}}");
let key = self.parts_for_span(text_start, text_start + key_trimmed.len());
let leading = value_text.len() - value_text.trim_start().len();
let value_start = text_start + colon + 1 + leading;
let value_parts = (!value.is_empty() && !block)
.then(|| self.parts_for_span(value_start, value_start + value.len()));
let closed = !value.is_empty() && !block && !template_value;
if closed || key_invalid {
self.attach(Node::Mapping(MappingEntry {
span: Span::new(text_start, le),
indent: eff_indent,
key,
value: value_parts,
block: None,
opens_scope: false,
children: Vec::new(),
}));
return;
}
let frame = self.push_frame(eff_indent, value.is_empty(), block);
self.owners
.push(OwnerFrame::container(OwnerData::Entry(EntrySeed {
frame,
span: Span::new(text_start, le),
indent: eff_indent,
key,
value: value_parts,
block: block.then(|| BlockSeed {
header: Span::new(value_start, value_start + value.len()),
body: None,
}),
})));
}
fn process_action_line(&mut self, ls: usize, le: usize) {
let mut pos = ls;
while let Some(token) = self
.tokens
.get(self.next_token)
.copied()
.filter(|token| token.span.start < le)
{
self.next_token += 1;
self.attach_gap_text(pos, token.span.start.min(le));
pos = pos.max(token.span.end);
self.handle_token_structural(token);
}
self.attach_gap_text(pos, le);
}
fn attach_gap_text(&mut self, start: usize, end: usize) {
if start >= end {
return;
}
let text = &self.source[start..end];
if text.trim().is_empty() {
return;
}
let lead = text.len() - text.trim_start().len();
let content_start = start + lead;
let content_end = content_start + text.trim().len();
self.attach(Node::Opaque(OpaqueNode {
span: Span::new(content_start, content_end),
kind: OpaqueKind::ActionLineText,
}));
}
fn handle_token_structural(&mut self, token: ActionToken) {
match token.kind {
TokenKind::Output { expr_span } => self.attach(Node::Output(OutputAction {
span: token.span,
expr_span,
})),
TokenKind::Assign => self.attach_opaque(token.span, OpaqueKind::Assignment),
TokenKind::TemplateComment => {
self.attach_opaque(token.span, OpaqueKind::TemplateComment);
}
TokenKind::Break => self.attach_opaque(token.span, OpaqueKind::Break),
TokenKind::Continue => self.attach_opaque(token.span, OpaqueKind::Continue),
TokenKind::Error => self.attach_opaque(token.span, OpaqueKind::ParseError),
TokenKind::RegionOpen {
region,
kind,
region_end,
} => {
self.region_modes.insert(region, RegionMode::Structured);
self.owners.push(OwnerFrame {
children: Vec::new(),
data: OwnerData::Branch(BranchSeed {
region,
builder: RegionBuilder {
kind,
span: Span::new(token.span.start, region_end),
branches: Vec::new(),
well_nested: true,
},
header: token.span,
}),
});
}
TokenKind::RegionBranch { region } => self.rotate_branch(region, token.span, true),
TokenKind::RegionEnd { region } => self.end_region(region, true),
}
}
fn rotate_branch(&mut self, region: usize, header: Span, clean_boundary: bool) {
if let Some((mut seed, _)) = self.close_branch(region, clean_boundary) {
seed.header = header;
self.owners.push(OwnerFrame {
children: Vec::new(),
data: OwnerData::Branch(seed),
});
}
}
fn end_region(&mut self, region: usize, clean_boundary: bool) {
if let Some((seed, index)) = self.close_branch(region, clean_boundary) {
self.region_modes.remove(®ion);
self.attach_at(
index.saturating_sub(1),
Node::Control(ControlRegion {
kind: seed.builder.kind,
span: seed.builder.span,
branches: seed.builder.branches,
well_nested: seed.builder.well_nested,
}),
);
}
}
fn close_branch(&mut self, region: usize, clean_boundary: bool) -> Option<(BranchSeed, usize)> {
if !matches!(self.region_modes.get(®ion), Some(RegionMode::Structured)) {
return None;
}
let index = self.owners.iter().rposition(
|owner| matches!(&owner.data, OwnerData::Branch(seed) if seed.region == region),
)?;
let escaped = self.owners.len() - 1 - index;
let owner = self.owners.remove(index);
let OwnerData::Branch(mut seed) = owner.data else {
return None;
};
if escaped > 0 || !clean_boundary {
seed.builder.well_nested = false;
}
seed.builder.branches.push(ControlBranch {
header: seed.header,
body: owner.children,
});
Some((seed, index))
}
fn consume_line_tokens(&mut self, le: usize, opaque_inline_regions: bool) {
while let Some(token) = self
.tokens
.get(self.next_token)
.copied()
.filter(|token| token.span.start < le)
{
self.next_token += 1;
match token.kind {
TokenKind::RegionOpen {
region, region_end, ..
} => {
self.region_modes.insert(region, RegionMode::Consumed);
if opaque_inline_regions {
self.attach(Node::Opaque(OpaqueNode {
span: Span::new(token.span.start, region_end),
kind: OpaqueKind::InlineRegion,
}));
}
}
TokenKind::RegionBranch { region } => self.rotate_branch(region, token.span, false),
TokenKind::RegionEnd { region } => self.end_region(region, false),
_ => {}
}
}
}
fn extend_block_body(&mut self, ls: usize, le: usize) {
for owner in self.owners.iter_mut().rev() {
let block = match &mut owner.data {
OwnerData::Entry(seed) => seed.block.as_mut(),
OwnerData::Item(seed) => seed.block.as_mut(),
OwnerData::Root | OwnerData::Branch(_) => continue,
};
if let Some(block) = block {
let start = block.body.map_or(ls, |span| span.start);
block.body = Some(Span::new(start, le));
}
return;
}
}
fn push_frame(&mut self, indent: usize, opened_empty: bool, block: bool) -> usize {
let id = self.frames.len();
self.frames.push(Frame {
parent: self.head,
indent,
opened_empty,
block,
marked_at: None,
});
self.head = Some(id);
id
}
fn pop(&mut self, indent: usize) {
while let Some(head) = self.head {
if self
.frames
.get(head)
.is_some_and(|frame| frame.indent >= indent)
{
self.close_container();
} else {
break;
}
}
}
fn seq_pop(&mut self, indent: usize) {
while let Some(head) = self.head {
let Some(frame) = self.frames.get(head) else {
self.head = None;
break;
};
let allow = frame.opened_empty && frame.marked_at.is_none();
if frame.indent > indent || (frame.indent == indent && !allow) {
self.close_container();
} else {
break;
}
}
}
fn mark(&mut self, indent: usize, ls: usize) {
let mut current = self.head;
while let Some(id) = current {
let Some(frame) = self.frames.get_mut(id) else {
self.head = None;
return;
};
if frame.indent < indent {
if frame.marked_at.is_none() {
frame.marked_at = Some(ls);
}
return;
}
current = frame.parent;
}
}
fn close_container(&mut self) {
let Some(index) = self
.owners
.iter()
.rposition(|owner| matches!(owner.data, OwnerData::Entry(_) | OwnerData::Item(_)))
else {
self.head = None;
return;
};
let owner = self.owners.remove(index);
let children = owner.children;
let (node, frame) = match owner.data {
OwnerData::Entry(seed) => (
Node::Mapping(MappingEntry {
span: seed.span,
indent: seed.indent,
key: seed.key,
value: seed.value,
block: seed.block.map(|block| self.finish_block(&block)),
opens_scope: true,
children,
}),
seed.frame,
),
OwnerData::Item(seed) => (
Node::Sequence(SequenceItem {
span: seed.span,
indent: seed.indent,
value: seed.value,
block: seed.block.map(|block| self.finish_block(&block)),
children,
}),
seed.frame,
),
OwnerData::Root | OwnerData::Branch(_) => {
self.head = None;
return;
}
};
self.head = self.frames.get(frame).and_then(|frame| frame.parent);
self.attach_at(index.saturating_sub(1), node);
}
fn finish_block(&self, seed: &BlockSeed) -> BlockScalar {
let body = seed
.body
.unwrap_or(Span::new(seed.header.end, seed.header.end));
let holes = self
.tokens
.iter()
.filter(|token| token.span.start >= body.start && token.span.start < body.end)
.map(|token| token.span)
.collect();
BlockScalar {
header: seed.header,
body,
holes,
}
}
fn close_all(&mut self) {
while self.owners.len() > 1 {
let top = self.owners.len() - 1;
let Some(owner) = self.owners.get(top) else {
break;
};
match &owner.data {
OwnerData::Entry(_) | OwnerData::Item(_) => self.close_container(),
OwnerData::Branch(seed) => {
let region = seed.region;
self.end_region(region, false);
}
OwnerData::Root => break,
}
}
}
fn attach(&mut self, node: Node) {
let index = self.owners.len() - 1;
self.attach_at(index, node);
}
fn attach_at(&mut self, index: usize, node: Node) {
if let Some(owner) = self.owners.get_mut(index) {
owner.children.push(node);
}
}
fn attach_opaque(&mut self, span: Span, kind: OpaqueKind) {
self.attach(Node::Opaque(OpaqueNode { span, kind }));
}
fn parts_for_span(&self, start: usize, end: usize) -> ScalarParts {
let mut parts = Vec::new();
let mut pos = start;
let first = self.tokens.partition_point(|token| token.span.end <= start);
for token in self.tokens.get(first..).unwrap_or_default() {
if token.span.start >= end {
break;
}
if token.span.start > pos {
parts.push(ScalarPart::Text(Span::new(pos, token.span.start)));
}
parts.push(ScalarPart::Hole(token.span));
pos = pos.max(token.span.end);
}
if pos < end {
parts.push(ScalarPart::Text(Span::new(pos, end)));
}
ScalarParts {
span: Span::new(start, end),
parts,
}
}
}
fn document_spans(source: &str) -> Vec<Span> {
let mut spans = Vec::new();
let mut start = 0usize;
let mut byte = 0usize;
for line in source.split_inclusive('\n') {
if line.trim() == "---" {
if start < byte {
spans.push(Span::new(start, byte));
}
start = byte + line.len();
}
byte += line.len();
}
if start < source.len() {
spans.push(Span::new(start, source.len()));
}
spans
}