1 # The emitter: values to YAML text, in block style, indented two spaces. 2 # 3 # A string is plain when the Core schema reads it back as that string, and 4 # double-quoted otherwise. A sequence or mapping inside a sequence starts on 5 # the "- " line ("- - a", "- a: 1"). A key that is a collection is written 6 # after "? ". An empty collection is "[]" or "{}". A tag the schema knows 7 # is written "!!int", one starting "!" as it is, and any other "!<...>". 8 # load(dump(v)) is v for any value load returns. 9 10 import Base 11 12 import ./value.bend as V 13 14 import ./core.bend as C 15 16 # Where a node is written: at the start of a line, right after "- ", right 17 # after a key's ":" (or a tag), or as the later items of a collection, each 18 # on a line of its own. 19 type Pos is Data: 20 Line{} 21 Dash{} 22 Colon{} 23 More{} 24 25 def when(-A: Type, hit: Bool, yes: Unit -> A, no: Unit -> A) -> A: 26 match hit: 27 case True{}: 28 yes(Unit{}) 29 case False{}: 30 no(Unit{}) 31 32 def code(c: Char) -> U32: 33 match c: 34 case Chr{x}: 35 x 36 37 def is_in(c: Char, s: String) -> Bool: 38 String.contains(s, SCon{c, SNil{}}) 39 40 # Quoting 41 # ------- 42 43 def hex.digit(+n: Nat) -> Char: 44 Chr{U32.from_nat(Bool.pick(Nat, Nat.is_lt(n, 10n), Nat.add(n, 48n), Nat.add(n, 55n)))} 45 46 # +n+ in +width+ hex digits. 47 def hex.text(width: Nat, +n: Nat) -> String: 48 match width: 49 case 0n: 50 "" 51 case 1n+k: 52 hex.text(k, Nat.div(n, 16n)) ++ SCon{hex.digit(Nat.mod(n, 16n)), SNil{}} 53 54 # What YAML may hold as it is: printable characters but the ones double 55 # quotes escape. 56 def is_plain.char(+x: U32) -> Bool: 57 Bool.or(Bool.and(U32.is_ge(x, 32), U32.is_le(x, 126)), 58 Bool.or(Bool.and(U32.is_ge(x, 160), U32.is_le(x, 55295)), Bool.or(Bool.and(U32.is_ge(x, 57344), U32.is_le(x, 65533)), U32.is_ge(x, 65536)))) 59 60 def escape.other(c: Char) -> String: 61 match c: 62 case Chr{+x}: 63 +n = U32.to_nat(x) 64 Bool.pick(String, U32.is_eq(x, 133), "\\N", 65 Bool.pick(String, U32.is_eq(x, 8232), "\\L", 66 Bool.pick(String, U32.is_eq(x, 8233), "\\P", 67 Bool.pick(String, is_plain.char(x), SCon{Chr{x}, SNil{}}, 68 Bool.pick(String, Nat.is_lt(n, 256n), "\\x" ++ hex.text(2n, n), "\\u" ++ hex.text(4n, n)))))) 69 70 def escape(c: Char) -> String: 71 match c: 72 case '"': 73 "\\\"" 74 case '\\': 75 "\\\\" 76 case '\n': 77 "\\n" 78 case '\t': 79 "\\t" 80 case '\r': 81 "\\r" 82 case _: 83 escape.other(c) 84 85 def escaped(s: String) -> String: 86 match s: 87 case SNil{}: 88 "" 89 case SCon{c, t}: 90 escape(c) ++ escaped(t) 91 92 def quoted(s: String) -> String: 93 "\"" ++ escaped(s) ++ "\"" 94 95 def all.plain(s: String) -> Bool: 96 match s: 97 case SNil{}: 98 True{} 99 case SCon{c, t}: 100 Bool.and(is_plain.char(code(c)), all.plain(t)) 101 102 def last.char(s: String) -> Char: 103 Maybe.default(&2, Char, String.get(String.reverse(s), 0n), ' ') 104 105 def first.char(s: String) -> Char: 106 Maybe.default(&2, Char, String.get(s, 0n), ' ') 107 108 def is_str(v: V.Yaml) -> Bool: 109 match v: 110 case V.Str{t}: 111 True{} 112 case _: 113 False{} 114 115 # Whether +s+ reads back as itself when written plain: it isn't empty, has 116 # no blank at either end, starts with no indicator, holds no ": ", " #" or 117 # character that needs escaping, and the Core schema reads it as a string. 118 def is_safe.plain(+s: String) -> Bool: 119 Bool.and(Bool.and(Bool.not(String.is_empty(s)), all.plain(s)), 120 Bool.and(Bool.and(Bool.not(is_in(first.char(s), " -?:,[]{}#&*!|>'\"%@`")), Bool.not(Char.is_eq(last.char(s), ' '))), 121 Bool.and(Bool.and(Bool.not(String.contains(s, ": ")), Bool.not(String.contains(s, " #"))), 122 Bool.and(Bool.not(String.ends_with(s, ":")), is_str(C.plain(s)))))) 123 124 def str.text(+s: String) -> String: 125 when(String, is_safe.plain(s), u => s, u => quoted(s)) 126 127 def tag.text(+tag: String) -> String: 128 +core = {"tag:yaml.org,2002:" : String} 129 Bool.pick(String, String.starts_with(tag, core), "!!" ++ String.drop(tag, String.length(core)), 130 Bool.pick(String, Bool.and(String.starts_with(tag, "!"), Bool.not(String.contains(tag, " "))), tag, "!<" ++ tag ++ ">")) 131 132 def spaces(n: Nat) -> String: 133 String.repeat(" ", n) 134 135 # What goes before a node's first line: indentation at a line's start, a 136 # break and indentation after a key, nothing after "- ". 137 def lead(p: Pos, +ind: Nat) -> String: 138 match p: 139 case Line{}: 140 spaces(ind) 141 case Dash{}: 142 "" 143 case Colon{}: 144 "\n" ++ spaces(ind) 145 case More{}: 146 spaces(ind) 147 148 # A scalar after its key gets a space; at a line's start, indentation. 149 def scalar.lead(p: Pos, +ind: Nat) -> String: 150 match p: 151 case Line{}: 152 spaces(ind) 153 case Dash{}: 154 "" 155 case Colon{}: 156 " " 157 case More{}: 158 spaces(ind) 159 160 def is_more(p: Pos) -> Bool: 161 match p: 162 case More{}: 163 True{} 164 case _: 165 False{} 166 167 def is_collection(v: V.Yaml) -> Bool: 168 match v: 169 case V.Seq{xs}: 170 Bool.not(List.is_empty(&2, V.Yaml, xs)) 171 case V.Mapping{es}: 172 Bool.not(List.is_empty(&2, V.Entry, es)) 173 case _: 174 False{} 175 176 # A scalar's text, or an empty collection's. 177 def scalar.text(v: V.Yaml) -> String: 178 match v: 179 case V.Null{}: 180 "null" 181 case V.Boolean{on}: 182 Bool.pick(String, on, "true", "false") 183 case V.Int{t}: 184 t 185 case V.Float{t}: 186 t 187 case V.Str{t}: 188 str.text(t) 189 case V.Seq{xs}: 190 "[]" 191 case V.Mapping{es}: 192 "{}" 193 case V.Tagged{t, x}: 194 "" 195 196 def is_tagged(v: V.Yaml) -> Bool: 197 match v: 198 case V.Tagged{t, x}: 199 True{} 200 case _: 201 False{} 202 203 # +v+ written at +p+, its lines indented +ind+. 204 def emit(v: V.Yaml, +ind: Nat, +p: Pos) -> String: 205 match v: 206 case V.Seq{xs}: 207 match xs: 208 case Nil{}: 209 Bool.pick(String, is_more(p), "", scalar.lead(p, ind) ++ "[]\n") 210 case Con{x, rest}: 211 lead(p, ind) ++ "- " ++ emit(x, Nat.add(ind, 2n), Dash{}) ++ emit(V.Seq{rest}, ind, More{}) 212 case V.Mapping{es}: 213 match es: 214 case Nil{}: 215 Bool.pick(String, is_more(p), "", scalar.lead(p, ind) ++ "{}\n") 216 case Con{V.Entry{+k, x}, rest}: 217 when(String, is_collection(k) || is_tagged(k), 218 u => lead(p, ind) ++ "? " ++ emit(k, Nat.add(ind, 2n), Dash{}) ++ spaces(ind) ++ ":", 219 u => lead(p, ind) ++ scalar.text(k) ++ ":") ++ 220 emit(x, Nat.add(ind, 2n), Colon{}) ++ emit(V.Mapping{rest}, ind, More{}) 221 case V.Tagged{+tag, +x}: 222 scalar.lead(p, ind) ++ tag.text(tag) ++ Bool.pick(String, is_collection(x), "", " ") ++ 223 emit(x, ind, Bool.pick(Pos, is_collection(x), Colon{}, Dash{})) 224 case _: 225 scalar.lead(p, ind) ++ scalar.text(v) ++ "\n" 226 227 # +v+ as a YAML document. 228 def dump(v: V.Yaml) -> String: 229 emit(v, 0n, Line{}) 230 231 # Values as a stream of documents, each after "---". 232 def dump.all(vs: List<&2, V.Yaml>) -> String: 233 match vs: 234 case Nil{}: 235 "" 236 case Con{v, rest}: 237 "---\n" ++ dump(v) ++ dump.all(rest)