
Yaml.pudu
Pudu212 lines8.6 KB
1/** @Docgen.Yaml.Module — YAML read into metadata and metadata written as YAML */2module PuduLangDocgen.Yaml34import Std.Option as Option5import Std.Text as Text6import Std.Yaml as Reader7import PuduLangDocgen as Docgen8import PuduLangDocgen.Meta as Meta910111213export fn decode(source: Str) -> Result[Docgen.Meta, Str] {14 let (plain, blocks) = extractBlocks(source)15 match Reader.decode(indentSequences(plain)) {16 case Ok(found) => Ok(restore(Meta.fromYaml(&found), &blocks))17 case Err(problem) => Err(Reader.explain(&problem))18 }19}202122fn indentSequences(source: Str) -> Str {23 let lines = source.split("\n")24 var shifts: Array[Int] = []25 var result: Array[Str] = []26 var index = 027 for line in lines {28 let content = line.trim()29 if content.isEmpty() || content.startsWith("#") {30 result = result.push(line)31 index = index + 132 continue33 }34 let column = line.length() - Text.trimStart(line).length()35 while !shifts.isEmpty() && !(column > shifts[shifts.length() - 1] || (column == shifts[shifts.length() - 1] && content.startsWith("- "))) {36 shifts = shifts.slice(0, shifts.length() - 1)37 }38 var padding: Array[Str] = []39 for _level in 0..shifts.length() { padding = padding.push(" ") }40 result = result.push(padding.join("") + line)41 if content.endsWith(":") {42 let key = if content.startsWith("- ") { column + 2 } else { column }43 var ahead = index + 144 while ahead < lines.length() && (lines[ahead].trim().isEmpty() || lines[ahead].trim().startsWith("#")) { ahead = ahead + 1 }45 if ahead < lines.length() {46 let next = lines[ahead]47 let nextColumn = next.length() - Text.trimStart(next).length()48 if nextColumn == key && next.trim().startsWith("- ") { shifts = shifts.push(key) }49 }50 }51 index = index + 152 }53 result.join("\n")54}555657const BLOCK: Str = "\u{2}"585960const INDICATORS: Array[Str] = ["|", "|-", "|+", ">", ">-", ">+"]616263fn extractBlocks(source: Str) -> (Str, Array[Str]) {64 let all = source.split("\n")65 var kept: Array[Str] = []66 var texts: Array[Str] = []67 var index = 068 while index < all.length() {69 let line = all[index]70 let trimmed = Text.trimEnd(line)71 var indicator = ""72 for candidate in INDICATORS {73 if trimmed.endsWith(": " + candidate) || trimmed == "- " + candidate || trimmed.endsWith(" - " + candidate) { indicator = candidate }74 }75 if indicator.isEmpty() {76 kept = kept.push(line)77 index = index + 178 continue79 }80 let parent = line.length() - Text.trimStart(line).length()81 var end = index + 182 var body: Array[Str] = []83 while end < all.length() && (all[end].trim().isEmpty() || all[end].length() - Text.trimStart(all[end]).length() > parent) {84 body = body.push(all[end])85 end = end + 186 }87 var depth = 100000088 for held in body {89 if !held.trim().isEmpty() {90 let column = held.length() - Text.trimStart(held).length()91 if column < depth { depth = column }92 }93 }94 var content = body.map(|held: Str| if held.trim().isEmpty() { "" } else { held.drop(depth) })95 var trailing = 096 while content.length() > trailing && content[content.length() - 1 - trailing].isEmpty() { trailing = trailing + 1 }97 let lines = content.slice(0, content.length() - trailing)98 var value = if indicator.startsWith(">") { folded(&lines) } else { lines.join("\n") }99 if indicator.endsWith("+") { for _extra in 0..(trailing + 1) { value = value + "\n" } }100 else if !indicator.endsWith("-") && !lines.isEmpty() { value = value + "\n" }101 kept = kept.push(trimmed.take(trimmed.length() - indicator.length()) + "\"" + BLOCK + show(texts.length()) + "\"")102 texts = texts.push(value)103 index = end104 }105 (kept.join("\n"), texts)106}107108109fn folded(lines: &Array[Str]) -> Str {110 var pieces: Array[Str] = []111 var previous = ""112 for line in *lines {113 if line.isEmpty() { pieces = pieces.push("\n") }114 else if !pieces.isEmpty() && previous != "" && !line.startsWith(" ") { pieces = pieces.push(" " + line) }115 else { pieces = pieces.push(line) }116 previous = line117 }118 pieces.join("")119}120121122fn restore(value: Docgen.Meta, texts: &Array[Str]) -> Docgen.Meta {123 if texts.isEmpty() { return value }124 match value {125 case Docgen.Text(written) => {126 if written.startsWith(BLOCK) {127 match written.drop(1).toInt() {128 case Some(position) => { if position >= 0 && position < texts.length() { return Docgen.Text(texts[position]) } }129 case None => {}130 }131 }132 Docgen.Text(written)133 }134 case Docgen.Items(listed) => Docgen.Items(listed.map(|held: Docgen.Meta| restore(held, texts)))135 case Docgen.Fields(fields) => Docgen.Fields(fields.map(fn(entry: (Str, Docgen.Meta)) -> (Str, Docgen.Meta) {136 let (key, held) = entry137 (key, restore(held, texts))138 }))139 case other => other140 }141}142143144const SPECIAL: Str = ":#[]\{\},&*!|>'\"%@`"145146147export fn encode(value: &Docgen.Meta) -> Str { emitted(value, "").join("\n") + "\n" }148149150fn emitted(value: &Docgen.Meta, indent: Str) -> Array[Str] {151 match value {152 case Docgen.Fields(fields) => {153 if fields.isEmpty() { return [indent + "\{\}"] }154 var lines: Array[Str] = []155 for (key, held) in fields {156 let head = indent + scalar(key) + ":"157 match held {158 case Docgen.Fields(inner) => { lines = if inner.isEmpty() { lines.push(head + " \{\}") } else { lines.push(head).concat(emitted(&held, indent + " ")) } }159 case Docgen.Items(listed) => { lines = if listed.isEmpty() { lines.push(head + " []") } else { lines.push(head).concat(emitted(&held, indent)) } }160 case other => { lines = lines.push(head + " " + leaf(&other, indent + " ")) }161 }162 }163 lines164 }165 case Docgen.Items(listed) => {166 if listed.isEmpty() { return [indent + "[]"] }167 var lines: Array[Str] = []168 for held in listed {169 let nested = emitted(&held, indent + " ")170 match held {171 case Docgen.Fields(_fields) => { lines = lines.push(indent + "- " + nested[0].drop(indent.length() + 2)).concat(nested.slice(1, nested.length())) }172 case Docgen.Items(_items) => { lines = lines.push(indent + "-").concat(nested) }173 case other => { lines = lines.push(indent + "- " + leaf(&other, indent + " ")) }174 }175 }176 lines177 }178 case other => [indent + leaf(&other, indent + " ")]179 }180}181182183fn leaf(value: &Docgen.Meta, indent: Str) -> Str {184 match value {185 case Docgen.Text(written) => {186 if !written.contains("\n") { return scalar(written) }187 let body = written.split("\n").map(|line: Str| if line.isEmpty() { "" } else { indent + line }).join("\n")188 (if written.endsWith("\n") { "|" } else { "|-" }) + "\n" + body189 }190 case Docgen.Whole(count) => show(count)191 case Docgen.Flag(held) => if held { "true" } else { "false" }192 case _ => "null"193 }194}195196197198export fn scalar(written: Str) -> Str {199 var plain = !written.isEmpty() && written == written.trim()200 var control = false201 for character in written.chars() {202 if SPECIAL.contains(character.toText()) { plain = false }203 if character < ' ' { control = true }204 }205 let lowered = written.toLower()206 if ["true", "false", "yes", "no", "null", "~", "on", "off"].contains(lowered) || Option.isSome(&written.toInt()) { plain = false }207 if plain && !control { return written }208 if !control && !written.contains("\"") && !written.contains("\\") { return "\"" + written + "\"" }209 if !control && !written.contains("'") { return "'" + written + "'" }210 "\"" + written.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\t", "\\t") + "\""211}212