Skip to content
Open
204 changes: 204 additions & 0 deletions vlib/yaml/emit.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
module yaml

import strings

// write_spaces appends `n` spaces to `sb`.
fn write_spaces(mut sb strings.Builder, n int) {
for _ in 0 .. n {
sb.write_u8(` `)
}
}

// emit_yaml_any streams `value` into `sb` as block-style YAML.
fn emit_yaml_any(mut sb strings.Builder, value Any, indent int) {
match value {
map[string]Any { emit_yaml_map(mut sb, value, indent) }
[]Any { emit_yaml_array(mut sb, value, indent) }
else { emit_yaml_scalar(mut sb, value) }
}
}

// emit_yaml_map writes `value` as a block-style YAML mapping. An empty map
// is emitted as the inline `{}` form; otherwise each key is JSON-quoted (see
// `write_json_escaped_string`) and nested containers indent one level deeper.
fn emit_yaml_map(mut sb strings.Builder, value map[string]Any, indent int) {
if value.len == 0 {
sb.write_string('{}')
return
}
mut first := true
for key, item in value {
if !first {
sb.write_u8(`\n`)
}
first = false
write_spaces(mut sb, indent)
write_json_escaped_string(mut sb, key)
match item {
map[string]Any, []Any {
sb.write_u8(`:`)
sb.write_u8(`\n`)
emit_yaml_any(mut sb, item, indent + 2)
}
else {
sb.write_string(': ')
emit_yaml_scalar(mut sb, item)
}
}
}
}

// emit_yaml_array writes `value` as a block-style YAML sequence. An empty
// array is emitted as the inline `[]` form; otherwise each item is prefixed
// with `- ` and nested containers indent one level deeper.
fn emit_yaml_array(mut sb strings.Builder, value []Any, indent int) {
if value.len == 0 {
sb.write_string('[]')
return
}
mut first := true
for item in value {
if !first {
sb.write_u8(`\n`)
}
first = false
write_spaces(mut sb, indent)
match item {
map[string]Any, []Any {
sb.write_u8(`-`)
sb.write_u8(`\n`)
emit_yaml_any(mut sb, item, indent + 2)
}
else {
sb.write_string('- ')
emit_yaml_scalar(mut sb, item)
}
}
}
}

// emit_yaml_scalar writes `value` as a single YAML scalar token: strings go
// through `write_json_escaped_string`, booleans / numbers / null print their
// literal form. The container branch is type-required by V's exhaustive
// `match` over `Any` but is unreachable: `emit_yaml_any` routes maps and
// arrays to their dedicated emitters before falling back here.
fn emit_yaml_scalar(mut sb strings.Builder, value Any) {
match value {
string { write_json_escaped_string(mut sb, value) }
bool { sb.write_string(if value { 'true' } else { 'false' }) }
f64 { sb.write_string(value.str()) }
i64 { sb.write_string(value.str()) }
int { sb.write_string(value.str()) }
u64 { sb.write_string(value.str()) }
Null { sb.write_string('null') }
[]Any, map[string]Any { emit_yaml_any(mut sb, value, 0) }
}
}

// write_json_escaped_string writes `value` as a JSON string literal directly
// into `sb`. Matches `json2.encode`'s rules: standard short escapes for control
// chars, `\u00XX` for the rest below 0x20, and UTF-8 bytes passed through
// verbatim (no per-byte `\uXXXX` re-escape). Passes safe runs through in bulk
// via `write_string` on the original slice — the overwhelmingly common case for
// human YAML — and only switches to per-byte handling at escape boundaries.
fn write_json_escaped_string(mut sb strings.Builder, value string) {
sb.write_u8(`"`)
mut start := 0
for i := 0; i < value.len; i++ {
c := value[i]
if c >= 0x20 && c != `"` && c != `\\` {
continue
}
if start < i {
sb.write_string(value[start..i])
}
match c {
`"` {
sb.write_string('\\"')
}
`\\` {
sb.write_string('\\\\')
}
`\n` {
sb.write_string('\\n')
}
`\r` {
sb.write_string('\\r')
}
`\t` {
sb.write_string('\\t')
}
`\b` {
sb.write_string('\\b')
}
`\f` {
sb.write_string('\\f')
}
else {
sb.write_string('\\u00')
hex := '0123456789abcdef'
sb.write_u8(hex[(c >> 4) & 0xf])
sb.write_u8(hex[c & 0xf])
}
}

start = i + 1
}
if start < value.len {
sb.write_string(value[start..])
}
sb.write_u8(`"`)
}

// emit_any_as_json writes `a` as a compact JSON document.
fn emit_any_as_json(mut sb strings.Builder, a Any) {
match a {
map[string]Any {
sb.write_u8(`{`)
mut first := true
for key, value in a {
if !first {
sb.write_u8(`,`)
}
first = false
write_json_escaped_string(mut sb, key)
sb.write_u8(`:`)
emit_any_as_json(mut sb, value)
}
sb.write_u8(`}`)
}
[]Any {
sb.write_u8(`[`)
mut first := true
for value in a {
if !first {
sb.write_u8(`,`)
}
first = false
emit_any_as_json(mut sb, value)
}
sb.write_u8(`]`)
}
string {
write_json_escaped_string(mut sb, a)
}
bool {
sb.write_string(if a { 'true' } else { 'false' })
}
f64 {
sb.write_string(a.str())
}
i64 {
sb.write_string(a.str())
}
int {
sb.write_string(a.str())
}
u64 {
sb.write_string(a.str())
}
Null {
sb.write_string('null')
}
}
}
180 changes: 180 additions & 0 deletions vlib/yaml/flow.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
module yaml

struct FlowParser {
src string
mut:
pos int
}

fn parse_flow_value(src string) !Any {
mut parser := FlowParser{
src: src
}
value := parser.parse_value()!
parser.skip_space()
if parser.pos != parser.src.len {
return error('yaml: unexpected trailing flow content')
}
return value
}

fn (mut p FlowParser) parse_value() !Any {
p.skip_space()
if p.pos >= p.src.len {
return error('yaml: unexpected end of flow value')
}
return match p.src[p.pos] {
`[` { p.parse_array() }
`{` { p.parse_object() }
`"`, `'` { Any(parse_quoted_flow_string(mut p)!) }
else { parse_scalar(p.parse_plain_token()) }
}
}

fn (mut p FlowParser) parse_array() !Any {
p.pos++
mut items := []Any{}
for {
p.skip_space()
if p.pos >= p.src.len {
return error('yaml: unterminated flow array')
}
if p.src[p.pos] == `]` {
p.pos++
break
}
items << p.parse_value()!
p.skip_space()
if p.pos >= p.src.len {
return error('yaml: unterminated flow array')
}
if p.src[p.pos] == `,` {
p.pos++
continue
}
if p.src[p.pos] == `]` {
p.pos++
break
}
return error('yaml: expected `,` or `]` in flow array')
}
return Any(items)
}

fn (mut p FlowParser) parse_object() !Any {
p.pos++
mut result := map[string]Any{}
for {
p.skip_space()
if p.pos >= p.src.len {
return error('yaml: unterminated flow object')
}
if p.src[p.pos] == `}` {
p.pos++
break
}
key := p.parse_key()!
p.skip_space()
if p.pos >= p.src.len || p.src[p.pos] != `:` {
return error('yaml: expected `:` in flow object')
}
p.pos++
result[key] = p.parse_value()!
p.skip_space()
if p.pos >= p.src.len {
return error('yaml: unterminated flow object')
}
if p.src[p.pos] == `,` {
p.pos++
continue
}
if p.src[p.pos] == `}` {
p.pos++
break
}
return error('yaml: expected `,` or `}` in flow object')
}
return Any(result)
}

fn (mut p FlowParser) parse_key() !string {
p.skip_space()
if p.pos >= p.src.len {
return error('yaml: unexpected end of flow key')
}
if p.src[p.pos] in [`"`, `'`] {
return parse_quoted_flow_string(mut p)
}
start := p.pos
for p.pos < p.src.len {
ch := p.src[p.pos]
if ch == `:` {
break
}
p.pos++
}
return p.src[start..p.pos].trim_space()
}

fn (mut p FlowParser) parse_plain_token() string {
start := p.pos
mut bracket_depth := 0
mut brace_depth := 0
for p.pos < p.src.len {
ch := p.src[p.pos]
if ch == `[` {
bracket_depth++
} else if ch == `]` {
if bracket_depth == 0 {
break
}
bracket_depth--
} else if ch == `{` {
brace_depth++
} else if ch == `}` {
if brace_depth == 0 {
break
}
brace_depth--
} else if ch == `,` && bracket_depth == 0 && brace_depth == 0 {
break
}
p.pos++
}
return p.src[start..p.pos].trim_space()
}

fn (mut p FlowParser) skip_space() {
for p.pos < p.src.len && p.src[p.pos].is_space() {
p.pos++
}
}

fn parse_quoted_flow_string(mut p FlowParser) !string {
start := p.pos
quote := p.src[p.pos]
p.pos++
mut escape := false
for p.pos < p.src.len {
ch := p.src[p.pos]
if quote == `"` {
if escape {
escape = false
} else if ch == `\\` {
escape = true
} else if ch == `"` {
p.pos++
return parse_quoted_string(p.src[start..p.pos])
}
} else if ch == `'` {
if p.pos + 1 < p.src.len && p.src[p.pos + 1] == `'` {
p.pos += 2
continue
}
p.pos++
return parse_quoted_string(p.src[start..p.pos])
}
p.pos++
}
return error('yaml: unterminated quoted flow string')
}
Loading
Loading