1 | // Copyright (C) 2016 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 |
3 | |
4 | #include "xmloutput.h" |
5 | |
6 | QT_BEGIN_NAMESPACE |
7 | |
8 | XmlOutput::XmlOutput(QTextStream &file, ConverstionType type) |
9 | : xmlFile(file), indent("\t" ), currentLevel(0), currentState(Bare), format(NewLine), |
10 | conversion(type) |
11 | { |
12 | tagStack.clear(); |
13 | } |
14 | |
15 | XmlOutput::~XmlOutput() |
16 | { |
17 | closeAll(); |
18 | } |
19 | |
20 | // Settings ------------------------------------------------------------------ |
21 | void XmlOutput::setIndentString(const QString &indentString) |
22 | { |
23 | indent = indentString; |
24 | } |
25 | |
26 | QString XmlOutput::indentString() |
27 | { |
28 | return indent; |
29 | } |
30 | |
31 | void XmlOutput::setIndentLevel(int level) |
32 | { |
33 | currentLevel = level; |
34 | } |
35 | |
36 | int XmlOutput::indentLevel() |
37 | { |
38 | return currentLevel; |
39 | } |
40 | |
41 | void XmlOutput::setState(XMLState state) |
42 | { |
43 | currentState = state; |
44 | } |
45 | |
46 | void XmlOutput::setFormat(XMLFormat newFormat) |
47 | { |
48 | format = newFormat; |
49 | } |
50 | |
51 | XmlOutput::XMLState XmlOutput::state() |
52 | { |
53 | return currentState; |
54 | } |
55 | |
56 | void XmlOutput::updateIndent() |
57 | { |
58 | currentIndent.clear(); |
59 | currentIndent.reserve(asize: currentLevel); |
60 | for (int i = 0; i < currentLevel; ++i) |
61 | currentIndent.append(s: indent); |
62 | } |
63 | |
64 | void XmlOutput::increaseIndent() |
65 | { |
66 | ++currentLevel; |
67 | updateIndent(); |
68 | } |
69 | |
70 | void XmlOutput::decreaseIndent() |
71 | { |
72 | if (currentLevel) |
73 | --currentLevel; |
74 | updateIndent(); |
75 | if (!currentLevel) |
76 | currentState = Bare; |
77 | } |
78 | |
79 | QString XmlOutput::doConversion(const QString &text) |
80 | { |
81 | if (!text.size()) |
82 | return QString(); |
83 | else if (conversion == NoConversion) |
84 | return text; |
85 | |
86 | QString output; |
87 | if (conversion == XMLConversion) { |
88 | |
89 | // this is a way to escape characters that shouldn't be converted |
90 | for (int i=0; i<text.size(); ++i) { |
91 | const QChar c = text.at(i); |
92 | if (c == QLatin1Char('&')) { |
93 | if ( (i + 7) < text.size() && |
94 | text.at(i: i + 1) == QLatin1Char('#') && |
95 | text.at(i: i + 2) == QLatin1Char('x') && |
96 | text.at(i: i + 7) == QLatin1Char(';') ) { |
97 | output += text.at(i); |
98 | } else { |
99 | output += QLatin1String("&" ); |
100 | } |
101 | } else if (c == QLatin1Char('<')) { |
102 | output += QLatin1String("<" ); |
103 | } else if (c == QLatin1Char('>')) { |
104 | output += QLatin1String(">" ); |
105 | } else { |
106 | if (c.unicode() < 0x20) { |
107 | output += QString("&#x%1;" ).arg(a: c.unicode(), fieldWidth: 2, base: 16, fillChar: QLatin1Char('0')); |
108 | } else { |
109 | output += c; |
110 | } |
111 | } |
112 | } |
113 | } else { |
114 | output = text; |
115 | } |
116 | |
117 | if (conversion == XMLConversion) { |
118 | output.replace(c: '\"', after: QLatin1String(""" )); |
119 | output.replace(c: '\'', after: QLatin1String("'" )); |
120 | } else if (conversion == EscapeConversion) { |
121 | output.replace(c: '\"', after: QLatin1String("\\\"" )); |
122 | output.replace(c: '\'', after: QLatin1String("\\\'" )); |
123 | } |
124 | return output; |
125 | } |
126 | |
127 | // Stream functions ---------------------------------------------------------- |
128 | XmlOutput& XmlOutput::operator<<(const QString& o) |
129 | { |
130 | return operator<<(o: data(text: o)); |
131 | } |
132 | |
133 | XmlOutput& XmlOutput::operator<<(const xml_output& o) |
134 | { |
135 | switch(o.xo_type) { |
136 | case tNothing: |
137 | break; |
138 | case tRaw: |
139 | addRaw(rawText: o.xo_text); |
140 | break; |
141 | case tDeclaration: |
142 | addDeclaration(version: o.xo_text, encoding: o.xo_value); |
143 | break; |
144 | case tTag: |
145 | newTagOpen(tag: o.xo_text); |
146 | break; |
147 | case tTagValue: |
148 | addRaw(rawText: QString("\n%1<%2>" ).arg(a: currentIndent).arg(a: o.xo_text)); |
149 | addRaw(rawText: doConversion(text: o.xo_value)); |
150 | addRaw(rawText: QString("</%1>" ).arg(a: o.xo_text)); |
151 | break; |
152 | case tValueTag: |
153 | addRaw(rawText: doConversion(text: o.xo_text)); |
154 | setFormat(NoNewLine); |
155 | closeTag(); |
156 | setFormat(NewLine); |
157 | break; |
158 | case tImport: |
159 | addRaw(rawText: QString("\n%1<Import %2=\"%3\" />" ).arg(a: currentIndent).arg(a: o.xo_text).arg(a: o.xo_value)); |
160 | break; |
161 | case tCloseTag: |
162 | if (o.xo_value.size()) |
163 | closeAll(); |
164 | else if (o.xo_text.size()) |
165 | closeTo(tag: o.xo_text); |
166 | else |
167 | closeTag(); |
168 | break; |
169 | case tAttribute: |
170 | addAttribute(attribute: o.xo_text, value: o.xo_value); |
171 | break; |
172 | case tAttributeTag: |
173 | addAttributeTag(attribute: o.xo_text, value: o.xo_value); |
174 | break; |
175 | case tData: |
176 | { |
177 | // Special case to be able to close tag in normal |
178 | // way ("</tag>", not "/>") without using addRaw().. |
179 | if (!o.xo_text.size()) { |
180 | closeOpen(); |
181 | break; |
182 | } |
183 | QString output = doConversion(text: o.xo_text); |
184 | output.replace(c: '\n', after: "\n" + currentIndent); |
185 | addRaw(rawText: QString("\n%1%2" ).arg(a: currentIndent).arg(a: output)); |
186 | } |
187 | break; |
188 | case tComment: |
189 | { |
190 | QString output("<!--%1-->" ); |
191 | addRaw(rawText: output.arg(a: o.xo_text)); |
192 | } |
193 | break; |
194 | case tCDATA: |
195 | { |
196 | QString output("<![CDATA[\n%1\n]]>" ); |
197 | addRaw(rawText: output.arg(a: o.xo_text)); |
198 | } |
199 | break; |
200 | } |
201 | return *this; |
202 | } |
203 | |
204 | |
205 | // Output functions ---------------------------------------------------------- |
206 | void XmlOutput::newTag(const QString &tag) |
207 | { |
208 | Q_ASSERT_X(tag.size(), "XmlOutput" , "Cannot open an empty tag" ); |
209 | newTagOpen(tag); |
210 | closeOpen(); |
211 | } |
212 | |
213 | void XmlOutput::newTagOpen(const QString &tag) |
214 | { |
215 | Q_ASSERT_X(tag.size(), "XmlOutput" , "Cannot open an empty tag" ); |
216 | closeOpen(); |
217 | |
218 | if (format == NewLine) |
219 | xmlFile << Qt::endl << currentIndent; |
220 | xmlFile << '<' << doConversion(text: tag); |
221 | currentState = Attribute; |
222 | tagStack.append(t: tag); |
223 | increaseIndent(); // ---> indent |
224 | } |
225 | |
226 | void XmlOutput::closeOpen() |
227 | { |
228 | switch(currentState) { |
229 | case Bare: |
230 | case Tag: |
231 | return; |
232 | case Attribute: |
233 | break; |
234 | } |
235 | xmlFile << '>'; |
236 | currentState = Tag; |
237 | } |
238 | |
239 | void XmlOutput::closeTag() |
240 | { |
241 | switch(currentState) { |
242 | case Bare: |
243 | if (tagStack.size()) |
244 | //warn_msg(WarnLogic, "<Root>: Cannot close tag in Bare state, %d tags on stack", tagStack.count()); |
245 | qDebug(msg: "<Root>: Cannot close tag in Bare state, %d tags on stack" , int(tagStack.size())); |
246 | else |
247 | //warn_msg(WarnLogic, "<Root>: Cannot close tag, no tags on stack"); |
248 | qDebug(msg: "<Root>: Cannot close tag, no tags on stack" ); |
249 | return; |
250 | case Tag: |
251 | decreaseIndent(); // <--- Pre-decrease indent |
252 | if (format == NewLine) |
253 | xmlFile << Qt::endl << currentIndent; |
254 | xmlFile << "</" << doConversion(text: tagStack.last()) << '>'; |
255 | tagStack.pop_back(); |
256 | break; |
257 | case Attribute: |
258 | xmlFile << " />" ; |
259 | tagStack.pop_back(); |
260 | currentState = Tag; |
261 | decreaseIndent(); // <--- Post-decrease indent |
262 | break; |
263 | } |
264 | } |
265 | |
266 | void XmlOutput::closeTo(const QString &tag) |
267 | { |
268 | bool cont = true; |
269 | if (!tagStack.contains(str: tag) && !tag.isNull()) { |
270 | //warn_msg(WarnLogic, "<%s>: Cannot close to tag <%s>, not on stack", tagStack.last().latin1(), tag.latin1()); |
271 | qDebug(msg: "<%s>: Cannot close to tag <%s>, not on stack" , tagStack.last().toLatin1().constData(), tag.toLatin1().constData()); |
272 | return; |
273 | } |
274 | int left = tagStack.size(); |
275 | while (left-- && cont) { |
276 | cont = tagStack.last().compare(s: tag) != 0; |
277 | closeTag(); |
278 | } |
279 | } |
280 | |
281 | void XmlOutput::closeAll() |
282 | { |
283 | if (!tagStack.size()) |
284 | return; |
285 | closeTo(tag: QString()); |
286 | } |
287 | |
288 | void XmlOutput::addDeclaration(const QString &version, const QString &encoding) |
289 | { |
290 | switch(currentState) { |
291 | case Bare: |
292 | break; |
293 | case Tag: |
294 | case Attribute: |
295 | //warn_msg(WarnLogic, "<%s>: Cannot add declaration when not in bare state", tagStack.last().toLatin1().constData()); |
296 | qDebug(msg: "<%s>: Cannot add declaration when not in bare state" , tagStack.last().toLatin1().constData()); |
297 | return; |
298 | } |
299 | QString outData = QString("<?xml version=\"%1\" encoding=\"%2\"?>" ) |
300 | .arg(a: doConversion(text: version)) |
301 | .arg(a: doConversion(text: encoding)); |
302 | addRaw(rawText: outData); |
303 | } |
304 | |
305 | void XmlOutput::addRaw(const QString &rawText) |
306 | { |
307 | closeOpen(); |
308 | xmlFile << rawText; |
309 | } |
310 | |
311 | void XmlOutput::addAttribute(const QString &attribute, const QString &value) |
312 | { |
313 | switch(currentState) { |
314 | case Bare: |
315 | case Tag: |
316 | //warn_msg(WarnLogic, "<%s>: Cannot add attribute since tags not open", tagStack.last().toLatin1().constData()); |
317 | qDebug(msg: "<%s>: Cannot add attribute (%s) since tag's not open" , |
318 | (tagStack.size() ? tagStack.last().toLatin1().constData() : "Root" ), |
319 | attribute.toLatin1().constData()); |
320 | return; |
321 | case Attribute: |
322 | break; |
323 | } |
324 | if (format == NewLine) |
325 | xmlFile << Qt::endl; |
326 | xmlFile << currentIndent << doConversion(text: attribute) << "=\"" << doConversion(text: value) << "\"" ; |
327 | } |
328 | |
329 | void XmlOutput::addAttributeTag(const QString &attribute, const QString &value) |
330 | { |
331 | switch(currentState) { |
332 | case Bare: |
333 | case Tag: |
334 | //warn_msg(WarnLogic, "<%s>: Cannot add attribute since tags not open", tagStack.last().toLatin1().constData()); |
335 | qDebug(msg: "<%s>: Cannot add attribute (%s) since tag's not open" , |
336 | (tagStack.size() ? tagStack.last().toLatin1().constData() : "Root" ), |
337 | attribute.toLatin1().constData()); |
338 | return; |
339 | case Attribute: |
340 | break; |
341 | } |
342 | xmlFile << " " << doConversion(text: attribute) << "=\"" << doConversion(text: value) << "\"" ; |
343 | } |
344 | |
345 | QT_END_NAMESPACE |
346 | |