| 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 | |