///|
priv struct Line {
number : Int32
indent : Int32
has_tab_indent : Bool
raw : String
content : String
}
///|
fn block_indent(line : Line) -> Int32 {
if line.has_tab_indent && line.indent > 0 {
line.indent + 1
} else {
line.indent
}
}
///|
fn block_content(line : Line) -> String {
if line.has_tab_indent && line.indent > 0 {
let raw = line.raw.view(start_offset=line.indent + 1).to_string()
let stripped = strip_inline_comment(raw)
stripped.trim_end().to_string()
} else {
line.content
}
}
///|
fn drop_first_char(text : String) -> String {
let chars = text.to_array()
if chars.length() <= 1 {
""
} else {
text.view(start_offset=1).to_string()
}
}
///|
fn make_virtual_line(number : Int32, indent : Int32, content : String) -> Line {
let sb = StringBuilder::new()
for _ in 0.. Int32 {
let next_index = next_non_empty(lines, line_index + 1)
if next_index < lines.length() {
let next_line = lines[next_index]
let next_indent = block_indent(next_line)
if next_indent > parent_indent &&
next_indent > default_indent &&
is_seq_line(next_line, next_indent) {
return next_indent
}
}
default_indent
}
///|
fn inline_virtual_indent_map(
lines : Array[Line],
line_index : Int32,
parent_indent : Int32,
default_indent : Int32,
) -> Int32 {
let next_index = next_non_empty(lines, line_index + 1)
if next_index < lines.length() {
let next_line = lines[next_index]
let next_indent = block_indent(next_line)
if next_indent > parent_indent &&
next_indent > default_indent &&
is_map_line(next_line, next_indent) {
return next_indent
}
}
default_indent
}
///|
priv enum Chomp {
Clip
Strip
Keep
}
///|
fn parse_block_document(
ctx : TagContext,
input : String,
) -> Yaml raise ParseError {
let lines = collect_lines(input)
let start = next_non_empty(lines, 0)
if start >= lines.length() {
return Yaml::Null
}
let first = lines[start]
let (value, next) = parse_block_node(ctx, lines, start, first.indent, true)
let rest = next_non_empty(lines, next)
if rest < lines.length() {
let line = lines[rest]
raise ParseError::UnexpectedChar(
pos={ line: line.number, column: line.indent + 1, },
found='-',
)
}
value
}
///|
fn parse_block_node(
ctx : TagContext,
lines : Array[Line],
index : Int32,
indent : Int32,
allow_same_indent_props : Bool,
) -> (Yaml, Int32) raise ParseError {
let start = next_non_empty(lines, index)
if start >= lines.length() {
let pos = match lines.last() {
None => { line: 1, column: 1, }
Some(last) => { line: last.number, column: last.indent + 1, }
}
raise ParseError::UnexpectedEof(pos~)
}
let line = lines[start]
if line.has_tab_indent && line.indent == 0 {
raise ParseError::InvalidIndent(pos={
line: line.number,
column: line.indent + 1,
})
}
let line_indent = block_indent(line)
let line_content = block_content(line)
if indicator_has_tab(line_content, '?') ||
indicator_has_tab(line_content, ':') {
raise ParseError::InvalidIndent(pos={
line: line.number,
column: line.indent + 1,
})
}
if line_indent < indent {
raise ParseError::InvalidIndent(pos={
line: line.number,
column: line.indent + 1,
})
}
if is_seq_line(line, indent) {
parse_block_seq(ctx, lines, start, indent)
} else if is_map_start(lines, start, indent) {
parse_block_map(ctx, lines, start, indent)
} else {
parse_value_from_text(
ctx, lines, start, indent, line_content, allow_same_indent_props,
)
}
}
///|
fn next_non_empty(lines : Array[Line], index : Int32) -> Int32 {
let mut i = index
while i < lines.length() && lines[i].content is "" {
i = i + 1
}
i
}
///|
fn parse_block_seq(
ctx : TagContext,
lines : Array[Line],
index : Int32,
indent : Int32,
) -> (Yaml, Int32) raise ParseError {
let items : Array[Yaml] = []
let mut i = index
while i < lines.length() {
i = next_non_empty(lines, i)
if i >= lines.length() {
break
}
let line = lines[i]
let line_indent = block_indent(line)
let line_content = block_content(line)
if line.has_tab_indent && line.indent == 0 && !(line_content is "") {
raise ParseError::InvalidIndent(pos={
line: line.number,
column: line.indent + 1,
})
}
if line_indent < indent {
break
}
if line_indent != indent || !is_seq_line(line, indent) {
break
}
let (sep_has_tab, rest) = split_indicator_rest(line_content, '-')
if sep_has_tab && starts_with_block_indicator(rest) {
raise ParseError::InvalidIndent(pos={
line: line.number,
column: line.indent + 1,
})
}
if rest is "" {
let next_index = next_non_empty(lines, i + 1)
if next_index >= lines.length() ||
block_indent(lines[next_index]) <= indent {
items.push(Yaml::Null)
i = next_index
} else {
let child_indent = block_indent(lines[next_index])
let (value, next) = parse_block_node(
ctx, lines, next_index, child_indent, true,
)
items.push(value)
i = next
}
} else {
match try_parse_block_scalar(ctx, rest, lines, i + 1, indent) {
Some((node, next)) => {
items.push(node)
i = next
}
None =>
match parse_props_only(ctx, rest) {
Some((tag, anchor)) => {
let next_index = next_non_empty(lines, i + 1)
if next_index < lines.length() &&
block_indent(lines[next_index]) > indent {
let child_indent = block_indent(lines[next_index])
let (value, next) = parse_block_node(
ctx, lines, next_index, child_indent, true,
)
items.push(
apply_node_props(value, tag, anchor, ctx.coerce_binary),
)
i = next
} else {
items.push(
apply_node_props(Yaml::Null, tag, anchor, ctx.coerce_binary),
)
i = i + 1
}
}
None =>
if rest is "-" || rest.has_prefix("- ") {
let virtual_indent = inline_virtual_indent_seq(
lines,
i,
indent,
indent + 2,
)
let virtual_line = make_virtual_line(
line.number,
virtual_indent,
rest,
)
let temp : Array[Line] = [virtual_line]
for j in (i + 1).. (Yaml, Int32) raise ParseError {
let items : Array[(Yaml, Yaml)] = []
let mut i = index
while i < lines.length() {
i = next_non_empty(lines, i)
if i >= lines.length() {
break
}
let line = lines[i]
let line_indent = block_indent(line)
let line_content = block_content(line)
if line.has_tab_indent && line.indent == 0 {
raise ParseError::InvalidIndent(pos={
line: line.number,
column: line.indent + 1,
})
}
if indicator_has_tab(line_content, '?') ||
indicator_has_tab(line_content, ':') {
raise ParseError::InvalidIndent(pos={
line: line.number,
column: line.indent + 1,
})
}
if line_indent < indent {
break
}
if line_indent != indent {
break
}
if is_explicit_key_line(line, indent) {
let (entry, next) = parse_explicit_entry(ctx, lines, i, indent)
items.push(entry)
i = next
} else if split_key_value_opt(line_content) is Some(_) {
let (key_text, value_text) = split_key_value(line)
let key = parse_inline_key(ctx, key_text)
let trimmed_value = value_text.trim_start().to_string()
if trimmed_value.has_prefix("- ") {
raise ParseError::InvalidIndent(pos={
line: line.number,
column: line.indent + 1,
})
}
if is_plain_scalar_start(trimmed_value) &&
has_plain_key_indicator(trimmed_value) {
raise ParseError::UnexpectedChar(
pos={ line: line.number, column: line.indent + 1, },
found=':',
)
}
let (value, next) = parse_value_from_text(
ctx, lines, i, indent, value_text, false,
)
items.push((key, value))
i = next
} else if is_value_line_after(lines, i, indent) {
let key = parse_inline_key(ctx, line_content)
let value_line = next_non_empty(lines, i + 1)
let value_text = drop_first_char(block_content(lines[value_line]))
let (value, next) = parse_value_from_text(
ctx, lines, value_line, indent, value_text, false,
)
items.push((key, value))
i = next
} else if !is_map_line(line, indent) {
break
}
}
(Yaml::Map(items), i)
}
///|
fn parse_inline_value(ctx : TagContext, text : String) -> Yaml raise ParseError {
let trimmed = text.trim().to_string()
if trimmed is "" {
return Yaml::Null
}
let scanner = Scanner::new(trimmed)
scanner.skip_ws_and_comments()
match parse_alias(scanner) {
Some(name) => Yaml::Alias(name)
None => {
let (tag, anchor) = parse_node_props(ctx, scanner)
scanner.skip_ws_and_comments()
if (tag is Some(_) || anchor is Some(_)) && scanner.peek() is Some('*') {
raise ParseError::InvalidTagValue(tag="alias", value="*")
}
let rest_pos = scanner.position()
let rest = trimmed
.view(start_offset=scanner.offset())
.to_string()
.trim_start()
.to_string()
let has_props = tag is Some(_) || anchor is Some(_)
let value = if rest is "" {
Yaml::Null
} else if rest.has_prefix("[") || rest.has_prefix("{") {
parse_flow_value_from_string(ctx, rest)
} else if rest.has_prefix("\"") || rest.has_prefix("'") {
parse_flow_value_from_string(ctx, rest)
} else {
if has_props && (rest.has_prefix("- ") || rest.has_prefix("-\t")) {
raise ParseError::UnexpectedChar(pos=rest_pos, found='-')
}
if has_props && rest.has_prefix(",") {
raise ParseError::UnexpectedChar(pos=rest_pos, found=',')
}
parse_scalar_value(rest)
}
apply_node_props(value, tag, anchor, ctx.coerce_binary)
}
}
}
///|
fn parse_inline_node_multiline(
ctx : TagContext,
lines : Array[Line],
line_index : Int32,
indent : Int32,
text : String,
allow_same_indent_props : Bool,
) -> (Yaml, Int32) raise ParseError {
let trimmed = text.trim().to_string()
let mut inline_comment = false
if line_index < lines.length() {
let raw_line = lines[line_index].raw
.view(start_offset=lines[line_index].indent)
.to_string()
let stripped = strip_inline_comment(raw_line)
if stripped.length() < raw_line.length() {
inline_comment = true
}
}
if trimmed is "" {
return (Yaml::Null, line_index + 1)
}
let scanner = Scanner::new(trimmed)
scanner.skip_ws_and_comments()
match parse_alias(scanner) {
Some(name) => (Yaml::Alias(name), line_index + 1)
None => {
let (tag, anchor) = parse_node_props(ctx, scanner)
scanner.skip_ws_and_comments()
if (tag is Some(_) || anchor is Some(_)) && scanner.peek() is Some('*') {
raise ParseError::InvalidTagValue(tag="alias", value="*")
}
let rest_pos = scanner.position()
let rest = trimmed
.view(start_offset=scanner.offset())
.to_string()
.trim_start()
.to_string()
let has_props = tag is Some(_) || anchor is Some(_)
if rest is "" {
let value = Yaml::Null
(
apply_node_props(value, tag, anchor, ctx.coerce_binary),
line_index + 1,
)
} else if rest.has_prefix("[") || rest.has_prefix("{") {
let value = parse_flow_value_from_string(ctx, rest)
(
apply_node_props(value, tag, anchor, ctx.coerce_binary),
line_index + 1,
)
} else if rest.has_prefix("\"") || rest.has_prefix("'") {
let value = parse_flow_value_from_string(ctx, rest)
(
apply_node_props(value, tag, anchor, ctx.coerce_binary),
line_index + 1,
)
} else if is_plain_scalar_start(rest) {
if has_props && (rest.has_prefix("- ") || rest.has_prefix("-\t")) {
raise ParseError::UnexpectedChar(pos=rest_pos, found='-')
}
if has_props && rest.has_prefix(",") {
raise ParseError::UnexpectedChar(pos=rest_pos, found=',')
}
if inline_comment {
let value = parse_scalar_value(rest)
let node = apply_node_props(value, tag, anchor, ctx.coerce_binary)
(node, line_index + 1)
} else {
let (parts, next) = collect_plain_continuation(
lines,
line_index + 1,
indent,
allow_same_indent_props,
)
let joined = if parts.length() > 0 {
fold_plain_lines(rest, parts)
} else {
rest
}
let value = parse_scalar_value(joined)
let node = apply_node_props(value, tag, anchor, ctx.coerce_binary)
if parts.length() > 0 {
(node, next)
} else {
(node, line_index + 1)
}
}
} else {
let value = parse_scalar_value(rest)
(
apply_node_props(value, tag, anchor, ctx.coerce_binary),
line_index + 1,
)
}
}
}
}
///|
fn parse_inline_key(ctx : TagContext, text : String) -> Yaml raise ParseError {
let trimmed = text.trim().to_string()
if trimmed is "" {
Yaml::Null
} else {
parse_inline_value(ctx, text)
}
}
///|
fn parse_value_from_text(
ctx : TagContext,
lines : Array[Line],
line_index : Int32,
indent : Int32,
value_text : String,
allow_same_indent_props : Bool,
) -> (Yaml, Int32) raise ParseError {
let raw_value = if value_text == block_content(lines[line_index]) {
lines[line_index].raw
.view(start_offset=lines[line_index].indent)
.to_string()
} else {
value_text
}
let base_indent = if value_text == block_content(lines[line_index]) {
indent - 1
} else {
indent
}
let trimmed_value = value_text.trim_start().to_string()
let raw_trimmed = raw_value.trim_start().to_string()
if trimmed_value is "" {
let next_index = next_non_empty(lines, line_index + 1)
if next_index >= lines.length() {
(Yaml::Null, next_index)
} else if block_indent(lines[next_index]) > indent {
let next_line = lines[next_index]
match parse_props_only(ctx, block_content(next_line)) {
Some((tag, anchor)) => {
let after_props = next_non_empty(lines, next_index + 1)
if after_props < lines.length() {
let after_line = lines[after_props]
let outdented_seq = block_indent(after_line) == indent &&
is_seq_line(after_line, indent)
if block_indent(after_line) < block_indent(next_line) &&
(block_indent(after_line) > indent || outdented_seq) {
match
try_parse_block_scalar(
ctx,
block_content(after_line),
lines,
after_props + 1,
indent,
) {
Some((value, next)) =>
return (
apply_node_props(value, tag, anchor, ctx.coerce_binary),
next,
)
None => {
let (value, next) = parse_block_node(
ctx,
lines,
after_props,
block_indent(after_line),
true,
)
return (
apply_node_props(value, tag, anchor, ctx.coerce_binary),
next,
)
}
}
}
}
}
None => ()
}
let child_indent = block_indent(next_line)
parse_block_node(ctx, lines, next_index, child_indent, true)
} else if is_seq_line(lines[next_index], indent) {
parse_block_seq(ctx, lines, next_index, indent)
} else {
(Yaml::Null, next_index)
}
} else {
if trimmed_value is "-" ||
trimmed_value.has_prefix("- ") ||
trimmed_value.has_prefix("-\t") {
let virtual_indent = inline_virtual_indent_seq(
lines,
line_index,
indent,
indent + 2,
)
let virtual_line = make_virtual_line(
lines[line_index].number,
virtual_indent,
trimmed_value,
)
let temp : Array[Line] = [virtual_line]
for i in (line_index + 1).. return (node, next)
None => ()
}
match try_parse_flow_multiline(ctx, trimmed_value, lines, line_index) {
Some((node, next)) => return (node, next)
None => ()
}
match
try_parse_props_only(
ctx, trimmed_value, lines, line_index, indent, allow_same_indent_props,
) {
Some((node, next)) => return (node, next)
None => ()
}
match
try_parse_block_scalar(
ctx,
trimmed_value,
lines,
line_index + 1,
base_indent,
) {
Some((node, next)) => (node, next)
None =>
parse_inline_node_multiline(
ctx, lines, line_index, indent, trimmed_value, allow_same_indent_props,
)
}
}
}
///|
fn try_parse_props_only(
ctx : TagContext,
text : String,
lines : Array[Line],
line_index : Int32,
indent : Int32,
allow_same_indent_props : Bool,
) -> (Yaml, Int32)? raise ParseError {
let scanner = Scanner::new(text)
scanner.skip_ws_and_comments()
if parse_alias(scanner) is Some(_) {
return None
}
let (tag, anchor) = parse_node_props(ctx, scanner)
scanner.skip_ws_and_comments()
let rest = text
.view(start_offset=scanner.offset())
.to_string()
.trim_start()
.to_string()
if rest is "" && (tag is Some(_) || anchor is Some(_)) {
let next_index = next_non_empty(lines, line_index + 1)
if next_index < lines.length() {
let next_line = lines[next_index]
if block_indent(next_line) > indent {
let child_indent = block_indent(next_line)
let (value, next) = parse_block_node(
ctx, lines, next_index, child_indent, true,
)
return Some(
(apply_node_props(value, tag, anchor, ctx.coerce_binary), next),
)
}
if block_indent(next_line) == indent && is_seq_line(next_line, indent) {
let (value, next) = parse_block_seq(ctx, lines, next_index, indent)
return Some(
(apply_node_props(value, tag, anchor, ctx.coerce_binary), next),
)
}
if allow_same_indent_props && block_indent(next_line) == indent {
let (value, next) = parse_block_node(
ctx, lines, next_index, indent, true,
)
return Some(
(apply_node_props(value, tag, anchor, ctx.coerce_binary), next),
)
}
}
return Some(
(
apply_node_props(Yaml::Null, tag, anchor, ctx.coerce_binary),
line_index + 1,
),
)
}
None
}
///|
fn parse_props_only(
ctx : TagContext,
text : String,
) -> (String?, String?)? raise ParseError {
let scanner = Scanner::new(text)
scanner.skip_ws_and_comments()
if parse_alias(scanner) is Some(_) {
return None
}
let (tag, anchor) = parse_node_props(ctx, scanner)
scanner.skip_ws_and_comments()
let rest = text
.view(start_offset=scanner.offset())
.to_string()
.trim()
.to_string()
if rest is "" && (tag is Some(_) || anchor is Some(_)) {
Some((tag, anchor))
} else {
None
}
}
///|
fn try_parse_quoted_props(
ctx : TagContext,
text : String,
lines : Array[Line],
line_index : Int32,
) -> (Yaml, Int32)? raise ParseError {
let scanner = Scanner::new(text)
scanner.skip_ws_and_comments()
if parse_alias(scanner) is Some(_) {
return None
}
let (tag, anchor) = parse_node_props(ctx, scanner)
scanner.skip_ws_and_comments()
let rest = text
.view(start_offset=scanner.offset())
.to_string()
.trim_start()
.to_string()
if rest.has_prefix("\"") && !is_terminated_double(rest) {
let (combined, next) = collect_quoted_multiline(
lines,
line_index,
'"',
rest,
lines[line_index].indent,
)
let value = parse_flow_value_from_string(ctx, combined)
return Some((apply_node_props(value, tag, anchor, ctx.coerce_binary), next))
}
if rest.has_prefix("'") && !is_terminated_single(rest) {
let (combined, next) = collect_quoted_multiline(
lines,
line_index,
'\'',
rest,
lines[line_index].indent,
)
let value = parse_flow_value_from_string(ctx, combined)
return Some((apply_node_props(value, tag, anchor, ctx.coerce_binary), next))
}
None
}
///|
fn try_parse_flow_multiline(
ctx : TagContext,
text : String,
lines : Array[Line],
line_index : Int32,
) -> (Yaml, Int32)? raise ParseError {
let scanner = Scanner::new(text)
scanner.skip_ws_and_comments()
if parse_alias(scanner) is Some(_) {
return None
}
let (tag, anchor) = parse_node_props(ctx, scanner)
scanner.skip_ws_and_comments()
let rest = text
.view(start_offset=scanner.offset())
.to_string()
.trim_start()
.to_string()
if rest.has_prefix("[") || rest.has_prefix("{") {
let base_indent = lines[line_index].indent
let (combined, next) = collect_flow_multiline(
lines, line_index, rest, base_indent,
)
let value = parse_flow_value_from_string(ctx, combined)
return Some((apply_node_props(value, tag, anchor, ctx.coerce_binary), next))
}
None
}
///|
fn collect_flow_multiline(
lines : Array[Line],
index : Int32,
first : String,
base_indent : Int32,
) -> (String, Int32) raise ParseError {
let sb = StringBuilder::new()
sb.write_string(first)
let mut depth = 0
let mut in_single = false
let mut in_double = false
let (next_depth, next_single, next_double) = scan_flow_depth(
first, depth, in_single, in_double,
)
depth = next_depth
in_single = next_single
in_double = next_double
let mut i = index + 1
while depth > 0 && i < lines.length() {
let line = lines[i]
if line.has_tab_indent &&
!(line.content is "") &&
line.indent <= base_indent {
raise ParseError::InvalidIndent(pos={
line: line.number,
column: line.indent + 1,
})
}
if !(line.content is "") && line.indent <= base_indent {
let trimmed = line.content.trim().to_string()
if !(trimmed is "]" || trimmed is "}") {
raise ParseError::InvalidIndent(pos={
line: line.number,
column: line.indent + 1,
})
}
}
sb.write_char('\n')
let line_text = line.raw.view(start_offset=line.indent).to_string()
sb.write_string(line_text)
let (next_depth, next_single, next_double) = scan_flow_depth(
line_text, depth, in_single, in_double,
)
depth = next_depth
in_single = next_single
in_double = next_double
i = i + 1
}
(sb.to_string(), i)
}
///|
fn scan_flow_depth(
text : String,
depth : Int32,
in_single : Bool,
in_double : Bool,
) -> (Int32, Bool, Bool) {
let mut d = depth
let mut single = in_single
let mut double = in_double
let chars = text.to_array()
let mut i = 0
while i < chars.length() {
let c = chars[i]
if double {
if c is '\\' && i + 1 < chars.length() {
i = i + 2
continue
}
if c is '"' {
double = false
}
i = i + 1
continue
}
if single {
if c is '\'' {
single = false
}
i = i + 1
continue
}
if c is '"' {
double = true
i = i + 1
continue
}
if c is '\'' {
single = true
i = i + 1
continue
}
if c is '[' || c is '{' {
d = d + 1
} else if c is ']' || c is '}' {
if d > 0 {
d = d - 1
}
}
i = i + 1
}
(d, single, double)
}
///|
fn collect_quoted_multiline(
lines : Array[Line],
index : Int32,
quote : Char,
first : String,
base_indent : Int32,
) -> (String, Int32) raise ParseError {
let mut content_indent : Int32? = None
let mut combined = first
let mut i = index + 1
while i < lines.length() {
let line = lines[i]
let raw = line.raw.view(start_offset=line.indent).to_string()
if raw.trim() is "" {
combined = combined + "\n"
i = i + 1
continue
}
let candidate = combined + "\n" + raw
let terminates = if quote is '"' {
is_terminated_double(candidate)
} else {
is_terminated_single(candidate)
}
if content_indent is None {
if (line.has_tab_indent && line.indent == 0) || line.indent <= base_indent {
raise ParseError::InvalidIndent(pos={
line: line.number,
column: line.indent + 1,
})
}
content_indent = Some(line.indent)
} else {
let effective_indent = if line.has_tab_indent {
line.indent + 1
} else {
line.indent
}
if effective_indent < content_indent.unwrap_or(effective_indent) {
if terminates {
return (candidate, i + 1)
}
raise ParseError::InvalidIndent(pos={
line: line.number,
column: line.indent + 1,
})
}
}
if line.indent == 0 &&
content_indent.unwrap_or(0) == 0 &&
is_doc_marker_line(raw) {
raise ParseError::InvalidIndent(pos={ line: line.number, column: 1, })
}
combined = candidate
if terminates {
return (combined, i + 1)
}
i = i + 1
}
raise ParseError::UnexpectedEof(pos={
line: match lines.last() {
None => 1
Some(last) => last.number
},
column: 1,
})
}
///|
fn is_doc_marker_line(text : String) -> Bool {
if text.has_prefix("---") || text.has_prefix("...") {
let chars = text.to_array()
if chars.length() == 3 {
return true
}
chars[3].is_whitespace()
} else {
false
}
}
///|
fn is_terminated_double(text : String) -> Bool {
let chars = text.to_array()
let mut escaped = false
let mut i = 0
while i < chars.length() {
let c = chars[i]
if i == 0 && c is '"' {
i = i + 1
continue
}
if escaped {
escaped = false
i = i + 1
continue
}
if c is '\\' {
escaped = true
i = i + 1
continue
}
if c is '"' {
return true
}
i = i + 1
}
false
}
///|
fn is_terminated_single(text : String) -> Bool {
let chars = text.to_array()
let mut i = 0
while i < chars.length() {
let c = chars[i]
if i == 0 && c is '\'' {
i = i + 1
continue
}
if c is '\'' {
if i + 1 < chars.length() && chars[i + 1] is '\'' {
i = i + 2
continue
} else {
return true
}
}
i = i + 1
}
false
}
///|
fn parse_explicit_entry(
ctx : TagContext,
lines : Array[Line],
index : Int32,
indent : Int32,
) -> ((Yaml, Yaml), Int32) raise ParseError {
let line = lines[index]
let rest = drop_first_char(block_content(line)).trim_start().to_string()
if rest is "" {
let next_index = next_non_empty(lines, index + 1)
if next_index >= lines.length() || lines[next_index].indent < indent {
raise ParseError::InvalidIndent(pos={
line: line.number,
column: line.indent + 1,
})
}
let (key, next_after_key) = parse_block_node(
ctx,
lines,
next_index,
lines[next_index].indent,
false,
)
let value_line = next_non_empty(lines, next_after_key)
if value_line < lines.length() && is_value_line(lines[value_line], indent) {
let value_text = drop_first_char(block_content(lines[value_line]))
let (value, next) = parse_value_from_text(
ctx, lines, value_line, indent, value_text, false,
)
((key, value), next)
} else {
((key, Yaml::Null), next_after_key)
}
} else {
match try_parse_block_scalar(ctx, rest, lines, index + 1, indent) {
Some((key, next_after_key)) => {
let value_line = next_non_empty(lines, next_after_key)
if value_line < lines.length() &&
is_value_line(lines[value_line], indent) {
let value_text = drop_first_char(block_content(lines[value_line]))
let (value, next) = parse_value_from_text(
ctx, lines, value_line, indent, value_text, false,
)
((key, value), next)
} else {
((key, Yaml::Null), next_after_key)
}
}
None =>
match split_key_value_opt(rest) {
Some(_) => {
let virtual_indent = inline_virtual_indent_map(
lines,
index,
indent,
indent + 2,
)
let virtual_line = make_virtual_line(
line.number,
virtual_indent,
rest,
)
let temp : Array[Line] = [virtual_line]
for i in (index + 1).. {
let (key, next_after_key) = parse_value_from_text(
ctx, lines, index, indent, rest, false,
)
let value_line = next_non_empty(lines, next_after_key)
if value_line < lines.length() &&
is_value_line(lines[value_line], indent) {
let value_text = drop_first_char(block_content(lines[value_line]))
let (value, next) = parse_value_from_text(
ctx, lines, value_line, indent, value_text, false,
)
((key, value), next)
} else {
((key, Yaml::Null), next_after_key)
}
}
}
}
}
}
///|
fn is_plain_scalar_start(text : String) -> Bool {
if text is "" {
return false
}
let trimmed = text.trim_start().to_string()
!(trimmed.has_prefix("\"") ||
trimmed.has_prefix("'") ||
trimmed.has_prefix("[") ||
trimmed.has_prefix("{") ||
trimmed.has_prefix("!") ||
trimmed.has_prefix("&") ||
trimmed.has_prefix("*"))
}
///|
fn collect_plain_continuation(
lines : Array[Line],
index : Int32,
indent : Int32,
allow_same_indent : Bool,
) -> (Array[String], Int32) raise ParseError {
let parts : Array[String] = []
let mut i = index
let mut base_indent : Int32? = None
while i < lines.length() {
let line = lines[i]
let raw = line.raw.view(start_offset=line.indent).to_string()
if raw.trim() is "" {
let next_index = next_non_empty(lines, i + 1)
if next_index >= lines.length() {
break
}
if allow_same_indent {
if lines[next_index].indent < indent {
break
}
} else if lines[next_index].indent <= indent {
break
}
parts.push("")
i = i + 1
continue
}
let effective_indent = if line.has_tab_indent {
indent + 1
} else {
line.indent
}
if allow_same_indent {
if effective_indent < indent {
break
}
} else if effective_indent <= indent {
break
}
if base_indent is None {
base_indent = Some(effective_indent)
}
let base = base_indent.unwrap_or(effective_indent)
if effective_indent < base {
break
}
let slice_start = if line.has_tab_indent {
line.indent + 1
} else {
line.indent
}
let text = line.raw.view(start_offset=slice_start).to_string()
if has_plain_mapping_break(text) {
raise ParseError::InvalidIndent(pos={
line: line.number,
column: line.indent + 1,
})
}
let stripped = strip_inline_comment(text)
let trimmed = stripped.trim_end().to_string()
parts.push(trimmed)
if stripped.length() < text.length() {
i = i + 1
break
}
i = i + 1
}
(parts, i)
}
///|
fn has_plain_mapping_break(text : String) -> Bool {
let chars = text.to_array()
let mut i = 0
while i < chars.length() {
let c = chars[i]
if c is ':' {
if i + 1 == chars.length() || chars[i + 1].is_whitespace() {
return true
}
}
i = i + 1
}
false
}
///|
fn fold_plain_lines(first : String, rest : Array[String]) -> String {
let all : Array[String] = [first]
for line in rest {
all.push(line)
}
fold_lines(all)
}
///|
fn parse_block_indicator(
text : String,
pos : Position,
) -> (Char, Chomp, Int32?)? raise ParseError {
let trimmed = text.trim().to_string()
if trimmed is "" {
return None
}
let chars = trimmed.to_array()
let style = chars[0]
if !(style is '|' || style is '>') {
return None
}
let mut chomp = Chomp::Clip
let mut indent : Int32? = None
let mut chomp_set = false
let mut indent_set = false
let mut i = 1
while i < chars.length() {
let c = chars[i]
if c is '+' {
if chomp_set {
raise ParseError::InvalidIndent(pos~)
}
chomp = Chomp::Keep
chomp_set = true
} else if c is '-' {
if chomp_set {
raise ParseError::InvalidIndent(pos~)
}
chomp = Chomp::Strip
chomp_set = true
} else if c.is_ascii_digit() {
if indent_set {
raise ParseError::InvalidIndent(pos~)
}
let digit = c.to_int() - '0'.to_int()
if digit == 0 {
raise ParseError::InvalidIndent(pos~)
}
indent = Some(digit)
indent_set = true
} else {
break
}
i = i + 1
}
let mut rest = i
while rest < chars.length() && chars[rest].is_whitespace() {
rest = rest + 1
}
if rest < chars.length() {
if chars[rest] is '#' {
if rest == i {
raise ParseError::InvalidIndent(pos~)
}
} else {
raise ParseError::InvalidIndent(pos~)
}
}
Some((style, chomp, indent))
}
///|
fn try_parse_block_scalar(
ctx : TagContext,
rest : String,
lines : Array[Line],
index : Int32,
parent_indent : Int32,
) -> (Yaml, Int32)? raise ParseError {
let scanner = Scanner::new(rest)
scanner.skip_ws_and_comments()
if parse_alias(scanner) is Some(_) {
return None
}
let (tag, anchor) = parse_node_props(ctx, scanner)
scanner.skip_ws_and_comments()
let tail = rest
.view(start_offset=scanner.offset())
.to_string()
.trim_start()
.to_string()
let pos_line = if lines.length() == 0 {
None
} else if index > 0 && index - 1 < lines.length() {
Some(lines[index - 1])
} else {
lines.last()
}
let pos = match pos_line {
None => { line: 1, column: 1, }
Some(line) => { line: line.number, column: line.indent + 1, }
}
match parse_block_indicator(tail, pos) {
None => None
Some((style, chomp, indent_hint)) => {
let (value, next) = parse_block_scalar(
lines, index, parent_indent, style, chomp, indent_hint,
)
Some(
(
apply_node_props(Yaml::String(value), tag, anchor, ctx.coerce_binary),
next,
),
)
}
}
}
///|
fn parse_block_scalar(
lines : Array[Line],
index : Int32,
parent_indent : Int32,
style : Char,
chomp : Chomp,
indent_hint : Int32?,
) -> (String, Int32) raise ParseError {
let mut i = index
let mut content_indent : Int32? = None
let collected : Array[String] = []
let mut pending_blank = 0
let mut max_blank_indent : Int32? = None
while i < lines.length() {
let line = lines[i]
let tab_indent_threshold = match content_indent {
Some(indent) => indent
None =>
match indent_hint {
Some(hint) => parent_indent + hint
None => parent_indent + 1
}
}
if line.has_tab_indent && line.indent < tab_indent_threshold {
raise ParseError::InvalidIndent(pos={
line: line.number,
column: line.indent + 1,
})
}
if i == lines.length() - 1 && line.raw is "" {
break
}
let raw_content = line.raw.view(start_offset=line.indent).to_string()
let mut treat_as_blank = is_space_only(raw_content)
match content_indent {
Some(indent) => if line.indent > indent { treat_as_blank = false }
None => ()
}
if treat_as_blank {
let blank_threshold = match content_indent {
Some(indent) => indent
None => parent_indent
}
if line.indent < blank_threshold {
let next_index = next_non_empty(lines, i + 1)
if next_index >= lines.length() ||
lines[next_index].indent < blank_threshold {
if chomp is Chomp::Keep && content_indent is Some(_) {
pending_blank = pending_blank + 1
i = i + 1
continue
}
break
}
}
if content_indent is None && line.indent > parent_indent {
match max_blank_indent {
None => max_blank_indent = Some(line.indent)
Some(prev) =>
if line.indent > prev {
max_blank_indent = Some(line.indent)
}
}
}
pending_blank = pending_blank + 1
i = i + 1
continue
}
if content_indent is None && line.indent <= parent_indent {
pending_blank = 0
break
}
if content_indent is None {
match max_blank_indent {
Some(max_indent) =>
if line.indent < max_indent {
raise ParseError::InvalidIndent(pos={
line: line.number,
column: line.indent + 1,
})
}
None => ()
}
content_indent = match indent_hint {
None => Some(line.indent)
Some(hint) => Some(parent_indent + hint)
}
}
let indent = content_indent.unwrap_or(line.indent)
if line.indent < indent {
break
}
for _ in 0.. 0 {
for _ in 0.. 0 {
base + "\n"
} else if chomp is Chomp::Keep && index < lines.length() {
"\n"
} else {
""
}
let value = apply_chomp(with_newline, chomp)
(value, i)
}
///|
fn is_space_only(text : String) -> Bool {
let chars = text.to_array()
for c in chars {
if !(c is ' ') {
return false
}
}
true
}
///|
fn join_lines(lines : Array[String]) -> String {
let sb = StringBuilder::new()
for idx in 0.. 0 {
sb.write_char('\n')
}
sb.write_string(lines[idx])
}
sb.to_string()
}
///|
fn fold_lines(lines : Array[String]) -> String {
let sb = StringBuilder::new()
let mut prev_blank = false
for idx in 0.. 0 {
if is_blank {
sb.write_char('\n')
} else if !prev_blank {
sb.write_char(' ')
}
}
sb.write_string(line)
prev_blank = is_blank
}
sb.to_string()
}
///|
fn fold_block_lines(lines : Array[String]) -> String {
let sb = StringBuilder::new()
for idx in 0.. String {
match chomp {
Chomp::Clip => {
let trimmed = trim_trailing_newlines(text)
if trimmed is "" {
""
} else {
trimmed + "\n"
}
}
Chomp::Strip => trim_trailing_newlines(text)
Chomp::Keep => text
}
}
///|
fn trim_trailing_newlines(text : String) -> String {
let chars = text.to_array()
let mut end = chars.length()
while end > 0 && chars[end - 1] is '\n' {
end = end - 1
}
if end == chars.length() {
return text
}
let sb = StringBuilder::new()
for i in 0.. Bool {
let line_indent = block_indent(line)
if line_indent != indent {
return false
}
let content = block_content(line)
if content is "-" {
return true
}
let chars = content.to_array()
chars.length() > 1 && chars[0] is '-' && chars[1].is_whitespace()
}
///|
fn is_map_line(line : Line, indent : Int32) -> Bool {
let line_indent = block_indent(line)
if line_indent != indent {
return false
}
is_explicit_key_line(line, indent) ||
split_key_value_opt(block_content(line)) is Some(_)
}
///|
fn is_map_start(lines : Array[Line], index : Int32, indent : Int32) -> Bool {
let line = lines[index]
let line_indent = block_indent(line)
if line_indent != indent {
return false
}
is_explicit_key_line(line, indent) ||
split_key_value_opt(block_content(line)) is Some(_) ||
is_value_line_after(lines, index, indent)
}
///|
fn is_explicit_key_line(line : Line, indent : Int32) -> Bool {
let line_indent = block_indent(line)
if line_indent != indent {
return false
}
let content = block_content(line)
content is "?" || content.has_prefix("? ")
}
///|
fn is_value_line(line : Line, indent : Int32) -> Bool {
let line_indent = block_indent(line)
if line_indent != indent {
return false
}
let content = block_content(line)
content is ":" || content.has_prefix(": ")
}
///|
fn is_value_line_after(
lines : Array[Line],
index : Int32,
indent : Int32,
) -> Bool {
let next_index = next_non_empty(lines, index + 1)
if next_index >= lines.length() {
return false
}
is_value_line(lines[next_index], indent)
}
///|
fn split_indicator_rest(content : String, indicator : Char) -> (Bool, String) {
let chars = content.to_array()
if chars.length() == 0 || chars[0] != indicator {
return (false, content)
}
let mut i = 1
let mut has_tab = false
while i < chars.length() && chars[i].is_whitespace() {
if chars[i] is '\t' {
has_tab = true
}
i = i + 1
}
let sb = StringBuilder::new()
for j in i.. Bool {
let chars = text.to_array()
if chars.length() == 0 || chars[0] != indicator {
return false
}
if chars.length() == 1 {
return true
}
chars[1].is_whitespace()
}
///|
fn starts_with_block_indicator(text : String) -> Bool {
starts_with_indicator(text, '-') ||
starts_with_indicator(text, '?') ||
starts_with_indicator(text, ':')
}
///|
fn has_plain_key_indicator(text : String) -> Bool {
let chars = text.to_array()
let mut i = 0
while i < chars.length() {
let c = chars[i]
if c is ':' {
if i + 1 == chars.length() || chars[i + 1].is_whitespace() {
return true
}
}
i = i + 1
}
false
}
///|
fn indicator_has_tab(content : String, indicator : Char) -> Bool {
if !starts_with_indicator(content, indicator) {
return false
}
let (has_tab, _) = split_indicator_rest(content, indicator)
has_tab
}
///|
fn split_key_value(line : Line) -> (String, String) raise ParseError {
let content = block_content(line)
match split_key_value_opt(content) {
None =>
raise ParseError::UnexpectedChar(
pos={ line: line.number, column: line.indent + 1, },
found=':',
)
Some(pair) => pair
}
}
///|
fn split_key_value_opt(content : String) -> (String, String)? {
let chars = content.to_array()
let mut in_single = false
let mut in_double = false
let mut depth = 0
let mut i = 0
let mut found = false
let key_sb = StringBuilder::new()
let mut value_start = 0
let mut prev_space = true
while i < chars.length() {
let c = chars[i]
if prev_space && (c is '&' || c is '!' || c is '*') {
key_sb.write_char(c)
i = i + 1
while i < chars.length() {
let ch = chars[i]
if ch.is_whitespace() ||
ch is ',' ||
ch is ']' ||
ch is '}' ||
ch is '#' {
break
}
key_sb.write_char(ch)
i = i + 1
}
prev_space = false
continue
}
if in_single {
if c is '\'' {
in_single = false
}
key_sb.write_char(c)
prev_space = false
i = i + 1
continue
}
if in_double {
if c is '"' {
in_double = false
key_sb.write_char(c)
prev_space = false
i = i + 1
continue
}
if c is '\\' && i + 1 < chars.length() {
key_sb.write_char(c)
key_sb.write_char(chars[i + 1])
prev_space = false
i = i + 2
continue
}
key_sb.write_char(c)
prev_space = false
i = i + 1
continue
}
if c is '\'' && prev_space {
in_single = true
key_sb.write_char(c)
prev_space = false
i = i + 1
continue
}
if c is '"' && prev_space {
in_double = true
key_sb.write_char(c)
prev_space = false
i = i + 1
continue
}
if c is '{' || c is '[' {
depth = depth + 1
key_sb.write_char(c)
prev_space = c.is_whitespace()
i = i + 1
continue
}
if c is '}' || c is ']' {
if depth > 0 {
depth = depth - 1
}
key_sb.write_char(c)
i = i + 1
continue
}
if c is ':' {
if depth == 0 && (i + 1 == chars.length() || chars[i + 1].is_whitespace()) {
found = true
value_start = i + 1
break
}
}
key_sb.write_char(c)
prev_space = c.is_whitespace()
i = i + 1
}
if !found {
return None
}
let key = key_sb.to_string().trim().to_string()
let value_sb = StringBuilder::new()
for j in value_start.. Array[Line] {
let normalized = input
.replace_all(old="\r\n", new="\n")
.replace_all(old="\r", new="\n")
let lines : Array[Line] = []
let mut number = 1
for line_view in normalized.split("\n") {
let line = line_view.to_string()
let mut indent = 0
let mut has_tab_indent = false
let chars = line.to_array()
while indent < chars.length() {
let c = chars[indent]
if c is ' ' {
indent = indent + 1
} else if c is '\t' {
has_tab_indent = true
break
} else {
break
}
}
let content = line.view(start_offset=indent).to_string()
let stripped = strip_inline_comment(content)
let trimmed = stripped.trim_end().to_string()
lines.push({ number, indent, has_tab_indent, raw: line, content: trimmed, })
number = number + 1
}
lines
}
///|
fn strip_inline_comment(line : String) -> String {
let chars = line.to_array()
let mut i = 0
let mut in_single = false
let mut in_double = false
let mut prev_space = true
let sb = StringBuilder::new()
while i < chars.length() {
let c = chars[i]
if in_single {
if c is '\'' {
in_single = false
}
sb.write_char(c)
prev_space = false
i = i + 1
continue
}
if in_double {
if c is '"' {
in_double = false
sb.write_char(c)
prev_space = false
i = i + 1
continue
}
if c is '\\' && i + 1 < chars.length() {
sb.write_char(c)
sb.write_char(chars[i + 1])
prev_space = false
i = i + 2
continue
}
sb.write_char(c)
prev_space = false
i = i + 1
continue
}
if c is '\'' && prev_space {
in_single = true
sb.write_char(c)
prev_space = false
i = i + 1
continue
}
if c is '"' && prev_space {
in_double = true
sb.write_char(c)
prev_space = false
i = i + 1
continue
}
if c is '#' && prev_space {
break
}
sb.write_char(c)
prev_space = c.is_whitespace()
i = i + 1
}
sb.to_string()
}