Skip to main content

panache_parser/parser/yaml/
parser.rs

1use crate::syntax::{SyntaxKind, SyntaxNode};
2use rowan::GreenNodeBuilder;
3
4use super::model::{
5    ShadowYamlOptions, ShadowYamlOutcome, ShadowYamlReport, YamlInputKind, YamlParseReport,
6};
7
8/// Parse YAML in shadow mode using prototype groundwork only.
9///
10/// This API is intentionally read-only and does not replace production YAML
11/// parsing. By default it is disabled and reports `SkippedDisabled`.
12pub fn parse_shadow(input: &str, options: ShadowYamlOptions) -> ShadowYamlReport {
13    let line_count = input.lines().count().max(1);
14
15    if !options.enabled {
16        return ShadowYamlReport {
17            outcome: ShadowYamlOutcome::SkippedDisabled,
18            shadow_reason: "shadow-disabled",
19            input_kind: options.input_kind,
20            input_len_bytes: input.len(),
21            line_count,
22            normalized_input: None,
23        };
24    }
25
26    let normalized = match options.input_kind {
27        YamlInputKind::Plain => input.to_owned(),
28        YamlInputKind::Hashpipe => normalize_hashpipe_input(input),
29    };
30
31    let parsed = parse_yaml_tree(&normalized).is_some();
32
33    ShadowYamlReport {
34        outcome: if parsed {
35            ShadowYamlOutcome::PrototypeParsed
36        } else {
37            ShadowYamlOutcome::PrototypeRejected
38        },
39        shadow_reason: if parsed {
40            "prototype-basic-mapping-parsed"
41        } else {
42            "prototype-basic-mapping-rejected"
43        },
44        input_kind: options.input_kind,
45        input_len_bytes: input.len(),
46        line_count,
47        normalized_input: Some(normalized),
48    }
49}
50
51fn normalize_hashpipe_input(input: &str) -> String {
52    input
53        .lines()
54        .map(strip_hashpipe_prefix)
55        .collect::<Vec<_>>()
56        .join("\n")
57}
58
59fn strip_hashpipe_prefix(line: &str) -> &str {
60    if let Some(rest) = line.strip_prefix("#|") {
61        return rest.strip_prefix(' ').unwrap_or(rest);
62    }
63    line
64}
65
66/// Parse prototype YAML tree structure from input
67pub fn parse_yaml_tree(input: &str) -> Option<SyntaxNode> {
68    parse_yaml_report(input).tree
69}
70
71/// Parse prototype YAML tree structure and include diagnostics on failure.
72///
73/// Diagnostics flow through the v2-aware
74/// [`super::validator::validate_yaml`] pass, which composes per-cluster
75/// `check_*` functions covering directive ordering, structural shape
76/// (unterminated flow, trailing content, invalid keys, indent
77/// anomalies, block-scalar header, etc.), and lex-level checks like
78/// `LEX_INVALID_DOUBLE_QUOTED_ESCAPE`.
79///
80/// The returned tree, when present, comes from the v2 scanner+builder.
81pub fn parse_yaml_report(input: &str) -> YamlParseReport {
82    if let Some(err) = super::validator::validate_yaml(input) {
83        return YamlParseReport {
84            tree: None,
85            diagnostics: vec![err],
86        };
87    }
88
89    let v2_stream = super::parser_v2::parse_v2(input);
90    let mut builder = GreenNodeBuilder::new();
91    builder.start_node(SyntaxKind::DOCUMENT.into());
92    builder.start_node(SyntaxKind::YAML_METADATA_CONTENT.into());
93    let stream_green = v2_stream.green().into_owned();
94    builder.start_node(SyntaxKind::YAML_STREAM.into());
95    for child in stream_green.children() {
96        match child {
97            rowan::NodeOrToken::Node(n) => {
98                push_green_node(&mut builder, n);
99            }
100            rowan::NodeOrToken::Token(t) => {
101                builder.token(t.kind(), t.text());
102            }
103        }
104    }
105    builder.finish_node(); // YAML_STREAM
106    builder.finish_node(); // YAML_METADATA_CONTENT
107    builder.finish_node(); // DOCUMENT
108    YamlParseReport {
109        tree: Some(SyntaxNode::new_root(builder.finish())),
110        diagnostics: Vec::new(),
111    }
112}
113
114fn push_green_node(builder: &mut GreenNodeBuilder<'_>, node: &rowan::GreenNodeData) {
115    builder.start_node(node.kind());
116    for child in node.children() {
117        match child {
118            rowan::NodeOrToken::Node(n) => push_green_node(builder, n),
119            rowan::NodeOrToken::Token(t) => builder.token(t.kind(), t.text()),
120        }
121    }
122    builder.finish_node();
123}