emit.bend

yaml-for-bend@0.1.1.0 · 237 lines · 28 definitions

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
10import Base
11
12import yaml-for-bend@0.1.1.0/value.bend as V
13
14import yaml-for-bend@0.1.1.0/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.
19type Pos is Data:
20  Line{}
21  Dash{}
22  Colon{}
23  More{}
24
25def 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
32def code(c: Char) -> U32:
33  match c:
34    case Chr{x}:
35      x
36
37def is_in(c: Char, s: String) -> Bool:
38  String.contains(s, SCon{c, SNil{}})
39
40# Quoting
41# -------
42
43def 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.
47def 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.
56def 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
60def 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
70def 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
85def escaped(s: String) -> String:
86  match s:
87    case SNil{}:
88      ""
89    case SCon{c, t}:
90      escape(c) ++ escaped(t)
91
92def quoted(s: String) -> String:
93  "\"" ++ escaped(s) ++ "\""
94
95def 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
102def last.char(s: String) -> Char:
103  Maybe.default(&2, Char, String.get(String.reverse(s), 0n), ' ')
104
105def first.char(s: String) -> Char:
106  Maybe.default(&2, Char, String.get(s, 0n), ' ')
107
108def 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.
118def 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
124def str.text(+s: String) -> String:
125  when(String, is_safe.plain(s), u => s, u => quoted(s))
126
127def 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
132def 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 "- ".
137def 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.
149def 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
160def is_more(p: Pos) -> Bool:
161  match p:
162    case More{}:
163      True{}
164    case _:
165      False{}
166
167def 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.
177def 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
196def 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+.
204def 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.
228def dump(v: V.Yaml) -> String:
229  emit(v, 0n, Line{})
230
231# Values as a stream of documents, each after "---".
232def 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)