//
// Copyright (c) 2024, Brian Frank
// Licensed under the Academic Free License version 3.0
//
// History:
// 13 Feb 2026 Mike Jarmy Creation
//
**
** YamlWriter writes JSON-style values (null, strings, booleans, numbers,
** maps and lists) in YAML format.
**
@Js
class YamlWriter
{
new make(OutStream out)
{
this.out = out
}
This writeYaml(Obj? obj)
{
write(obj, 0, false)
}
private This write(Obj? obj, Int depth, Bool isListItem)
{
if (obj is Map) return writeMap(obj, depth, isListItem)
if (obj is List) return writeList(obj, depth, isListItem)
return writeVal(obj, depth)
}
private This writeMap(Map map, Int depth, Bool isListItem)
{
n := 0
map.each |val, key|
{
// If it's the first key of an object in a list, we don't indent; the
// dash and space (2 chars) that have already been written act as the
// indent.
if ((n == 0) && isListItem)
w(key).w(":")
else
indent(depth).w(key).w(":")
if ((val is Map) || (val is List))
nl.write(val, depth+1, false)
else
w(" ").writeVal(val, depth).nl
n++
}
return this
}
private This writeList(List list, Int depth, Bool isListItem)
{
list.each |item|
{
indent(depth).w("- ")
// If the item is a collection, handle it on the same line
if ((item is Map) || (item is List))
{
write(item, depth+1, true)
}
else
{
writeVal(item, depth).nl
}
}
return this
}
private This writeVal(Obj? obj, Int depth)
{
return (obj is Str) ? writeStr(obj, depth) : w(obj)
}
private This writeStr(Str? s, Int depth)
{
// null
if (s == null)
{
out.print("null")
return this
}
// empty
if (s.isEmpty())
{
out.print("''")
return this
}
// Multi-line
if (s.contains("\n") || s.contains("\r"))
{
return writeMultilineString(s, depth+1)
}
// special chars and reserved words
if (specialChars.matches(s) ||
leadingSpecialChars.matches(s))
{
out.print("\"")
s.each |c|
{
switch (c)
{
case '\"': out.print("\\\"")
case '\\': out.print("\\\\")
case '\t': out.print("\\t")
default: out.writeChar(c)
}
}
out.print("\"")
}
else
{
out.print(s)
}
return this
}
private This writeMultilineString(Str s, Int depth)
{
w("|").nl()
lines := s.splitLines
lines.each |ln, i|
{
indent(depth).w(ln)
if (i < lines.size - 1)
{
nl()
}
}
return this
}
private This w(Obj? obj)
{
if (obj == null)
out.print("null")
else
out.print(obj.toStr)
return this
}
private This nl()
{
out.printLine
return this
}
private This indent(Int depth) { w(Str.spaces(depth*2)) }
//////////////////////////////////////////////////////////////////////////
// Fields
//////////////////////////////////////////////////////////////////////////
private static const Regex specialChars := Regex.fromStr(".*[:#\\{\\}\\[\\],\\s!&\\*|>='\"\\t].*")
private static const Regex leadingSpecialChars := Regex.fromStr("^[-?:\\[\\]\\{\\}#|>&%@`!,].*")
private OutStream out
}