This commit is contained in:
304
internal/parser/yamlmini.go
Normal file
304
internal/parser/yamlmini.go
Normal file
@@ -0,0 +1,304 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseYAMLDocuments(input []byte) ([]any, error) {
|
||||
raw := string(input)
|
||||
docsRaw := splitYAMLDocuments(raw)
|
||||
docs := make([]any, 0, len(docsRaw))
|
||||
for _, doc := range docsRaw {
|
||||
trimmed := strings.TrimSpace(doc)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "{") || strings.HasPrefix(trimmed, "[") {
|
||||
var parsed any
|
||||
if err := json.Unmarshal([]byte(trimmed), &parsed); err != nil {
|
||||
return nil, fmt.Errorf("json parse failed: %w", err)
|
||||
}
|
||||
docs = append(docs, parsed)
|
||||
continue
|
||||
}
|
||||
parsed, err := parseYAMLDoc(trimmed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
docs = append(docs, parsed)
|
||||
}
|
||||
return docs, nil
|
||||
}
|
||||
|
||||
func splitYAMLDocuments(input string) []string {
|
||||
lines := strings.Split(input, "\n")
|
||||
parts := []string{}
|
||||
cur := []string{}
|
||||
for _, line := range lines {
|
||||
if strings.TrimSpace(line) == "---" {
|
||||
parts = append(parts, strings.Join(cur, "\n"))
|
||||
cur = cur[:0]
|
||||
continue
|
||||
}
|
||||
cur = append(cur, line)
|
||||
}
|
||||
parts = append(parts, strings.Join(cur, "\n"))
|
||||
return parts
|
||||
}
|
||||
|
||||
func parseYAMLDoc(doc string) (any, error) {
|
||||
lines := preprocessLines(doc)
|
||||
if len(lines) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
v, next, err := parseBlock(lines, 0, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if next < len(lines) {
|
||||
return nil, fmt.Errorf("unexpected content near line %d", lines[next].num)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
type yamlLine struct {
|
||||
num int
|
||||
indent int
|
||||
text string
|
||||
}
|
||||
|
||||
func preprocessLines(doc string) []yamlLine {
|
||||
raw := strings.Split(doc, "\n")
|
||||
out := make([]yamlLine, 0, len(raw))
|
||||
for i, line := range raw {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
clean := stripInlineComment(line)
|
||||
if strings.TrimSpace(clean) == "" {
|
||||
continue
|
||||
}
|
||||
indent := countIndent(clean)
|
||||
text := strings.TrimSpace(clean)
|
||||
out = append(out, yamlLine{num: i + 1, indent: indent, text: text})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func parseBlock(lines []yamlLine, idx int, indent int) (any, int, error) {
|
||||
if idx >= len(lines) {
|
||||
return nil, idx, nil
|
||||
}
|
||||
if lines[idx].indent < indent {
|
||||
return nil, idx, nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(lines[idx].text, "- ") || lines[idx].text == "-" {
|
||||
return parseList(lines, idx, indent)
|
||||
}
|
||||
return parseMap(lines, idx, indent)
|
||||
}
|
||||
|
||||
func parseMap(lines []yamlLine, idx int, indent int) (map[string]any, int, error) {
|
||||
out := map[string]any{}
|
||||
for idx < len(lines) {
|
||||
line := lines[idx]
|
||||
if line.indent < indent {
|
||||
break
|
||||
}
|
||||
if line.indent > indent {
|
||||
break
|
||||
}
|
||||
if strings.HasPrefix(line.text, "- ") || line.text == "-" {
|
||||
break
|
||||
}
|
||||
key, value, valid, hasValue := splitKeyValue(line.text)
|
||||
if !valid {
|
||||
return nil, idx, fmt.Errorf("line %d: invalid mapping", line.num)
|
||||
}
|
||||
if hasValue {
|
||||
out[key] = parseScalar(value)
|
||||
idx++
|
||||
continue
|
||||
}
|
||||
|
||||
childIndent := indent + 2
|
||||
if idx+1 < len(lines) && lines[idx+1].indent >= indent {
|
||||
childIndent = lines[idx+1].indent
|
||||
}
|
||||
nested, next, err := parseBlock(lines, idx+1, childIndent)
|
||||
if err != nil {
|
||||
return nil, idx, err
|
||||
}
|
||||
out[key] = nested
|
||||
idx = next
|
||||
}
|
||||
return out, idx, nil
|
||||
}
|
||||
|
||||
func parseList(lines []yamlLine, idx int, indent int) ([]any, int, error) {
|
||||
out := []any{}
|
||||
for idx < len(lines) {
|
||||
line := lines[idx]
|
||||
if line.indent < indent {
|
||||
break
|
||||
}
|
||||
if line.indent > indent {
|
||||
break
|
||||
}
|
||||
if !strings.HasPrefix(line.text, "-") {
|
||||
break
|
||||
}
|
||||
itemText := strings.TrimSpace(strings.TrimPrefix(line.text, "-"))
|
||||
idx++
|
||||
if itemText == "" {
|
||||
nested, next, err := parseBlock(lines, idx, indent+2)
|
||||
if err != nil {
|
||||
return nil, idx, err
|
||||
}
|
||||
out = append(out, nested)
|
||||
idx = next
|
||||
continue
|
||||
}
|
||||
|
||||
if k, v, valid, hasValue := splitKeyValue(itemText); valid {
|
||||
m := map[string]any{}
|
||||
if hasValue {
|
||||
m[k] = parseScalar(v)
|
||||
if idx < len(lines) && lines[idx].indent > indent {
|
||||
nested, next, err := parseBlock(lines, idx, indent+2)
|
||||
if err != nil {
|
||||
return nil, idx, err
|
||||
}
|
||||
if nestedMap, ok := nested.(map[string]any); ok {
|
||||
for nk, nv := range nestedMap {
|
||||
m[nk] = nv
|
||||
}
|
||||
}
|
||||
idx = next
|
||||
}
|
||||
} else {
|
||||
childIndent := indent + 2
|
||||
if idx < len(lines) && lines[idx].indent >= indent {
|
||||
childIndent = lines[idx].indent
|
||||
}
|
||||
nested, next, err := parseBlock(lines, idx, childIndent)
|
||||
if err != nil {
|
||||
return nil, idx, err
|
||||
}
|
||||
m[k] = nested
|
||||
idx = next
|
||||
// YAML list items may continue with sibling keys after an inline key without value:
|
||||
// - hosts:
|
||||
// - example.local
|
||||
// secretName: demo-tls
|
||||
if idx < len(lines) && lines[idx].indent >= childIndent && !strings.HasPrefix(lines[idx].text, "-") {
|
||||
extra, nextMap, err := parseMap(lines, idx, childIndent)
|
||||
if err != nil {
|
||||
return nil, idx, err
|
||||
}
|
||||
for ek, ev := range extra {
|
||||
m[ek] = ev
|
||||
}
|
||||
idx = nextMap
|
||||
}
|
||||
}
|
||||
out = append(out, m)
|
||||
continue
|
||||
}
|
||||
|
||||
out = append(out, parseScalar(itemText))
|
||||
}
|
||||
return out, idx, nil
|
||||
}
|
||||
|
||||
func splitKeyValue(text string) (string, string, bool, bool) {
|
||||
parts := strings.SplitN(text, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", "", false, false
|
||||
}
|
||||
key := strings.TrimSpace(parts[0])
|
||||
value := strings.TrimSpace(parts[1])
|
||||
if key == "" {
|
||||
return "", "", false, false
|
||||
}
|
||||
if value == "" {
|
||||
return key, "", true, false
|
||||
}
|
||||
return key, value, true, true
|
||||
}
|
||||
|
||||
func parseScalar(s string) any {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(s, "\"") && strings.HasSuffix(s, "\"") && len(s) >= 2 {
|
||||
return strings.Trim(s, "\"")
|
||||
}
|
||||
if strings.HasPrefix(s, "'") && strings.HasSuffix(s, "'") && len(s) >= 2 {
|
||||
return strings.Trim(s, "'")
|
||||
}
|
||||
lower := strings.ToLower(s)
|
||||
if lower == "true" {
|
||||
return true
|
||||
}
|
||||
if lower == "false" {
|
||||
return false
|
||||
}
|
||||
if lower == "null" || s == "~" {
|
||||
return nil
|
||||
}
|
||||
if i, err := strconv.Atoi(s); err == nil {
|
||||
return i
|
||||
}
|
||||
if f, err := strconv.ParseFloat(s, 64); err == nil {
|
||||
return f
|
||||
}
|
||||
if strings.HasPrefix(s, "{") || strings.HasPrefix(s, "[") {
|
||||
var out any
|
||||
if err := json.Unmarshal([]byte(s), &out); err == nil {
|
||||
return out
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func stripInlineComment(line string) string {
|
||||
inSingle := false
|
||||
inDouble := false
|
||||
for i, r := range line {
|
||||
switch r {
|
||||
case '\'':
|
||||
if !inDouble {
|
||||
inSingle = !inSingle
|
||||
}
|
||||
case '"':
|
||||
if !inSingle {
|
||||
inDouble = !inDouble
|
||||
}
|
||||
case '#':
|
||||
if !inSingle && !inDouble {
|
||||
if i == 0 || line[i-1] == ' ' || line[i-1] == '\t' {
|
||||
return strings.TrimRight(line[:i], " \t")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
func countIndent(line string) int {
|
||||
count := 0
|
||||
for _, r := range line {
|
||||
if r == ' ' {
|
||||
count++
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return count
|
||||
}
|
||||
Reference in New Issue
Block a user