1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
| #### libremiliacr
#### Copyright(C) 2020-2024 Remilia Scarlet <remilia@posteo.jp>
####
#### This program is free software: you can redistribute it and/or modify it
#### under the terms of the GNU General Public License as published the Free
#### Software Foundation, either version 3 of the License, or (at your option)
#### any later version.
####
#### This program is distributed in the hope that it will be useful, but WITHOUT
#### ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#### FITNESS FOR A PARTICULAR PURPOSE.See the GNU General Public License for
#### more details.
####
#### You should have received a copy of the GNU General Public License along
#### with this program.If not, see<http:####www.gnu.org/licenses/.>
require "./common"
module RemiLib::RSConf
# The `Writer` class is used to generate properly-formatted RSConf data from
# `RSValue`s. There is also the `RemiLib::RSConf::Builder` class, which is
# much more flexible.
#
# ```crystal
# require "libremiliacr"
#
# root = RemiLib::RSConf::RSObject.new
# root["test"] = 69
# root["test2"] = "Hello, world!"
#
# io = IO::Memory.new
# writer = RemiLib::RSConf::Writer.new(io)
# writer.write(root)
# puts io.to_s
# ```
class Writer
# Returns the underlying `IO`.
getter stream : IO
@indent : Int64 = 0
@toplevelWritten : Bool = false
# The size of the indent (ASCII spaces).
property indentSize : UInt32 = 2
# When true, then key names are always quoted, even when they don't need to
# be. Otherwise they are only quoted when necessary.
property? alwaysQuoteKeys : Bool = false
# When true, then float values are always written using scientific notation.
property? alwaysUseScientificNotation : Bool = false
# When true, and the toplevel value is an `RSObject`, then braces will be
# inserted around it (similar to JSON). Otherwise these are omitted, as
# permitted by the specs.
property? explicitRootObject : Bool = false
# When true, then a comma is always inserted after values, otherwise commas
# are omitted.
property? commaAfterValues : Bool = false
# When true, then an extra newline is added after every topevel key/value
# pair (i.e., an extra blank line between each pair). Otherwise only a
# single newline is emitted after each pair.
property? extraNewlineBetweenToplevelKeys : Bool = false
# Creates a new `Writer` instance that will write data to *stream*.
def initialize(@stream : IO)
unless @stream.encoding == "UTF-8"
raise RSConfEncodingError.new("Unsupported stream encoding: #{@stream.encoding}")
end
end
# Writes RSConf data to the given `IO`.
def self.write(dest : IO, data : RSTopLevel) : Nil
writer : Writer = Writer.new(dest)
writer.write(data)
end
# Writes RSConf data to a new string and returns it.
def self.write(data : RSTopLevel) : String
ret = IO::Memory.new
write(ret, data)
ret.to_s
end
@[AlwaysInline]
protected def writeIndent(size : Int) : Nil
@stream << (" " * size)
end
@[AlwaysInline]
protected def writeQuotedString(str : String) : Nil
@stream << '"' << str.gsub("\"", "\\\"") << '"'
end
@[AlwaysInline]
protected def writeUnuotedString(str : String) : Nil
@stream << str
end
protected def writeValue(val : RSValue) : Nil
case val
in RSObject
writeObject(val)
in RSScalar
case
when val.isString?
writeQuotedString(val.string)
@stream << ',' if @commaAfterValues
when val.isFloat?
if @alwaysUseScientificNotation
@stream << sprintf("%e", val.float).gsub('e', 'd')
else
@stream << sprintf("%f", val.float)
end
@stream << ',' if @commaAfterValues
when val.isInt?
@stream << val.int
@stream << ',' if @commaAfterValues
when val.isBool?
@stream << (val.bool ? "true" : "false")
@stream << ',' if @commaAfterValues
when val.isNil?
@stream << "nil"
@stream << ',' if @commaAfterValues
end
in RSArray
writeArray(val)
end
@stream << '\n'
end
protected def writeArray(val : RSArray) : Nil
@stream << "[\n"
@indent += @indentSize
val.each do |elt|
writeIndent(@indent)
writeValue(elt)
end
@indent = Math.max(0i64, @indent - @indentSize)
writeIndent(@indent)
if @commaAfterValues
@stream << "],\n"
else
@stream << "],"
end
end
@[AlwaysInline]
protected def keyNeedsQuotes(key : String) : Bool
key.includes?(':') ||
key.includes?('[') ||
key.includes?(']') ||
key.includes?('{') ||
key.includes?('}') ||
key.includes?('"')
end
protected def writeObjectPairs(data : RSObject, extraNewline : Bool = false) : Nil
data.each do |key, val|
writeIndent(@indent)
# Do we need quotes?
if @alwaysQuoteKeys || keyNeedsQuotes(key)
writeQuotedString(key)
else
writeUnuotedString(key)
end
@stream << ": "
writeValue(val)
@stream << '\n' if extraNewline
end
end
protected def writeObject(data : RSObject) : Nil
@stream << "{\n"
@indent += @indentSize
writeObjectPairs(data)
@indent = Math.max(0i64, @indent - @indentSize)
writeIndent(@indent)
if @commaAfterValues
@stream << "},\n"
else
@stream << "},"
end
end
# Writes the toplevel `RSObject` value. After this, no more writing can
# happen until `#stream=` is called.
def write(data : RSObject) : Nil
raise RSConfError.new("Cannot write an additional toplevel") if @toplevelWritten
if @explicitRootObject
@stream << "{\n"
@indent += @indentSize
end
writeObjectPairs(data, @extraNewlineBetweenToplevelKeys)
if @explicitRootObject
@stream << "}"
@indent = Math.max(0i64, @indent - @indentSize)
end
@stream << '\n'
@toplevelWritten = true
end
# Writes the toplevel `RSArray` value. After this, no more writing can
# happen until `#stream=` is called.
def write(data : RSArray) : Nil
raise RSConfError.new("Cannot write an additional toplevel") if @toplevelWritten
writeArray(data)
@stream << '\n'
@toplevelWritten = true
end
# Changes the destination stream. Calling this effectively "resets" the
# `Writer` instance, allowing you to call `#write` again to generate new
# RSConf data. The old stream is NOT closed.
def stream=(value : IO) : Nil
@stream = value
@toplevelWritten = false
end
end
end
|