OXYGENHUB
◆ main openmsx / src / config / XMLOutputStream.hh
6.5 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#ifndef XMLOUTPUTSTREAM_HH
2
#define XMLOUTPUTSTREAM_HH
3
4
#include "XMLEscape.hh"
5
6
#include "ranges.hh"
7
8
#include <algorithm>
9
#include <array>
10
#include <concepts>
11
#include <cstdint>
12
#include <string>
13
#include <string_view>
14
#include <vector>
15
16
/**
17
 * 'XMLOutputStream' is a helper to write an XML file in a streaming way.
18
 *
19
 * That is: all information must be provided in the exact same order as it
20
 * appears in the output. So attributes for a tag must be provided before the
21
 * data for that tag. It's not possible to go back to an earlier tag and
22
 * add/change/remove attributes or data, nor is it possible to add/remove
23
 * subtags to already streamed tags.
24
 *
25
 * In other words: this helper class immediately write the provided data to the
26
 * output stream, and it cannot make changes to already outputted data. So
27
 * basically the only thing this class does is take care of the syntax details
28
 * and formatting of the XML file. The main advantage of this class is that it's
29
 * very cheap: it does not need to keep (much) state in memory (e.g. it does not
30
 * build the whole XML tree in memory and then dump it all at the end).
31
 *
32
 * One limitation of this helper is that it does not support mixed-content tags.
33
 * A tag either contains data or it contains subtags, but not both (neither data
34
 * nor subtags is fine).
35
 *
36
 * ---
37
 *
38
 * After instantiation of the 'XMLOutputStream', you must call the
39
 *     begin(), attribute(), data(), end()
40
 * methods. These directly correspond to elements in the XML file, so the
41
 * meaning should be clear. There are also constraints on the order in which
42
 * these methods are allowed to be called:
43
 * - begin() and end() calls should be properly paired. That is for each begin()
44
 *   there must be a corresponding end() and the 'tag' parameter for both must
45
 *   match. One pair must be either fully nested inside another pair or it must
46
 *   be fully outside that other pair.
47
 * - attribute() must be called directly after begin() or after another
48
 *   attribute() call, but not after data() or end()
49
 * - data() can only be called once per tag, it must be called after begin()
50
 *   or after attribute(), but not after data() or end().
51
 * - A call to data() must be followed by a call to end(). Not another data()
52
 *   call nor attribute() nor begin().
53
 *
54
 * ---
55
 *
56
 * 'XMLOutputStream' is templatized on an 'Operations' type. This must be a class
57
 * that implements the following 3 methods:
58
 *     struct MyOperations {
59
 *         void write(const char* buf, size_t len);
60
 *         void write1(char c);
61
 *         void check(bool condition) const;
62
 *     };
63
 *
64
 * The write() and write1() methods write data (e.g. to a file). Either a whole
65
 * buffer or a single character. In some implementations it's more efficient to
66
 * write a single char than to write a buffer of length 1, but in your
67
   implementation feel free to implement write1() as:
68
 *     write1(char c) { write(&c, 1); }
69
 *
70
 * The check() method should be implemented as:
71
 *     assert(condition);
72
 * Though in the unittests we instead make this throw an exception so that we
73
 * can test the error cases.
74
 */
75
template<typename Operations>
76
class XMLOutputStream
77
{
78
public:
79
	explicit XMLOutputStream(Operations& ops_)
80
		: ops(ops_) {}
81
82
	void begin(std::string_view tag);
83
	void attribute(std::string_view name, std::string_view value);
84
	void data(std::string_view value);
85
	void dataRaw(std::string_view value); // if you're sure 'value' doesn't need to be escaped
86
	void end(std::string_view tag);
87
88
	void with_tag(std::string_view tag, std::invocable auto next);
89
90
private:
91
	void writeSpaces(unsigned n);
92
	void writeChar(char c);
93
	void writeString(std::string_view s);
94
	void writeEscapedString(std::string_view s);
95
96
private:
97
	Operations& ops;
98
	unsigned level = 0;
99
	enum State : uint8_t {
100
		INDENT, // the next (begin or end) tag needs to be indented
101
		CLOSE,  // the begin tag still needs to be closed
102
		DATA,   // we got data, but no end-tag yet
103
	};
104
	State state = INDENT;
105
#ifdef DEBUG
106
	std::vector<std::string> stack;
107
#endif
108
};
109
110
111
template<typename Writer>
112
void XMLOutputStream<Writer>::begin(std::string_view tag)
113
{
114
#ifdef DEBUG
115
	stack.emplace_back(tag);
116
#endif
117
	ops.check(state != DATA);
118
	if (state == CLOSE) {
119
		writeString(">\n");
120
	}
121
	writeSpaces(2 * level);
122
	writeChar('<');
123
	writeString(tag);
124
	++level;
125
	state = CLOSE;
126
}
127
128
template<typename Writer>
129
void XMLOutputStream<Writer>::attribute(std::string_view name, std::string_view value)
130
{
131
	ops.check(level > 0);
132
	ops.check(state == CLOSE);
133
	writeChar(' ');
134
	writeString(name);
135
	writeString("=\"");
136
	writeEscapedString(value);
137
	writeChar('"');
138
}
139
140
template<typename Writer>
141
void XMLOutputStream<Writer>::data(std::string_view value)
142
{
143
	ops.check(level > 0);
144
	ops.check(state == CLOSE);
145
146
	if (value.empty()) return;
147
148
	writeChar('>');
149
	writeEscapedString(value);
150
	state = DATA;
151
}
152
153
template<typename Writer>
154
void XMLOutputStream<Writer>::dataRaw(std::string_view value)
155
{
156
	ops.check(level > 0);
157
	ops.check(state == CLOSE);
158
159
	if (value.empty()) return;
160
161
	writeChar('>');
162
	writeString(value); // no escaping here
163
	state = DATA;
164
}
165
166
template<typename Writer>
167
void XMLOutputStream<Writer>::end(std::string_view tag)
168
{
169
#ifdef DEBUG
170
	ops.check(stack.size() == level);
171
	ops.check(!stack.empty());
172
	ops.check(stack.back() == tag);
173
	stack.pop_back();
174
#endif
175
	ops.check(level > 0);
176
	--level;
177
	if (state == CLOSE) {
178
		writeString("/>\n");
179
	} else {
180
		if (state == INDENT) {
181
			writeSpaces(2 * level);
182
		}
183
		writeString("</");
184
		writeString(tag);
185
		writeString(">\n");
186
	}
187
	state = INDENT;
188
}
189
190
template<typename Writer>
191
void XMLOutputStream<Writer>::with_tag(std::string_view tag, std::invocable auto next)
192
{
193
	begin(tag);
194
	next();
195
	end(tag);
196
}
197
198
template<typename Writer>
199
void XMLOutputStream<Writer>::writeSpaces(unsigned n)
200
{
201
	static constexpr std::array<char, 64> spaces = {
202
		' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
203
		' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
204
		' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
205
		' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',
206
	};
207
	while (n) {
208
		auto t = std::min(64u, n);
209
		ops.write(subspan(spaces, 0, t));
210
		n -= t;
211
	}
212
}
213
214
template<typename Writer>
215
void XMLOutputStream<Writer>::writeChar(char c)
216
{
217
	ops.write1(c);
218
}
219
220
template<typename Writer>
221
void XMLOutputStream<Writer>::writeString(std::string_view s)
222
{
223
	ops.write(s);
224
}
225
226
template<typename Writer>
227
void XMLOutputStream<Writer>::writeEscapedString(std::string_view s)
228
{
229
	XMLEscape(s, [&](std::string_view chunk) { writeString(chunk); });
230
}
231
232
#endif