// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include "webxmlgenerator.h"

#include "aggregate.h"
#include "collectionnode.h"
#include "config.h"
#include "helpprojectwriter.h"
#include "node.h"
#include "propertynode.h"
#include "qdocdatabase.h"
#include "quoter.h"

#include <QtCore/qxmlstream.h>

QT_BEGIN_NAMESPACE

using namespace Qt::StringLiterals;

static CodeMarker *marker_ = nullptr;

WebXMLGenerator::WebXMLGenerator(FileResolver& file_resolver) : HtmlGenerator(file_resolver) {}

void WebXMLGenerator::initializeGenerator()
{
    HtmlGenerator::initializeGenerator();
}

void WebXMLGenerator::terminateGenerator()
{
    HtmlGenerator::terminateGenerator();
}

QString WebXMLGenerator::format()
{
    return "WebXML";
}

QString WebXMLGenerator::fileExtension() const
{
    // As this is meant to be an intermediate format,
    // use .html for internal references. The name of
    // the output file is set separately in
    // beginSubPage() calls.
    return "html";
}

/*!
    Most of the output is generated by QDocIndexFiles and the append() callback.
    Some pages produce supplementary output while being generated, and that's
    handled here.
*/
qsizetype WebXMLGenerator::generateAtom(const Atom *atom, const Node *relative, CodeMarker *marker)
{
    if (m_supplement && currentWriter)
        addAtomElements(*currentWriter.data(), atom, relative, marker);
    return 0;
}

void WebXMLGenerator::generateCppReferencePage(Aggregate *aggregate, CodeMarker * /* marker */)
{
    QByteArray data;
    QXmlStreamWriter writer(&data);
    writer.setAutoFormatting(true);
    beginSubPage(aggregate, Generator::fileName(aggregate, "webxml"));
    writer.writeStartDocument();
    writer.writeStartElement("WebXML");
    writer.writeStartElement("document");

    generateIndexSections(writer, aggregate);

    writer.writeEndElement(); // document
    writer.writeEndElement(); // WebXML
    writer.writeEndDocument();

    out() << data;
    endSubPage();
}

void WebXMLGenerator::generatePageNode(PageNode *pn, CodeMarker * /* marker */)
{
    QByteArray data;
    currentWriter.reset(new QXmlStreamWriter(&data));
    currentWriter->setAutoFormatting(true);
    beginSubPage(pn, Generator::fileName(pn, "webxml"));
    currentWriter->writeStartDocument();
    currentWriter->writeStartElement("WebXML");
    currentWriter->writeStartElement("document");

    generateIndexSections(*currentWriter.data(), pn);

    currentWriter->writeEndElement(); // document
    currentWriter->writeEndElement(); // WebXML
    currentWriter->writeEndDocument();

    out() << data;
    endSubPage();
}

void WebXMLGenerator::generateExampleFilePage(const Node *en, ResolvedFile resolved_file, CodeMarker* /* marker */)
{
    // TODO: [generator-insufficient-structural-abstraction]

    QByteArray data;
    QXmlStreamWriter writer(&data);
    writer.setAutoFormatting(true);
    beginSubPage(en, linkForExampleFile(resolved_file.get_query(), "webxml"));
    writer.writeStartDocument();
    writer.writeStartElement("WebXML");
    writer.writeStartElement("document");
    writer.writeStartElement("page");
    writer.writeAttribute("name", resolved_file.get_path());
    writer.writeAttribute("href", linkForExampleFile(resolved_file.get_path()));
    QString title = exampleFileTitle(static_cast<const ExampleNode *>(en), resolved_file.get_path());
    writer.writeAttribute("title", title);
    writer.writeAttribute("fulltitle", title);
    writer.writeAttribute("subtitle", resolved_file.get_path());
    writer.writeStartElement("description");

    if (Config::instance().getBool(CONFIG_LOCATIONINFO)) {
        writer.writeAttribute("path", resolved_file.get_path());
        writer.writeAttribute("line", "0");
        writer.writeAttribute("column", "0");
    }

    Quoter quoter;
    Doc::quoteFromFile(en->doc().location(), quoter, resolved_file);
    QString code = quoter.quoteTo(en->location(), QString(), QString());
    writer.writeTextElement("code", trimmedTrailing(code, QString(), QString()));

    writer.writeEndElement(); // description
    writer.writeEndElement(); // page
    writer.writeEndElement(); // document
    writer.writeEndElement(); // WebXML
    writer.writeEndDocument();

    out() << data;
    endSubPage();
}

void WebXMLGenerator::generateIndexSections(QXmlStreamWriter &writer, Node *node)
{
    marker_ = CodeMarker::markerForFileName(node->location().filePath());
    auto qdocIndexFiles = QDocIndexFiles::qdocIndexFiles();
    if (qdocIndexFiles) {
        qdocIndexFiles->generateIndexSections(writer, node, this);
        // generateIndexSections does nothing for groups, so handle them explicitly
        if (node->isGroup())
            qdocIndexFiles->generateIndexSection(writer, node, this);
    }
}

// Handles callbacks from QDocIndexFiles to add documentation to node
void WebXMLGenerator::append(QXmlStreamWriter &writer, Node *node)
{
    Q_ASSERT(marker_);

    writer.writeStartElement("description");
    if (Config::instance().getBool(CONFIG_LOCATIONINFO)) {
        writer.writeAttribute("path", node->doc().location().filePath());
        writer.writeAttribute("line", QString::number(node->doc().location().lineNo()));
        writer.writeAttribute("column", QString::number(node->doc().location().columnNo()));
    }

    if (node->isTextPageNode())
        generateRelations(writer, node);

    if (node->isModule()) {
        writer.writeStartElement("generatedlist");
        writer.writeAttribute("contents", "classesbymodule");
        auto *cnn = static_cast<CollectionNode *>(node);

        if (cnn->hasNamespaces()) {
            writer.writeStartElement("section");
            writer.writeStartElement("heading");
            writer.writeAttribute("level", "1");
            writer.writeCharacters("Namespaces");
            writer.writeEndElement(); // heading
            NodeMap namespaces;
            cnn->getMemberNamespaces(namespaces);
            generateAnnotatedList(writer, node, namespaces);
            writer.writeEndElement(); // section
        }
        if (cnn->hasClasses()) {
            writer.writeStartElement("section");
            writer.writeStartElement("heading");
            writer.writeAttribute("level", "1");
            writer.writeCharacters("Classes");
            writer.writeEndElement(); // heading
            NodeMap classes;
            cnn->getMemberClasses(classes);
            generateAnnotatedList(writer, node, classes);
            writer.writeEndElement(); // section
        }
        writer.writeEndElement(); // generatedlist
    }

    m_inLink = m_inSectionHeading = m_hasQuotingInformation = false;

    const Atom *atom = node->doc().body().firstAtom();
    while (atom)
        atom = addAtomElements(writer, atom, node, marker_);

    QList<Text> alsoList = node->doc().alsoList();
    supplementAlsoList(node, alsoList);

    if (!alsoList.isEmpty()) {
        writer.writeStartElement("see-also");
        for (const auto &item : alsoList) {
            const auto *atom = item.firstAtom();
            while (atom)
                atom = addAtomElements(writer, atom, node, marker_);
        }
        writer.writeEndElement(); // see-also
    }

    if (node->isExample()) {
        m_supplement = true;
        generateRequiredLinks(node, marker_);
        m_supplement = false;
    } else if (node->isGroup()) {
        auto *cn = static_cast<CollectionNode *>(node);
        if (!cn->noAutoList())
            generateAnnotatedList(writer, node, cn->members());
    }

    writer.writeEndElement(); // description
}

void WebXMLGenerator::generateDocumentation(Node *node)
{
    // Don't generate nodes that are already processed, or if they're not supposed to
    // generate output, ie. external, index or images nodes.
    if (!node->url().isNull() || node->isExternalPage() || node->isIndexNode())
        return;

    if (node->isInternal() && !m_showInternal)
        return;

    if (node->parent()) {
        if (node->isNamespace() || node->isClassNode() || node->isHeader())
            generateCppReferencePage(static_cast<Aggregate *>(node), nullptr);
        else if (node->isCollectionNode()) {
            if (node->wasSeen()) {
                // see remarks in base class impl.
                m_qdb->mergeCollections(static_cast<CollectionNode *>(node));
                generatePageNode(static_cast<PageNode *>(node), nullptr);
            }
        } else if (node->isTextPageNode())
            generatePageNode(static_cast<PageNode *>(node), nullptr);
        // else if TODO: anything else?
    }

    if (node->isAggregate()) {
        auto *aggregate = static_cast<Aggregate *>(node);
        for (auto c : aggregate->childNodes()) {
            if ((c->isAggregate() || c->isTextPageNode() || c->isCollectionNode())
                && !c->isPrivate())
                generateDocumentation(c);
        }
    }
}

const Atom *WebXMLGenerator::addAtomElements(QXmlStreamWriter &writer, const Atom *atom,
                                             const Node *relative, CodeMarker *marker)
{
    bool keepQuoting = false;

    if (!atom)
        return nullptr;

    switch (atom->type()) {
    case Atom::AnnotatedList: {
        const CollectionNode *cn = m_qdb->getCollectionNode(atom->string(), Node::Group);
        if (cn)
            generateAnnotatedList(writer, relative, cn->members());
    } break;
    case Atom::AutoLink: {
        const Node *node{nullptr};
        QString link{};

        if (!m_inLink && !m_inSectionHeading) {
            link = getAutoLink(atom, relative, &node, Node::API);

            if (!link.isEmpty() && node && node->isDeprecated()
                && relative->parent() != node && !relative->isDeprecated()) {
                link.clear();
            }
        }

        startLink(writer, atom, node, link);

        writer.writeCharacters(atom->string());

        if (m_inLink) {
            writer.writeEndElement(); // link
            m_inLink = false;
        }

        break;
    }
    case Atom::BaseName:
        break;
    case Atom::BriefLeft:

        writer.writeStartElement("brief");
        switch (relative->nodeType()) {
        case Node::Property:
            writer.writeCharacters("This property");
            break;
        case Node::Variable:
            writer.writeCharacters("This variable");
            break;
        default:
            break;
        }
        if (relative->isProperty() || relative->isVariable()) {
            QString str;
            const Atom *a = atom->next();
            while (a != nullptr && a->type() != Atom::BriefRight) {
                if (a->type() == Atom::String || a->type() == Atom::AutoLink)
                    str += a->string();
                a = a->next();
            }
            str[0] = str[0].toLower();
            if (str.endsWith('.'))
                str.chop(1);

            const QList<QStringView> words = QStringView{str}.split(' ');
            if (!words.isEmpty()) {
                QStringView first(words.at(0));
                if (!(first == u"contains" || first == u"specifies" || first == u"describes"
                      || first == u"defines" || first == u"holds" || first == u"determines"))
                    writer.writeCharacters(" holds ");
                else
                    writer.writeCharacters(" ");
            }
        }
        break;

    case Atom::BriefRight:
        if (relative->isProperty() || relative->isVariable())
            writer.writeCharacters(".");

        writer.writeEndElement(); // brief
        break;

    case Atom::C:
        writer.writeStartElement("teletype");
        if (m_inLink)
            writer.writeAttribute("type", "normal");
        else
            writer.writeAttribute("type", "highlighted");

        writer.writeCharacters(plainCode(atom->string()));
        writer.writeEndElement(); // teletype
        break;

    case Atom::Code:
        if (!m_hasQuotingInformation)
            writer.writeTextElement(
                    "code", trimmedTrailing(plainCode(atom->string()), QString(), QString()));
        else
            keepQuoting = true;
        break;

#ifdef QDOC_QML
    case Atom::Qml:
        if (!m_hasQuotingInformation)
            writer.writeTextElement(
                    "qml", trimmedTrailing(plainCode(atom->string()), QString(), QString()));
        else
            keepQuoting = true;
#endif
    case Atom::CodeBad:
        writer.writeTextElement("badcode",
                                trimmedTrailing(plainCode(atom->string()), QString(), QString()));
        break;

    case Atom::CodeQuoteArgument:
        if (m_quoting) {
            if (quoteCommand == "dots") {
                writer.writeAttribute("indent", atom->string());
                writer.writeCharacters("...");
            } else {
                writer.writeCharacters(atom->string());
            }
            writer.writeEndElement(); // code
            keepQuoting = true;
        }
        break;

    case Atom::CodeQuoteCommand:
        if (m_quoting) {
            quoteCommand = atom->string();
            writer.writeStartElement(quoteCommand);
        }
        break;

    case Atom::ExampleFileLink: {
        if (!m_inLink) {
            QString link = linkForExampleFile(atom->string());
            if (!link.isEmpty())
                startLink(writer, atom, relative, link);
        }
    } break;

    case Atom::ExampleImageLink: {
        if (!m_inLink) {
            QString link = atom->string();
            if (!link.isEmpty())
                startLink(writer, atom, nullptr, "images/used-in-examples/" + link);
        }
    } break;

    case Atom::FootnoteLeft:
        writer.writeStartElement("footnote");
        break;

    case Atom::FootnoteRight:
        writer.writeEndElement(); // footnote
        break;

    case Atom::FormatEndif:
        writer.writeEndElement(); // raw
        break;
    case Atom::FormatIf:
        writer.writeStartElement("raw");
        writer.writeAttribute("format", atom->string());
        break;
    case Atom::FormattingLeft: {
        if (atom->string() == ATOM_FORMATTING_BOLD)
            writer.writeStartElement("bold");
        else if (atom->string() == ATOM_FORMATTING_ITALIC)
            writer.writeStartElement("italic");
        else if (atom->string() == ATOM_FORMATTING_UNDERLINE)
            writer.writeStartElement("underline");
        else if (atom->string() == ATOM_FORMATTING_SUBSCRIPT)
            writer.writeStartElement("subscript");
        else if (atom->string() == ATOM_FORMATTING_SUPERSCRIPT)
            writer.writeStartElement("superscript");
        else if (atom->string() == ATOM_FORMATTING_TELETYPE)
            writer.writeStartElement("teletype");
        else if (atom->string() == ATOM_FORMATTING_PARAMETER)
            writer.writeStartElement("argument");
        else if (atom->string() == ATOM_FORMATTING_INDEX)
            writer.writeStartElement("index");
    } break;

    case Atom::FormattingRight: {
        if (atom->string() == ATOM_FORMATTING_BOLD)
            writer.writeEndElement();
        else if (atom->string() == ATOM_FORMATTING_ITALIC)
            writer.writeEndElement();
        else if (atom->string() == ATOM_FORMATTING_UNDERLINE)
            writer.writeEndElement();
        else if (atom->string() == ATOM_FORMATTING_SUBSCRIPT)
            writer.writeEndElement();
        else if (atom->string() == ATOM_FORMATTING_SUPERSCRIPT)
            writer.writeEndElement();
        else if (atom->string() == ATOM_FORMATTING_TELETYPE)
            writer.writeEndElement();
        else if (atom->string() == ATOM_FORMATTING_PARAMETER)
            writer.writeEndElement();
        else if (atom->string() == ATOM_FORMATTING_INDEX)
            writer.writeEndElement();
    }
        if (m_inLink) {
            writer.writeEndElement(); // link
            m_inLink = false;
        }
        break;

    case Atom::GeneratedList:
        writer.writeStartElement("generatedlist");
        writer.writeAttribute("contents", atom->string());
        writer.writeEndElement();
        break;

    // TODO: The other generators treat inlineimage and image
    // simultaneously as the diffirences aren't big. It should be
    // possible to do the same for webxmlgenerator instead of
    // repeating the code.

    // TODO: [generator-insufficient-structural-abstraction]
    case Atom::Image: {
        auto maybe_resolved_file{file_resolver.resolve(atom->string())};
        if (!maybe_resolved_file) {
            // TODO: [uncentralized-admonition][failed-resolve-file]
            relative->location().warning(QStringLiteral("Missing image: %1").arg(atom->string()));
        } else {
            ResolvedFile file{*maybe_resolved_file};
            QString file_name{QFileInfo{file.get_path()}.fileName()};

            // TODO: [uncentralized-output-directory-structure]
            Config::copyFile(relative->doc().location(), file.get_path(), file_name, outputDir() + QLatin1String("/images"));

            writer.writeStartElement("image");
            // TODO: [uncentralized-output-directory-structure]
            writer.writeAttribute("href", "images/" + file_name);
            writer.writeEndElement();
            // TODO: [uncentralized-output-directory-structure]
            setImageFileName(relative, "images/" + file_name);
        }
        break;
    }
    // TODO: [generator-insufficient-structural-abstraction]
    case Atom::InlineImage: {
        auto maybe_resolved_file{file_resolver.resolve(atom->string())};
        if (!maybe_resolved_file) {
            // TODO: [uncentralized-admonition][failed-resolve-file]
            relative->location().warning(QStringLiteral("Missing image: %1").arg(atom->string()));
        } else {
            ResolvedFile file{*maybe_resolved_file};
            QString file_name{QFileInfo{file.get_path()}.fileName()};

            // TODO: [uncentralized-output-directory-structure]
            Config::copyFile(relative->doc().location(), file.get_path(), file_name, outputDir() + QLatin1String("/images"));

            writer.writeStartElement("inlineimage");
            // TODO: [uncentralized-output-directory-structure]
            writer.writeAttribute("href", "images/" + file_name);
            writer.writeEndElement();
            // TODO: [uncentralized-output-directory-structure]
            setImageFileName(relative, "images/" + file_name);
        }
        break;
    }
    case Atom::ImageText:
        break;

    case Atom::ImportantLeft:
        writer.writeStartElement("para");
        writer.writeTextElement("bold", "Important:");
        writer.writeCharacters(" ");
        break;

    case Atom::LegaleseLeft:
        writer.writeStartElement("legalese");
        break;

    case Atom::LegaleseRight:
        writer.writeEndElement(); // legalese
        break;

    case Atom::Link:
    case Atom::LinkNode:
        if (!m_inLink) {
            const Node *node = nullptr;
            QString link = getLink(atom, relative, &node);
            if (!link.isEmpty())
                startLink(writer, atom, node, link);
        }
        break;

    case Atom::ListLeft:
        writer.writeStartElement("list");

        if (atom->string() == ATOM_LIST_BULLET)
            writer.writeAttribute("type", "bullet");
        else if (atom->string() == ATOM_LIST_TAG)
            writer.writeAttribute("type", "definition");
        else if (atom->string() == ATOM_LIST_VALUE) {
            if (relative->isEnumType())
                writer.writeAttribute("type", "enum");
            else
                writer.writeAttribute("type", "definition");
        } else {
            writer.writeAttribute("type", "ordered");
            if (atom->string() == ATOM_LIST_UPPERALPHA)
                writer.writeAttribute("start", "A");
            else if (atom->string() == ATOM_LIST_LOWERALPHA)
                writer.writeAttribute("start", "a");
            else if (atom->string() == ATOM_LIST_UPPERROMAN)
                writer.writeAttribute("start", "I");
            else if (atom->string() == ATOM_LIST_LOWERROMAN)
                writer.writeAttribute("start", "i");
            else // (atom->string() == ATOM_LIST_NUMERIC)
                writer.writeAttribute("start", "1");
        }
        break;

    case Atom::ListItemNumber:
        break;
    case Atom::ListTagLeft: {
        writer.writeStartElement("definition");

        writer.writeTextElement(
                "term", plainCode(marker->markedUpEnumValue(atom->next()->string(), relative)));
    } break;

    case Atom::ListTagRight:
        writer.writeEndElement(); // definition
        break;

    case Atom::ListItemLeft:
        writer.writeStartElement("item");
        break;

    case Atom::ListItemRight:
        writer.writeEndElement(); // item
        break;

    case Atom::ListRight:
        writer.writeEndElement(); // list
        break;

    case Atom::NoteLeft:
        writer.writeStartElement("para");
        writer.writeTextElement("bold", "Note:");
        writer.writeCharacters(" ");
        break;

    // End admonition elements
    case Atom::ImportantRight:
    case Atom::NoteRight:
    case Atom::WarningRight:
        writer.writeEndElement(); // para
        break;

    case Atom::Nop:
        break;

    case Atom::CaptionLeft:
    case Atom::ParaLeft:
        writer.writeStartElement("para");
        break;

    case Atom::CaptionRight:
    case Atom::ParaRight:
        writer.writeEndElement(); // para
        break;

    case Atom::QuotationLeft:
        writer.writeStartElement("quote");
        break;

    case Atom::QuotationRight:
        writer.writeEndElement(); // quote
        break;

    case Atom::RawString:
        writer.writeCharacters(atom->string());
        break;

    case Atom::SectionLeft:
        writer.writeStartElement("section");
        writer.writeAttribute("id", Doc::canonicalTitle(Text::sectionHeading(atom).toString()));
        break;

    case Atom::SectionRight:
        writer.writeEndElement(); // section
        break;

    case Atom::SectionHeadingLeft: {
        writer.writeStartElement("heading");
        int unit = atom->string().toInt(); // + hOffset(relative)
        writer.writeAttribute("level", QString::number(unit));
        m_inSectionHeading = true;
    } break;

    case Atom::SectionHeadingRight:
        writer.writeEndElement(); // heading
        m_inSectionHeading = false;
        break;

    case Atom::SidebarLeft:
    case Atom::SidebarRight:
        break;

    case Atom::SnippetCommand:
        if (m_quoting) {
            writer.writeStartElement(atom->string());
        }
        break;

    case Atom::SnippetIdentifier:
        if (m_quoting) {
            writer.writeAttribute("identifier", atom->string());
            writer.writeEndElement();
            keepQuoting = true;
        }
        break;

    case Atom::SnippetLocation:
        if (m_quoting) {
            const QString &location = atom->string();
            writer.writeAttribute("location", location);
            auto maybe_resolved_file{file_resolver.resolve(location)};
            // const QString resolved = Doc::resolveFile(Location(), location);
            if (maybe_resolved_file)
                writer.writeAttribute("path", (*maybe_resolved_file).get_path());
            else {
                // TODO: [uncetnralized-admonition][failed-resolve-file]
                QString details = std::transform_reduce(
                    file_resolver.get_search_directories().cbegin(),
                    file_resolver.get_search_directories().cend(),
                    u"Searched directories:"_s,
                    std::plus(),
                    [](const DirectoryPath &directory_path) -> QString { return u' ' + directory_path.value(); }
                );

                relative->location().warning(u"Cannot find file to quote from: %1"_s.arg(location), details);
            }
        }
        break;

    case Atom::String:
        writer.writeCharacters(atom->string());
        break;
    case Atom::TableLeft:
        writer.writeStartElement("table");
        if (atom->string().contains("%"))
            writer.writeAttribute("width", atom->string());
        break;

    case Atom::TableRight:
        writer.writeEndElement(); // table
        break;

    case Atom::TableHeaderLeft:
        writer.writeStartElement("header");
        break;

    case Atom::TableHeaderRight:
        writer.writeEndElement(); // header
        break;

    case Atom::TableRowLeft:
        writer.writeStartElement("row");
        break;

    case Atom::TableRowRight:
        writer.writeEndElement(); // row
        break;

    case Atom::TableItemLeft: {
        writer.writeStartElement("item");
        QStringList spans = atom->string().split(",");
        if (spans.size() == 2) {
            if (spans.at(0) != "1")
                writer.writeAttribute("colspan", spans.at(0).trimmed());
            if (spans.at(1) != "1")
                writer.writeAttribute("rowspan", spans.at(1).trimmed());
        }
    } break;
    case Atom::TableItemRight:
        writer.writeEndElement(); // item
        break;

    case Atom::Target:
        writer.writeStartElement("target");
        writer.writeAttribute("name", Doc::canonicalTitle(atom->string()));
        writer.writeEndElement();
        break;

    case Atom::WarningLeft:
        writer.writeStartElement("para");
        writer.writeTextElement("bold", "Warning:");
        writer.writeCharacters(" ");
        break;

    case Atom::UnhandledFormat:
    case Atom::UnknownCommand:
        writer.writeCharacters(atom->typeString());
        break;
    default:
        break;
    }

    m_hasQuotingInformation = keepQuoting;
    return atom->next();
}

void WebXMLGenerator::startLink(QXmlStreamWriter &writer, const Atom *atom, const Node *node,
                                const QString &link)
{
    QString fullName = link;
    if (node)
        fullName = node->fullName();
    if (!fullName.isEmpty() && !link.isEmpty()) {
        writer.writeStartElement("link");
        if (atom && !atom->string().isEmpty())
            writer.writeAttribute("raw", atom->string());
        else
            writer.writeAttribute("raw", fullName);
        writer.writeAttribute("href", link);
        writer.writeAttribute("type", targetType(node));
        if (node) {
            switch (node->nodeType()) {
            case Node::Enum:
                writer.writeAttribute("enum", fullName);
                break;
            case Node::Example: {
                const auto *en = static_cast<const ExampleNode *>(node);
                const QString fileTitle = atom ? exampleFileTitle(en, atom->string()) : QString();
                if (!fileTitle.isEmpty()) {
                    writer.writeAttribute("page", fileTitle);
                    break;
                }
            }
                Q_FALLTHROUGH();
            case Node::Page:
                writer.writeAttribute("page", fullName);
                break;
            case Node::Property: {
                const auto *propertyNode = static_cast<const PropertyNode *>(node);
                if (!propertyNode->getters().empty())
                    writer.writeAttribute("getter", propertyNode->getters().at(0)->fullName());
            } break;
            default:
                break;
            }
        }
        m_inLink = true;
    }
}

void WebXMLGenerator::endLink(QXmlStreamWriter &writer)
{
    if (m_inLink) {
        writer.writeEndElement(); // link
        m_inLink = false;
    }
}

void WebXMLGenerator::generateRelations(QXmlStreamWriter &writer, const Node *node)
{
    if (node && !node->links().empty()) {
        std::pair<QString, QString> anchorPair;
        const Node *linkNode;

        for (auto it = node->links().cbegin(); it != node->links().cend(); ++it) {

            linkNode = m_qdb->findNodeForTarget(it.value().first, node);

            if (!linkNode)
                linkNode = node;

            if (linkNode == node)
                anchorPair = it.value();
            else
                anchorPair = anchorForNode(linkNode);

            writer.writeStartElement("relation");
            writer.writeAttribute("href", anchorPair.first);
            writer.writeAttribute("type", targetType(linkNode));

            switch (it.key()) {
            case Node::StartLink:
                writer.writeAttribute("meta", "start");
                break;
            case Node::NextLink:
                writer.writeAttribute("meta", "next");
                break;
            case Node::PreviousLink:
                writer.writeAttribute("meta", "previous");
                break;
            case Node::ContentsLink:
                writer.writeAttribute("meta", "contents");
                break;
            default:
                writer.writeAttribute("meta", "");
            }
            writer.writeAttribute("description", anchorPair.second);
            writer.writeEndElement(); // link
        }
    }
}

void WebXMLGenerator::generateAnnotatedList(QXmlStreamWriter &writer, const Node *relative,
                                            const NodeMap &nodeMap)
{
    generateAnnotatedList(writer, relative, nodeMap.values());
}

void WebXMLGenerator::generateAnnotatedList(QXmlStreamWriter &writer, const Node *relative,
                                            const NodeList &nodeList)
{
    writer.writeStartElement("table");
    writer.writeAttribute("width", "100%");

    for (const auto *node : nodeList) {
        writer.writeStartElement("row");
        writer.writeStartElement("item");
        writer.writeStartElement("para");
        const QString link = linkForNode(node, relative);
        startLink(writer, node->doc().body().firstAtom(), node, link);
        endLink(writer);
        writer.writeEndElement(); // para
        writer.writeEndElement(); // item

        writer.writeStartElement("item");
        writer.writeStartElement("para");
        writer.writeCharacters(node->doc().briefText().toString());
        writer.writeEndElement(); // para
        writer.writeEndElement(); // item
        writer.writeEndElement(); // row
    }
    writer.writeEndElement(); // table
}

QString WebXMLGenerator::fileBase(const Node *node) const
{
    return Generator::fileBase(node);
}

QT_END_NAMESPACE
