/****************************************************************************
**
** Copyright (C) 2021 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the tools applications of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#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

static CodeMarker *marker_ = nullptr;

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

void WebXMLGenerator::terminateGenerator()
{
    Generator::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, const QString &file,
                                              CodeMarker * /* marker */)
{
    QByteArray data;
    QXmlStreamWriter writer(&data);
    writer.setAutoFormatting(true);
    beginFilePage(en, linkForExampleFile(file, "webxml"));
    writer.writeStartDocument();
    writer.writeStartElement("WebXML");
    writer.writeStartElement("document");
    writer.writeStartElement("page");
    writer.writeAttribute("name", file);
    writer.writeAttribute("href", linkForExampleFile(file));
    QString title = exampleFileTitle(static_cast<const ExampleNode *>(en), file);
    writer.writeAttribute("title", title);
    writer.writeAttribute("fulltitle", title);
    writer.writeAttribute("subtitle", file);
    writer.writeStartElement("description");

    if (Config::instance().getBool(CONFIG_LOCATIONINFO)) {
        QString userFriendlyFilePath; // unused
        writer.writeAttribute("path",
                              Doc::resolveFile(en->doc().location(), file, &userFriendlyFilePath));
        writer.writeAttribute("line", "0");
        writer.writeAttribute("column", "0");
    }

    Quoter quoter;
    Doc::quoteFromFile(en->doc().location(), quoter, 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;
    endFilePage();
}

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:
        if (!m_inLink && !m_inSectionHeading) {
            const Node *node = nullptr;
            QString link = getAutoLink(atom, relative, &node, Node::API);
            if (!link.isEmpty() && node && node->isDeprecated()
                && relative->parent() != node && !relative->isDeprecated()) {
                link.clear();
            }
            if (node) {
                startLink(writer, atom, node, link);
                if (m_inLink) {
                    writer.writeCharacters(atom->string());
                    writer.writeEndElement(); // link
                    m_inLink = false;
                }
            } else {
                writer.writeCharacters(atom->string());
            }
        } else {
            writer.writeCharacters(atom->string());
        }
        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::CodeNew:
        writer.writeTextElement("para", "you can rewrite it as");
        writer.writeTextElement("newcode",
                                trimmedTrailing(plainCode(atom->string()), QString(), QString()));
        break;

    case Atom::CodeOld:
        writer.writeTextElement("para", "For example, if you have code like");
        writer.writeTextElement("oldcode",
                                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;
    case Atom::Image:
        writer.writeStartElement("image");
        writer.writeAttribute("href", imageFileName(relative, atom->string()));
        writer.writeEndElement();
        setImageFileName(relative, atom->string());
        break;

    case Atom::InlineImage:
        writer.writeStartElement("inlineimage");
        writer.writeAttribute("href", imageFileName(relative, atom->string()));
        writer.writeEndElement();
        setImageFileName(relative, atom->string());
        break;

    case Atom::ImageText:
        break;

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

    case Atom::ImportantRight:
        writer.writeEndElement(); // para
        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;

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

    case Atom::Nop:
        break;

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

    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);
            const QString resolved = Doc::resolveFile(Location(), location);
            if (!resolved.isEmpty())
                writer.writeAttribute("path", resolved);
        }
        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::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);
                QString fileTitle = exampleFileTitle(en, atom->string());
                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()) {
        QPair<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
