LCOV - code coverage report
Current view: top level - src/app - generator.cpp (source / functions) Coverage Total Hit
Project: Smithy Qt Lines: 0.0 % 259 0
Version: 0.1.0-pre Functions: 0.0 % 20 0

            Line data    Source code
       1              : // SPDX-FileCopyrightText: 2013-2025 Paul Colby <git@colby.id.au>
       2              : // SPDX-License-Identifier: LGPL-3.0-or-later
       3              : 
       4              : #include "generator.h"
       5              : #include "renderer.h"
       6              : 
       7              : #include <qtsmithy/model.h>
       8              : 
       9              : #include <QDebug>
      10              : #include <QDirIterator>
      11              : #include <QJsonParseError>
      12              : #include <QRegularExpression>
      13              : 
      14              : #define QSL(str) QStringLiteral(str) // Shorten the QStringLiteral macro for readability.
      15              : 
      16              : class ScopedContext
      17              : {
      18              : public:
      19              :     ScopedContext(Renderer * renderer, const QVariantHash &hash) : renderer(renderer)
      20              :     {
      21            0 :         renderer->push(hash);
      22            0 :     }
      23              : 
      24              :     ScopedContext() {
      25              :         renderer->pop();
      26              :     }
      27              : 
      28              : private:
      29              :     Renderer * renderer;
      30              : };
      31              : 
      32              : 
      33              : const QRegularExpression Generator::servicePattern{
      34              :     QSL("(?<type>Service)(?<seperator>[^a-zA-Z0-9]*)(?<id>Title|Id|sdkId)"),
      35              :     QRegularExpression::CaseInsensitiveOption
      36              : };
      37              : 
      38              : const QRegularExpression Generator::operationPattern{
      39              :     QSL("(?<type>Operation)(?<seperator>[^a-zA-Z0-9]*)(?<id>Name)"),
      40              :     QRegularExpression::CaseInsensitiveOption
      41              : };
      42              : 
      43            0 : Generator::Generator(const smithy::Model * const model, Renderer * const renderer)
      44            0 :     : model(model), renderer(renderer)
      45              : {
      46              :     Q_ASSERT(model->isValid());
      47              :     Q_ASSERT(renderer);
      48            0 : }
      49              : 
      50              : 
      51            0 : int Generator::expectedFileCount() const
      52              : {
      53            0 :     const QHash<smithy::ShapeId, smithy::Shape> services = model->shapes(smithy::Shape::Type::Service);
      54            0 :     const int operations = std::accumulate(services.constBegin(), services.constEnd(), 0,
      55            0 :         [](const int a, const smithy::Shape &shape) { return a + shape.operations().size();
      56            0 :     });
      57            0 :     const QStringList templates = renderer->templatesNames();
      58            0 :     return std::accumulate(templates.constBegin(), templates.constEnd(), 0,
      59            0 :         [&](const int a, const QString &name) {
      60            0 :             return a + (servicePattern.match(name).hasMatch() ?
      61            0 :                 (operationPattern.match(name).hasMatch() ? operations : services.size()) : 1);
      62            0 :         });
      63            0 : }
      64              : 
      65            0 : bool Generator::generate(const QString &outputDir, ClobberMode clobberMode)
      66              : {
      67              :     // Add initial context.
      68            0 :     QVariantMap servicesMap;
      69            0 :     const QHash<smithy::ShapeId, smithy::Shape> services = model->shapes(smithy::Shape::Type::Service);
      70            0 :     for (const smithy::Shape &service: services) {
      71            0 :         servicesMap.insert(service.id().toString(), toContext(service));
      72              :     }
      73            0 :     const ScopedContext context(renderer, { { QSL("services"), servicesMap } });
      74              : 
      75              :     // Build the list of template names.
      76            0 :     const QStringList templatesNames = renderer->templatesNames();
      77            0 :     QStringList serviceTemplateNames, operationTemplateNames, plainTemplateNames;
      78            0 :     for (const QString &name: templatesNames) {
      79            0 :         ((servicePattern.match(name).hasMatch()) ? (operationPattern.match(name).hasMatch() ?
      80            0 :             operationTemplateNames : serviceTemplateNames) : plainTemplateNames).append(name);
      81              :     }
      82              :     Q_ASSERT(serviceTemplateNames.size() + operationTemplateNames.size()
      83              :              + plainTemplateNames.size() == templatesNames.size());
      84              : 
      85              :     // Render all of the services.
      86            0 :     if (!std::all_of(services.constBegin(), services.constEnd(), [&](const smithy::Shape &service) {
      87            0 :          return renderService(service, serviceTemplateNames, operationTemplateNames, outputDir, clobberMode);
      88              :     })) {
      89            0 :         return false;
      90              :     }
      91              : 
      92              :     // Render all of the one-off (not per-service) files.
      93            0 :     qCDebug(lc).noquote() << tr("Rendering %n unary template/s", nullptr, plainTemplateNames.size());
      94            0 :     return std::all_of(plainTemplateNames.constBegin(), plainTemplateNames.constEnd(),
      95            0 :         [&](const QString &templateName){
      96            0 :             const QString outputPathName = outputDir + QLatin1Char('/') + templateName;
      97            0 :             return render(templateName, outputPathName, clobberMode);
      98            0 :     });
      99            0 : }
     100              : 
     101            0 : QStringList Generator::renderedFiles() const
     102              : {
     103            0 :     return renderedPathNames;
     104              : }
     105              : 
     106            0 : QStringList Generator::skippedFiles() const
     107              : {
     108            0 :     return skippedPathNames;
     109              : }
     110              : 
     111            0 : bool Generator::renderService(const smithy::Shape &service,
     112              :                               const QStringList &serviceTemplateNames,
     113              :                               const QStringList &operationTemplateNames,
     114              :                               const QString &outputDir, ClobberMode &clobberMode)
     115              : {
     116            0 :     qCDebug(lc).noquote() << tr("Rendering templates for service %1").arg(service.id().toString());
     117              : 
     118              :     // Add renderer context for this service.
     119            0 :     const QVariantHash serviceContext = toContext(service);
     120              :     // cppcheck-suppress unreadVariable
     121            0 :     const ScopedContext context(renderer, { { QSL("service"), serviceContext } });
     122              : 
     123              :     // Render each service template.
     124            0 :     for (const QString &templateName: serviceTemplateNames) {
     125            0 :         const QString outputPathName = makeOutputPathName(templateName, servicePattern, {
     126            0 :             { QSL("name"), service.id().shapeName() },
     127            0 :             { QSL("id"), serviceContext.value(QSL("canonicalId")).toString() },
     128            0 :             { QSL("sdkid"), serviceContext.value(QSL("sdkId")).toString() },
     129            0 :         }, outputDir);
     130            0 :         if (!render(templateName, outputPathName, clobberMode)) {
     131              :             return false;
     132              :         }
     133            0 :     }
     134              : 
     135              :     // Render each of the service's operation.
     136            0 :     const QVariantMap operations = serviceContext.value(QSL("operations")).toMap();
     137            0 :     for (auto iter = operations.constBegin(); iter != operations.constEnd(); ++iter) {
     138            0 :         const smithy::Shape operation = model->shape(iter->toHash().value(QSL("shapeId")).toString());
     139            0 :         if (!operation.isValid()) {
     140            0 :             qCCritical(lc).noquote() << tr("Failed to find shape for %1 operation in %2 service")
     141            0 :                 .arg(iter.key(), service.id().toString());
     142            0 :             return false;
     143              :         }
     144            0 :         if (!renderOperation(operation, serviceContext, operationTemplateNames, outputDir, clobberMode)) {
     145              :             return false;
     146              :         }
     147            0 :     }
     148              :     return true;
     149            0 : }
     150              : 
     151            0 : bool Generator::renderOperation(const smithy::Shape &operation, const QVariantHash serviceContext,
     152              :                                 const QStringList &templateNames, const QString &outputDir,
     153              :                                 ClobberMode &clobberMode)
     154              : {
     155            0 :     qCDebug(lc).noquote() << tr("Rendering templates for operation %1").arg(operation.id().toString());
     156              : 
     157              :     // Add renderer context for this operation.
     158              :     // cppcheck-suppress unreadVariable
     159            0 :     const ScopedContext context(renderer, { { QSL("operation"), toContext(operation) } });
     160            0 :     for (const QString &templateName: templateNames) {
     161            0 :         QString outputPathName = makeOutputPathName(templateName, servicePattern, {
     162            0 :             { QSL("name"), serviceContext.value(QSL("shapeName")).toString() },
     163            0 :             { QSL("id"), serviceContext.value(QSL("canonicalId")).toString() },
     164            0 :             { QSL("sdkid"), serviceContext.value(QSL("sdkId")).toString() },
     165            0 :         });
     166            0 :         outputPathName = makeOutputPathName(outputPathName, operationPattern, {
     167            0 :             { QSL("name"), operation.id().shapeName() },
     168              :         }, outputDir);
     169            0 :         if (!render(templateName, outputPathName, clobberMode)) {
     170              :             return false;
     171              :         }
     172            0 :     }
     173              :     return true;
     174            0 : }
     175              : 
     176            0 : bool Generator::render(const QString &templateName, const QString &outputPathName,
     177              :                        ClobberMode &clobberMode)
     178              : {
     179            0 :     if (QFile::exists(outputPathName)) {
     180            0 :         switch (clobberMode) {
     181            0 :         case ClobberMode::Overwrite:
     182            0 :             qCDebug(lc) << tr("Overwriting existing file: %1").arg(outputPathName);
     183            0 :             break;
     184            0 :         case ClobberMode::Prompt:
     185            0 :             if (promptToOverwrite(outputPathName, clobberMode)) {
     186              :                 break; // User said yes, so jump out of case statement.
     187              :             }
     188              :             __attribute__((fallthrough));
     189              :         case ClobberMode::Skip:
     190            0 :             qCDebug(lc) << tr("Skipping existing output file: %1").arg(outputPathName);
     191            0 :             skippedPathNames.append(outputPathName);
     192            0 :             return true;
     193              :         }
     194              :     }
     195            0 :     if (!renderer->render(templateName, outputPathName)) {
     196              :         return false;
     197              :     }
     198            0 :     renderedPathNames.append(outputPathName);
     199            0 :     return true;
     200              : }
     201              : 
     202              : // Turn shape into a context hash. Note, only service and operation shapes supported for now.
     203            0 : QVariantHash Generator::toContext(const smithy::Shape &shape) const
     204              : {
     205            0 :     if (shape.type() == smithy::Shape::Type::Service) {
     206            0 :         QVariantHash hash = shape.rawAst().toVariantHash();
     207            0 :         hash.insert(QSL("shapeName"), shape.id().shapeName());
     208            0 :         hash.insert(QSL("canonicalId"), canonicalServiceId(shape.traits()
     209            0 :             .value(QSL("aws.api#service")).toObject().value(QSL("sdkId")).toString()));
     210            0 :         hash.insert(QSL("sdkId"), shape.traits().value(QSL("aws.api#service")).toObject()
     211            0 :             .value(QSL("sdkId")).toString());
     212            0 :         hash.insert(QSL("documentation"), formatHtmlDocumentation(
     213            0 :             shape.traits().value(QSL("smithy.api#documentation")).toString()));
     214            0 :         QVariantMap operations;
     215            0 :         const smithy::Shape::ShapeReferences operationRefs = shape.operations();
     216            0 :         for (const smithy::Shape::ShapeReference &operationRef: operationRefs) {
     217            0 :             operations.insert(operationRef.target.shapeName(),
     218            0 :                               toContext(model->shape(operationRef.target)));
     219              :         }
     220              :         Q_ASSERT(operationRefs.size() == operations.size());
     221            0 :         const smithy::Shape::ShapeReferences resourceRefs = shape.resources();
     222            0 :         for (const smithy::Shape::ShapeReference &resourceRef: resourceRefs) {
     223            0 :             const QVariantHash resourceContext = toContext(model->shape(resourceRef.target));
     224              :             // resourceContext will contain the raw AST, and probably other things too over time,
     225              :             // but for now (to replicate historical aws-sdk-qt codegen behaviour) we just want to
     226              :             // add the resource operations to the service operations list.
     227            0 :             const QVariantMap resourceOperations = resourceContext.value(QSL("operations")).toMap();
     228            0 :             for (auto iter = resourceOperations.constBegin(); iter != resourceOperations.constEnd(); ++iter) {
     229            0 :                 operations.insert(iter.key(), iter.value());
     230              :             }
     231            0 :         }
     232            0 :         hash.insert(QSL("operations"), operations);
     233              :         return hash;
     234            0 :     }
     235              : 
     236            0 :     if (shape.type() == smithy::Shape::Type::Operation) {
     237            0 :         QVariantHash hash = shape.rawAst().toVariantHash();
     238            0 :         hash.insert(QSL("name"), shape.id().shapeName());
     239            0 :         hash.insert(QSL("shapeId"), shape.id().toString());
     240            0 :         hash.insert(QSL("documentation"), formatHtmlDocumentation(
     241            0 :             shape.traits().value(QSL("smithy.api#documentation")).toString()));
     242              :         return hash;
     243            0 :     }
     244              : 
     245            0 :     if (shape.type() == smithy::Shape::Type::Resource) {
     246            0 :         QVariantHash hash = shape.rawAst().toVariantHash();
     247            0 :         QVariantMap operations;
     248              :         #define QTSMITHY_IF_VALID_INSERT(action) { \
     249              :             const smithy::ShapeId action##TargetId = shape.action().target; \
     250              :             if (action##TargetId.isValid()) {                   \
     251              :                 operations.insert(action##TargetId.shapeName(), \
     252              :                     toContext(model->shape(action##TargetId))); \
     253              :             } \
     254              :         }
     255            0 :         QTSMITHY_IF_VALID_INSERT(create)
     256            0 :         QTSMITHY_IF_VALID_INSERT(put)
     257            0 :         QTSMITHY_IF_VALID_INSERT(read)
     258            0 :         QTSMITHY_IF_VALID_INSERT(update)
     259            0 :         QTSMITHY_IF_VALID_INSERT(Delete)
     260            0 :         QTSMITHY_IF_VALID_INSERT(list)
     261              :         #undef QTSMITHY_IF_VALID_INSERT
     262              : 
     263              :         #define QTSMITHY_ADD_SHAPES(property) { \
     264              :             const smithy::Shape::ShapeReferences refs = shape.property(); \
     265              :             for (const smithy::Shape::ShapeReference &ref: refs) { \
     266              :                 operations.insert(ref.target.shapeName(), toContext(model->shape(ref.target))); \
     267              :             } \
     268              :         }
     269            0 :         QTSMITHY_ADD_SHAPES(operations)
     270            0 :         QTSMITHY_ADD_SHAPES(collectionOperations)
     271              :         #undef QTSMITHY_ADD_SHAPES
     272              : 
     273            0 :         const smithy::Shape::ShapeReferences resourceRefs = shape.resources();
     274            0 :         for (const smithy::Shape::ShapeReference &resourceRef: resourceRefs) {
     275            0 :             const QVariantHash resourceContext = toContext(model->shape(resourceRef.target));
     276              :             // resourceContext will contain the raw AST, and probably other things too over time,
     277              :             // but for now (to replicate historical aws-sdk-qt codegen behaviour) we just want to
     278              :             // add the resource operations to the service operations list.
     279            0 :             const QVariantMap resourceOperations = resourceContext.value(QSL("operations")).toMap();
     280            0 :             for (auto iter = resourceOperations.constBegin(); iter != resourceOperations.constEnd(); ++iter) {
     281            0 :                 operations.insert(iter.key(), iter.value());
     282              :             }
     283            0 :         }
     284              : 
     285            0 :         hash.insert(QSL("operations"), operations);
     286              :         return hash;
     287            0 :     }
     288              : 
     289            0 :     qCCritical(lc).noquote() << tr("Cannot generate context for shape type 0x%1")
     290            0 :         .arg((int)shape.type(), 0, 16);
     291            0 :     return QVariantHash{};
     292              : }
     293              : 
     294              : 
     295            0 : const QString Generator::canonicalServiceId(const QString &sdkId)
     296              : {
     297              :     // Handle some hard-coded special cases that are hard to automate without a dictionary.
     298              :     const QMap<QString,QString> specialCases{
     299            0 :         { QSL("identitystore"   ), QSL("IdentityStore")    },
     300            0 :         { QSL("ivschat"         ), QSL("IvsChat")          },
     301            0 :         { QSL("forecastquery"   ), QSL("ForecastQuery")    },
     302            0 :         { QSL("resiliencehub"   ), QSL("ResilienceHub")    },
     303            0 :         { QSL("savingsplans"    ), QSL("SavingsPlans")     },
     304            0 :         { QSL("codeartifact"    ), QSL("CodeArtifact")     },
     305            0 :         { QSL("imagebuilder"    ), QSL("ImageBuilder")     },
     306            0 :         { QSL("billingconductor"), QSL("BillingConductor") },
     307            0 :     };
     308            0 :     for (const auto iter = specialCases.constFind(sdkId); iter != specialCases.constEnd(); ) {
     309              :         return iter.value();
     310              :     }
     311              : 
     312              :     // Start with the sdkId.
     313              :     QString id = sdkId;
     314              : 
     315              :     // Convert runs of upercase letters with first-upper, and the rest lower.
     316            0 :     auto iter = QRegularExpression(QSL("[A-Z]{2,}([^a-zA-Z]|$)")).globalMatch(id);
     317            0 :     while (iter.hasNext()) {
     318            0 :         const auto match = iter.next();
     319            0 :         id.replace(match.capturedStart()+1, match.capturedLength()-1,
     320            0 :                    match.captured().mid(1).toLower());
     321            0 :     }
     322              : 
     323              :     // If the sdkId was all lowercase, then uppercase the first letter of each word.
     324              :     #if (QT_VERSION < QT_VERSION_CHECK(5, 12, 0))
     325              :     if (id == id.toLower()) {
     326              :     #else
     327            0 :     if (id.isLower()) { // QString::isLower() added in Qt 5.12.
     328              :     #endif
     329            0 :         const QStringList words = id.split(QRegularExpression(QSL("[^a-zA-Z0-9]+")));
     330            0 :         id.clear();
     331            0 :         for (const QString &word: words) {
     332            0 :             id.append(word.at(0).toUpper() + word.mid(1));
     333              :         }
     334              :     }
     335              : 
     336              :     // Strip all non-alphanumeric characters, and any instances of "Amazon" and "AWS".
     337            0 :     id.replace(QRegularExpression(QSL("[^a-zA-Z0-9]|Amazon|AWS"),
     338            0 :                QRegularExpression::PatternOption::CaseInsensitiveOption),QString());
     339              : 
     340              :     // Remove any trailing instances of "API", "Client" and "Service".
     341            0 :     if (id == QSL("ConfigService"))    return id; // Skip dropping the Service for this one.
     342            0 :     if (id == QSL("DirectoryService")) return id; // Skip dropping the Service for this one.
     343            0 :     id.replace(QRegularExpression(QSL("(API|Client|Service)$"),
     344            0 :                QRegularExpression::PatternOption::CaseInsensitiveOption),QString());
     345              :     return id;
     346            0 : }
     347              : 
     348            0 : bool Generator::promptToOverwrite(const QString &pathName, ClobberMode &clobberMode)
     349              : {
     350              :     Q_ASSERT(clobberMode == ClobberMode::Prompt);
     351              :     while (true) {
     352            0 :         qCWarning(lc).noquote() << tr("Overwrite %1 [y,n,a,s,q,?]? ").arg(pathName);
     353            0 :         QTextStream stream(stdin);
     354            0 :         const QString response = stream.readLine().toLower();
     355            0 :         if (response == QSL("y")) {
     356              :             return true;
     357            0 :         } else if (response == QSL("n")) {
     358              :             return false;
     359            0 :         } else if (response == QSL("a")) {
     360            0 :             clobberMode = ClobberMode::Overwrite;
     361            0 :             return true;
     362            0 :         } else if (response == QSL("s")) {
     363            0 :             clobberMode = ClobberMode::Skip;
     364            0 :             return false;
     365            0 :         } else if (response == QSL("q")) {
     366            0 :             QCoreApplication::exit(EXIT_FAILURE);
     367              :         } else {
     368            0 :             qCInfo(lc).noquote() << tr("y - overwrite this file");
     369            0 :             qCInfo(lc).noquote() << tr("n - do not overwrite this file");
     370            0 :             qCInfo(lc).noquote() << tr("a - overwrite this, and all remaining files");
     371            0 :             qCInfo(lc).noquote() << tr("s - do not overwrite this, or any remaining files");
     372            0 :             qCInfo(lc).noquote() << tr("q - quit now, without writing any further files");
     373            0 :             qCInfo(lc).noquote() << tr("? - print help");
     374              :         }
     375            0 :     }
     376              :     Q_UNREACHABLE();
     377              : }
     378              : 
     379            0 : Generator::Capitalisation Generator::getCase(const QString &first, const QString &second)
     380              : {
     381              :     #if (QT_VERSION < QT_VERSION_CHECK(5, 12, 0))
     382              :     if ((first == first.toLower()) && (second == second.toLower())) {
     383              :     #else
     384            0 :     if (first.isLower() && second.isLower()) {
     385              :     #endif
     386              :         return Capitalisation::lowercase;
     387              :     }
     388              :     #if (QT_VERSION < QT_VERSION_CHECK(5, 12, 0))
     389              :     if ((first == first.toUpper()) && (second == second.toUpper())) {
     390              :     #else
     391            0 :     if (first.isUpper() && second.isUpper()) {
     392              :     #endif
     393              :         return Capitalisation::UPPERCASE;
     394              :     }
     395              :     #if (QT_VERSION < QT_VERSION_CHECK(5, 12, 0))
     396              :     if ((first.at(0) == first.at(0).toLower()) && (second.at(0) == second.at(0).toUpper())) {
     397              :     #else
     398            0 :     if (first.front().isLower() && second.front().isUpper()) {
     399              :     #endif
     400            0 :         return Capitalisation::camelCase;
     401              :     }
     402              :     #if (QT_VERSION < QT_VERSION_CHECK(5, 12, 0))
     403              :     if ((first.at(0) == first.at(0).isUpper()) && (second.at(0) == second.at(0).toLower())) {
     404              :     #else
     405            0 :     if (first.front().isUpper() && second.front().isUpper()) {
     406              :     #endif
     407            0 :         return Capitalisation::CamelCase;
     408              :     }
     409              :     return Capitalisation::NoChange;
     410              : }
     411              : 
     412            0 : QString Generator::makeCase(const QString &string, const Capitalisation &capitalisation)
     413              : {
     414            0 :     switch (capitalisation) {
     415              :     case Capitalisation::NoChange:
     416              :         return string;
     417            0 :     case Capitalisation::lowercase:
     418              :         return string.toLower();
     419              :     case Capitalisation::camelCase:
     420            0 :         Q_UNIMPLEMENTED();
     421            0 :         break;
     422              :     case Capitalisation::CamelCase:
     423            0 :         Q_UNIMPLEMENTED();
     424            0 :         break;
     425            0 :     case Capitalisation::UPPERCASE:
     426              :         return string.toUpper();
     427              :     }
     428            0 :     qCCritical(lc).noquote() << tr("Unknown capitalisation %1 for %2")
     429            0 :         .arg((int)capitalisation).arg(string);
     430              :     return QString{};
     431              : }
     432              : 
     433            0 : QString Generator::makeOutputPathName(const QString &templateName, const QRegularExpression &pattern,
     434              :                                       const QHash<QString,QString> &ids, const QString &outputDir)
     435              : {
     436              :     QString outputPathName = templateName;
     437            0 :     for (auto iter = pattern.globalMatch(templateName); iter.hasNext(); ) {
     438            0 :         const QRegularExpressionMatch match = iter.next();
     439            0 :         const QString type = match.captured(QSL("type"));
     440            0 :         const QString sep = match.captured(QSL("separator"));
     441            0 :         const QString id = match.captured(QSL("id"));
     442            0 :         const QString newId = ids.value(id.toLower());
     443            0 :         if (newId.isNull()) {
     444            0 :             qCCritical(lc).noquote() << tr("No pathname ID value for %1").arg(id);
     445              :             return QString{};
     446              :         }
     447            0 :         const QString label = makeCase(newId, getCase(type, id)).replace(QLatin1Char(' '), sep);
     448            0 :         outputPathName.replace(type+sep+id, label);
     449            0 :     }
     450            0 :     if (!outputDir.isEmpty()) {
     451            0 :         outputPathName.prepend(outputDir + QLatin1Char('/'));
     452              :     }
     453              :     return outputPathName;
     454            0 : }
     455              : 
     456              : /// \todo This is often dropping the last word of a sentence... won't fix yet so we can maintain
     457              : /// the old aws-sdk-qt codegen behaviour until the swithover to smithy-qt templating is complete
     458              : /// (ie to minimise the initial change-over diff).
     459            0 : QStringList Generator::formatHtmlDocumentation(const QString &html)
     460              : {
     461              :     QString content(html);
     462              : 
     463              :     /// @todo There's much more we can do here, indeed the documentation
     464              :     /// conversation still needs a lot of love. But its a start, and no point
     465              :     /// prioritising it yet, since its more important we get the code structure
     466              :     /// right first.
     467              : 
     468            0 :     content.replace(QSL("<function>"), QSL("<code>"));
     469            0 :     content.replace(QSL("</function>"), QSL("</code>"));
     470              : 
     471            0 :     content.replace(QSL("<important>"), QSL("<b>"));
     472            0 :     content.replace(QSL("</important>"), QSL("</b>"));
     473              : 
     474              :     // Replace /* and */ with &ast; versions to avoid breaking comment blocks.
     475            0 :     content.replace(QSL("/*"), QSL("/&ast;"));
     476            0 :     content.replace(QSL("*/"), QSL("&ast;/"));
     477              : 
     478            0 :     QStringList lines;
     479            0 :     QString line;
     480              :     #if (QT_VERSION > QT_VERSION_CHECK(5, 14, 0))
     481              :         #define SKIP_EMPTY_PARTS Qt::SkipEmptyParts // Introduced in Qt 5.14.
     482              :     #else
     483              :         #define SKIP_EMPTY_PARTS QString::SkipEmptyParts // Deprecated in Qt 5.15.
     484              :     #endif
     485            0 :     for (QString word: content.split(QRegularExpression(QSL("\\s+")), SKIP_EMPTY_PARTS)) {
     486            0 :         if (word.startsWith(QSL("<p>")) || word.endsWith(QSL("</p>"))) {
     487            0 :             lines.append(line);
     488            0 :             line.clear();
     489            0 :             if (!lines.last().isEmpty()) {
     490            0 :                 lines.append(QString()); // A blank line.
     491              :             }
     492            0 :             if (word.startsWith(QSL("<p>"))) {
     493            0 :                 word.remove(0,3);
     494              :             }
     495            0 :             if (word.endsWith(QSL("</p>"))) {
     496            0 :                 word.remove(word.size()-5,4);
     497              :             }
     498              :         }
     499              : 
     500            0 :         if (line.isEmpty()) {
     501              :             line += word;
     502            0 :         } else if (line.size() + word.size() < 120) {
     503            0 :             line += QLatin1Char(' ') + word;
     504              :         } else {
     505            0 :             lines.append(line);
     506            0 :             line = word;
     507              :         }
     508            0 :     }
     509              : 
     510              :     // Remove leading and trailing blank lines.
     511            0 :     while ((!lines.isEmpty()) && (lines.first().isEmpty())) {
     512            0 :         lines.removeFirst();
     513              :     }
     514            0 :     while ((!lines.isEmpty()) && (lines.last().isEmpty())) {
     515            0 :         lines.removeLast();
     516              :     }
     517            0 :     return lines;
     518            0 : }
        

Generated by: LCOV version 2.2-1