File indexing completed on 2024-05-05 05:52:22

0001 /***************************************************************************
0002   pluginKatexmltools.cpp
0003 
0004   List elements, attributes, attribute values and entities allowed by DTD.
0005   Needs a DTD in XML format ( as produced by dtdparse ) for most features.
0006 
0007   copyright         : ( C ) 2001-2002 by Daniel Naber
0008   email             : daniel.naber@t-online.de
0009 
0010   SPDX-FileCopyrightText: 2005 Anders Lund <anders@alweb.dk>
0011 
0012   KDE SC 4 version (C) 2010 Tomas Trnka <tomastrnka@gmx.com>
0013  ***************************************************************************/
0014 
0015 /***************************************************************************
0016  This program is free software; you can redistribute it and/or
0017  modify it under the terms of the GNU General Public License
0018  as published by the Free Software Foundation; either version 2
0019  of the License, or ( at your option ) any later version.
0020 
0021  This program is distributed in the hope that it will be useful,
0022  but WITHOUT ANY WARRANTY; without even the implied warranty of
0023  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0024  GNU General Public License for more details.
0025 
0026  You should have received a copy of the GNU General Public License
0027  along with this program; if not, write to the Free Software
0028  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
0029  ***************************************************************************/
0030 
0031 /*
0032 README:
0033 The basic idea is this: certain keyEvents(), namely [<&" ], trigger a completion box.
0034 This is intended as a help for editing. There are some cases where the XML
0035 spec is not followed, e.g. one can add the same attribute twice to an element.
0036 Also see the user documentation. If backspace is pressed after a completion popup
0037 was closed, the popup will re-open. This way typos can be corrected and the popup
0038 will reappear, which is quite comfortable.
0039 
0040 FIXME:
0041 -( docbook ) <author lang="">: insert space between the quotes, press "de" and return -> only "d" inserted
0042 -The "Insert Element" dialog isn't case insensitive, but it should be
0043 -See the "fixme"'s in the code
0044 
0045 TODO:
0046 -check for mem leaks
0047 -add "Go to opening/parent tag"?
0048 -check doctype to get top-level element
0049 -can undo behaviour be improved?, e.g. the plugins internal deletions of text
0050  don't have to be an extra step
0051 -don't offer entities if inside tag but outside attribute value
0052 
0053 -Support for more than one namespace at the same time ( e.g. XSLT + XSL-FO )?
0054 =>This could also be handled in the XSLT DTD fragment, as described in the XSLT 1.0 spec,
0055  but then at <xsl:template match="/"><html> it will only show you HTML elements!
0056 =>So better "Assign meta DTD" and "Add meta DTD", the latter will expand the current meta DTD
0057 -Option to insert empty element in <empty/> form
0058 -Show expanded entities with QChar::QChar( int rc ) + unicode font
0059 -Don't ignore entities defined in the document's prologue
0060 -Only offer 'valid' elements, i.e. don't take the elements as a set but check
0061  if the DTD is matched ( order, number of occurrences, ... )
0062 
0063 -Maybe only read the meta DTD file once, then store the resulting QMap on disk ( using QDataStream )?
0064  We'll then have to compare timeOf_cacheFile <-> timeOf_metaDtd.
0065 -Try to use libxml
0066 */
0067 
0068 #include "plugin_katexmltools.h"
0069 
0070 #include <QAction>
0071 #include <QFile>
0072 #include <QFileDialog>
0073 #include <QGuiApplication>
0074 #include <QLabel>
0075 #include <QLineEdit>
0076 #include <QPushButton>
0077 #include <QRegularExpression>
0078 #include <QStandardPaths>
0079 #include <QUrl>
0080 #include <QVBoxLayout>
0081 
0082 #include <ktexteditor/editor.h>
0083 
0084 #include <KActionCollection>
0085 #include <KHistoryComboBox>
0086 #include <KIO/JobUiDelegate>
0087 #include <KIO/TransferJob>
0088 #include <KLocalizedString>
0089 #include <KMessageBox>
0090 #include <KPluginFactory>
0091 #include <KXMLGUIClient>
0092 #include <kxmlguifactory.h>
0093 #include <map>
0094 
0095 K_PLUGIN_FACTORY_WITH_JSON(PluginKateXMLToolsFactory, "katexmltools.json", registerPlugin<PluginKateXMLTools>();)
0096 
0097 PluginKateXMLTools::PluginKateXMLTools(QObject *const parent, const QVariantList &)
0098     : KTextEditor::Plugin(parent)
0099 {
0100 }
0101 
0102 PluginKateXMLTools::~PluginKateXMLTools()
0103 {
0104 }
0105 
0106 QObject *PluginKateXMLTools::createView(KTextEditor::MainWindow *mainWindow)
0107 {
0108     return new PluginKateXMLToolsView(mainWindow);
0109 }
0110 
0111 PluginKateXMLToolsView::PluginKateXMLToolsView(KTextEditor::MainWindow *mainWin)
0112     : QObject(mainWin)
0113     , m_mainWindow(mainWin)
0114     , m_model(this)
0115 {
0116     // qDebug() << "PluginKateXMLTools constructor called";
0117 
0118     KXMLGUIClient::setComponentName(QStringLiteral("katexmltools"), i18n("XML Tools"));
0119     setXMLFile(QStringLiteral("ui.rc"));
0120 
0121     QAction *actionInsert = new QAction(i18n("&Insert Element..."), this);
0122     connect(actionInsert, &QAction::triggered, &m_model, &PluginKateXMLToolsCompletionModel::slotInsertElement);
0123     actionCollection()->addAction(QStringLiteral("xml_tool_insert_element"), actionInsert);
0124     actionCollection()->setDefaultShortcut(actionInsert, Qt::CTRL | Qt::Key_Return);
0125 
0126     QAction *actionClose = new QAction(i18n("&Close Element"), this);
0127     connect(actionClose, &QAction::triggered, &m_model, &PluginKateXMLToolsCompletionModel::slotCloseElement);
0128     actionCollection()->addAction(QStringLiteral("xml_tool_close_element"), actionClose);
0129     actionCollection()->setDefaultShortcut(actionClose, Qt::CTRL | Qt::Key_Less);
0130 
0131     QAction *actionAssignDTD = new QAction(i18n("Assign Meta &DTD..."), this);
0132     connect(actionAssignDTD, &QAction::triggered, &m_model, &PluginKateXMLToolsCompletionModel::getDTD);
0133     actionCollection()->addAction(QStringLiteral("xml_tool_assign"), actionAssignDTD);
0134 
0135     mainWin->guiFactory()->addClient(this);
0136 
0137     connect(KTextEditor::Editor::instance()->application(),
0138             &KTextEditor::Application::documentDeleted,
0139             &m_model,
0140             &PluginKateXMLToolsCompletionModel::slotDocumentDeleted);
0141 }
0142 
0143 PluginKateXMLToolsView::~PluginKateXMLToolsView()
0144 {
0145     m_mainWindow->guiFactory()->removeClient(this);
0146 
0147     // qDebug() << "xml tools destructor 1...";
0148     // TODO: unregister the model
0149 }
0150 
0151 PluginKateXMLToolsCompletionModel::PluginKateXMLToolsCompletionModel(QObject *const parent)
0152     : CodeCompletionModel(parent)
0153     , m_viewToAssignTo(nullptr)
0154     , m_mode(none)
0155     , m_correctPos(0)
0156 {
0157 }
0158 
0159 PluginKateXMLToolsCompletionModel::~PluginKateXMLToolsCompletionModel()
0160 {
0161     qDeleteAll(m_dtds);
0162     m_dtds.clear();
0163 }
0164 
0165 void PluginKateXMLToolsCompletionModel::slotDocumentDeleted(KTextEditor::Document *doc)
0166 {
0167     // Remove the document from m_DTDs, and also delete the PseudoDTD
0168     // if it becomes unused.
0169     if (m_docDtds.contains(doc)) {
0170         qDebug() << "XMLTools:slotDocumentDeleted: documents: " << m_docDtds.count() << ", DTDs: " << m_dtds.count();
0171         PseudoDTD *dtd = m_docDtds.take(doc);
0172 
0173         if (m_docDtds.key(dtd)) {
0174             return;
0175         }
0176 
0177         QHash<QString, PseudoDTD *>::iterator it;
0178         for (it = m_dtds.begin(); it != m_dtds.end(); ++it) {
0179             if (it.value() == dtd) {
0180                 m_dtds.erase(it);
0181                 delete dtd;
0182                 return;
0183             }
0184         }
0185     }
0186 }
0187 
0188 void PluginKateXMLToolsCompletionModel::completionInvoked(KTextEditor::View *kv, const KTextEditor::Range &range, const InvocationType invocationType)
0189 {
0190     Q_UNUSED(range)
0191     Q_UNUSED(invocationType)
0192 
0193     qDebug() << "xml tools completionInvoked";
0194 
0195     KTextEditor::Document *doc = kv->document();
0196     if (!m_docDtds[doc])
0197     // no meta DTD assigned yet
0198     {
0199         return;
0200     }
0201 
0202     // debug to test speed:
0203     // QTime t; t.start();
0204 
0205     beginResetModel();
0206     m_allowed.clear();
0207 
0208     // get char on the left of the cursor:
0209     KTextEditor::Cursor curpos = kv->cursorPosition();
0210     uint line = curpos.line(), col = curpos.column();
0211 
0212     QString lineStr = kv->document()->line(line);
0213     QString leftCh = lineStr.mid(col - 1, 1);
0214     QString secondLeftCh = lineStr.mid(col - 2, 1);
0215 
0216     if (leftCh == QLatin1String("&")) {
0217         qDebug() << "Getting entities";
0218         m_allowed = m_docDtds[doc]->entities(QString());
0219         m_mode = entities;
0220     } else if (leftCh == QLatin1String("<")) {
0221         qDebug() << "*outside tag -> get elements";
0222         QString parentElement = getParentElement(*kv, 1);
0223         qDebug() << "parent: " << parentElement;
0224         m_allowed = m_docDtds[doc]->allowedElements(parentElement);
0225         m_mode = elements;
0226     } else if (leftCh == QLatin1String("/") && secondLeftCh == QLatin1String("<")) {
0227         qDebug() << "*close parent element";
0228         QString parentElement = getParentElement(*kv, 2);
0229 
0230         if (!parentElement.isEmpty()) {
0231             m_mode = closingtag;
0232             m_allowed = QStringList(parentElement);
0233         }
0234     } else if (leftCh == QLatin1Char(' ') || (isQuote(leftCh) && secondLeftCh == QLatin1String("="))) {
0235         // TODO: check secondLeftChar, too?! then you don't need to trigger
0236         // with space and we yet save CPU power
0237         QString currentElement = insideTag(*kv);
0238         QString currentAttribute;
0239         if (!currentElement.isEmpty()) {
0240             currentAttribute = insideAttribute(*kv);
0241         }
0242 
0243         qDebug() << "Tag: " << currentElement;
0244         qDebug() << "Attr: " << currentAttribute;
0245 
0246         if (!currentElement.isEmpty() && !currentAttribute.isEmpty()) {
0247             qDebug() << "*inside attribute -> get attribute values";
0248             m_allowed = m_docDtds[doc]->attributeValues(currentElement, currentAttribute);
0249             if (m_allowed.count() == 1
0250                 && (m_allowed[0] == QLatin1String("CDATA") || m_allowed[0] == QLatin1String("ID") || m_allowed[0] == QLatin1String("IDREF")
0251                     || m_allowed[0] == QLatin1String("IDREFS") || m_allowed[0] == QLatin1String("ENTITY") || m_allowed[0] == QLatin1String("ENTITIES")
0252                     || m_allowed[0] == QLatin1String("NMTOKEN") || m_allowed[0] == QLatin1String("NMTOKENS") || m_allowed[0] == QLatin1String("NAME"))) {
0253                 // these must not be taken literally, e.g. don't insert the string "CDATA"
0254                 m_allowed.clear();
0255             } else {
0256                 m_mode = attributevalues;
0257             }
0258         } else if (!currentElement.isEmpty()) {
0259             qDebug() << "*inside tag -> get attributes";
0260             m_allowed = m_docDtds[doc]->allowedAttributes(currentElement);
0261             m_mode = attributes;
0262         }
0263     }
0264 
0265     // qDebug() << "time elapsed (ms): " << t.elapsed();
0266     qDebug() << "Allowed strings: " << m_allowed.count();
0267 
0268     if (m_allowed.count() >= 1 && m_allowed[0] != QLatin1String("__EMPTY")) {
0269         m_allowed = sortQStringList(m_allowed);
0270     }
0271     setRowCount(m_allowed.count());
0272     endResetModel();
0273 }
0274 
0275 int PluginKateXMLToolsCompletionModel::columnCount(const QModelIndex &) const
0276 {
0277     return 1;
0278 }
0279 
0280 int PluginKateXMLToolsCompletionModel::rowCount(const QModelIndex &parent) const
0281 {
0282     if (!m_allowed.isEmpty()) { // Is there smth to complete?
0283         if (!parent.isValid()) { // Return the only one group node for root
0284             return 1;
0285         }
0286         if (parent.internalId() == groupNode) { // Return available rows count for group level node
0287             return m_allowed.size();
0288         }
0289     }
0290     return 0;
0291 }
0292 
0293 QModelIndex PluginKateXMLToolsCompletionModel::parent(const QModelIndex &index) const
0294 {
0295     if (!index.isValid()) { // Is root/invalid index?
0296         return QModelIndex(); // Nothing to return...
0297     }
0298     if (index.internalId() == groupNode) { // Return a root node for group
0299         return QModelIndex();
0300     }
0301     // Otherwise, this is a leaf level, so return the only group as a parent
0302     return createIndex(0, 0, groupNode);
0303 }
0304 
0305 QModelIndex PluginKateXMLToolsCompletionModel::index(const int row, const int column, const QModelIndex &parent) const
0306 {
0307     if (!parent.isValid()) {
0308         // At 'top' level only 'header' present, so nothing else than row 0 can be here...
0309         return row == 0 ? createIndex(row, column, groupNode) : QModelIndex();
0310     }
0311     if (parent.internalId() == groupNode) { // Is this a group node?
0312         if (0 <= row && row < m_allowed.size()) { // Make sure to return only valid indices
0313             return createIndex(row, column, nullptr); // Just return a leaf-level index
0314         }
0315     }
0316     // Leaf node has no children... nothing to return
0317     return QModelIndex();
0318 }
0319 
0320 QVariant PluginKateXMLToolsCompletionModel::data(const QModelIndex &index, int role) const
0321 {
0322     if (!index.isValid()) { // Nothing to do w/ invalid index
0323         return QVariant();
0324     }
0325 
0326     if (index.internalId() == groupNode) { // Return group level node data
0327         switch (role) {
0328         case KTextEditor::CodeCompletionModel::GroupRole:
0329             return QVariant(Qt::DisplayRole);
0330         case Qt::DisplayRole:
0331             return currentModeToString();
0332         default:
0333             break;
0334         }
0335         return QVariant(); // Nothing to return for other roles
0336     }
0337     switch (role) {
0338     case Qt::DisplayRole:
0339         switch (index.column()) {
0340         case KTextEditor::CodeCompletionModel::Name:
0341             return m_allowed.at(index.row());
0342         default:
0343             break;
0344         }
0345     default:
0346         break;
0347     }
0348     return QVariant();
0349 }
0350 
0351 bool PluginKateXMLToolsCompletionModel::shouldStartCompletion(KTextEditor::View *view,
0352                                                               const QString &insertedText,
0353                                                               bool userInsertion,
0354                                                               const KTextEditor::Cursor &position)
0355 {
0356     Q_UNUSED(view)
0357     Q_UNUSED(userInsertion)
0358     Q_UNUSED(position)
0359     const QString triggerChars = QStringLiteral("&</ '\""); // these are subsequently handled by completionInvoked()
0360 
0361     return triggerChars.contains(insertedText.right(1));
0362 }
0363 
0364 /**
0365  * Load the meta DTD. In case of success set the 'ready'
0366  * flag to true, to show that we're is ready to give hints about the DTD.
0367  */
0368 void PluginKateXMLToolsCompletionModel::getDTD()
0369 {
0370     if (!KTextEditor::Editor::instance()->application()->activeMainWindow()) {
0371         return;
0372     }
0373 
0374     KTextEditor::View *kv = KTextEditor::Editor::instance()->application()->activeMainWindow()->activeView();
0375     if (!kv) {
0376         qDebug() << "Warning: no KTextEditor::View";
0377         return;
0378     }
0379 
0380     // ### replace this with something more sane
0381     // Start where the supplied XML-DTDs are fed by default unless
0382     // user changed directory last time:
0383     QString defaultDir = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("katexmltools")) + "/katexmltools/";
0384     if (m_urlString.isNull()) {
0385         m_urlString = defaultDir;
0386     }
0387 
0388     // Guess the meta DTD by looking at the doctype's public identifier.
0389     // XML allows comments etc. before the doctype, so look further than
0390     // just the first line.
0391     // Example syntax:
0392     // <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd">
0393     uint checkMaxLines = 200;
0394     QString documentStart = kv->document()->text(KTextEditor::Range(0, 0, checkMaxLines + 1, 0));
0395     const QRegularExpression re(QStringLiteral("<!DOCTYPE\\s+\\b(\\w+)\\b\\s+PUBLIC\\s+[\"\']([^\"\']+?)[\"\']"), QRegularExpression::CaseInsensitiveOption);
0396     const QRegularExpressionMatch match = re.match(documentStart);
0397     QString filename;
0398     QString doctype;
0399     QString topElement;
0400 
0401     if (match.hasMatch()) {
0402         topElement = match.captured(1);
0403         doctype = match.captured(2);
0404         qDebug() << "Top element: " << topElement;
0405         qDebug() << "Doctype match: " << doctype;
0406         // XHTML:
0407         if (doctype == QLatin1String("-//W3C//DTD XHTML 1.0 Transitional//EN")) {
0408             filename = QStringLiteral("xhtml1-transitional.dtd.xml");
0409         } else if (doctype == QLatin1String("-//W3C//DTD XHTML 1.0 Strict//EN")) {
0410             filename = QStringLiteral("xhtml1-strict.dtd.xml");
0411         } else if (doctype == QLatin1String("-//W3C//DTD XHTML 1.0 Frameset//EN")) {
0412             filename = QStringLiteral("xhtml1-frameset.dtd.xml");
0413         }
0414         // HTML 4.0:
0415         else if (doctype == QLatin1String("-//W3C//DTD HTML 4.01 Transitional//EN")) {
0416             filename = QStringLiteral("html4-loose.dtd.xml");
0417         } else if (doctype == QLatin1String("-//W3C//DTD HTML 4.01//EN")) {
0418             filename = QStringLiteral("html4-strict.dtd.xml");
0419         }
0420         // KDE Docbook:
0421         else if (doctype == QLatin1String("-//KDE//DTD DocBook XML V4.1.2-Based Variant V1.1//EN")) {
0422             filename = QStringLiteral("kde-docbook.dtd.xml");
0423         }
0424     } else if (documentStart.indexOf(QLatin1String("<xsl:stylesheet")) != -1
0425                && documentStart.indexOf(QLatin1String("xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\"")) != -1) {
0426         /* XSLT doesn't have a doctype/DTD. We look for an xsl:stylesheet tag instead.
0427           Example:
0428           <xsl:stylesheet version="1.0"
0429           xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
0430           xmlns="http://www.w3.org/TR/xhtml1/strict">
0431         */
0432         filename = QStringLiteral("xslt-1.0.dtd.xml");
0433         doctype = QStringLiteral("XSLT 1.0");
0434     } else {
0435         qDebug() << "No doctype found";
0436     }
0437 
0438     QUrl url;
0439     if (filename.isEmpty()) {
0440         // no meta dtd found for this file
0441         url = QFileDialog::getOpenFileUrl(KTextEditor::Editor::instance()->application()->activeMainWindow()->window(),
0442                                           i18n("Assign Meta DTD in XML Format"),
0443                                           QUrl::fromLocalFile(m_urlString),
0444                                           QStringLiteral("*.xml"));
0445     } else {
0446         url.setUrl(defaultDir + filename);
0447         KMessageBox::information(nullptr,
0448                                  i18n("The current file has been identified "
0449                                       "as a document of type \"%1\". The meta DTD for this document type "
0450                                       "will now be loaded.",
0451                                       doctype),
0452                                  i18n("Loading XML Meta DTD"),
0453                                  QStringLiteral("DTDAssigned"));
0454     }
0455 
0456     if (url.isEmpty()) {
0457         return;
0458     }
0459 
0460     m_urlString = url.url(); // remember directory for next time
0461 
0462     if (m_dtds[m_urlString]) {
0463         assignDTD(m_dtds[m_urlString], kv);
0464     } else {
0465         m_dtdString.clear();
0466         m_viewToAssignTo = kv;
0467 
0468         QGuiApplication::setOverrideCursor(Qt::WaitCursor);
0469         KIO::TransferJob *job = KIO::get(url);
0470         connect(job, &KIO::TransferJob::result, this, &PluginKateXMLToolsCompletionModel::slotFinished);
0471         connect(job, &KIO::TransferJob::data, this, &PluginKateXMLToolsCompletionModel::slotData);
0472     }
0473     qDebug() << "XMLTools::getDTD: Documents: " << m_docDtds.count() << ", DTDs: " << m_dtds.count();
0474 }
0475 
0476 void PluginKateXMLToolsCompletionModel::slotFinished(KJob *job)
0477 {
0478     if (job->error()) {
0479         // qDebug() << "XML Plugin error: DTD in XML format (" << filename << " ) could not be loaded";
0480         static_cast<KIO::Job *>(job)->uiDelegate()->showErrorMessage();
0481     } else if (static_cast<KIO::TransferJob *>(job)->isErrorPage()) {
0482         // catch failed loading loading via http:
0483         KMessageBox::error(nullptr,
0484                            i18n("The file '%1' could not be opened. "
0485                                 "The server returned an error.",
0486                                 m_urlString),
0487                            i18n("XML Plugin Error"));
0488     } else {
0489         PseudoDTD *dtd = new PseudoDTD();
0490         dtd->analyzeDTD(m_urlString, m_dtdString);
0491 
0492         m_dtds.insert(m_urlString, dtd);
0493         assignDTD(dtd, m_viewToAssignTo);
0494 
0495         // clean up a bit
0496         m_viewToAssignTo = nullptr;
0497         m_dtdString.clear();
0498     }
0499     QGuiApplication::restoreOverrideCursor();
0500 }
0501 
0502 void PluginKateXMLToolsCompletionModel::slotData(KIO::Job *, const QByteArray &data)
0503 {
0504     m_dtdString += QString(data);
0505 }
0506 
0507 void PluginKateXMLToolsCompletionModel::assignDTD(PseudoDTD *dtd, KTextEditor::View *view)
0508 {
0509     m_docDtds.insert(view->document(), dtd);
0510 
0511     // TODO:perhaps for all views()?
0512     view->registerCompletionModel(this);
0513     view->setAutomaticInvocationEnabled(true);
0514 }
0515 
0516 /**
0517  * Offer a line edit with completion for possible elements at cursor position and insert the
0518  * tag one chosen/entered by the user, plus its closing tag. If there's a text selection,
0519  * add the markup around it.
0520  */
0521 void PluginKateXMLToolsCompletionModel::slotInsertElement()
0522 {
0523     if (!KTextEditor::Editor::instance()->application()->activeMainWindow()) {
0524         return;
0525     }
0526 
0527     KTextEditor::View *kv = KTextEditor::Editor::instance()->application()->activeMainWindow()->activeView();
0528     if (!kv) {
0529         qDebug() << "Warning: no KTextEditor::View";
0530         return;
0531     }
0532 
0533     KTextEditor::Document *doc = kv->document();
0534     PseudoDTD *dtd = m_docDtds[doc];
0535     QString parentElement = getParentElement(*kv, 0);
0536     QStringList allowed;
0537 
0538     if (dtd) {
0539         allowed = dtd->allowedElements(parentElement);
0540     }
0541 
0542     QString text;
0543     InsertElement dialog(allowed, kv);
0544     if (dialog.exec() == QDialog::Accepted) {
0545         text = dialog.text();
0546     }
0547 
0548     if (!text.isEmpty()) {
0549         QStringList list = text.split(QChar(' '));
0550         QString pre;
0551         QString post;
0552         // anders: use <tagname/> if the tag is required to be empty.
0553         // In that case maybe we should not remove the selection? or overwrite it?
0554         int adjust = 0; // how much to move cursor.
0555         // if we know that we have attributes, it goes
0556         // just after the tag name, otherwise between tags.
0557         if (dtd && dtd->allowedAttributes(list[0]).count()) {
0558             adjust++; // the ">"
0559         }
0560 
0561         if (dtd && dtd->allowedElements(list[0]).contains(QLatin1String("__EMPTY"))) {
0562             pre = '<' + text + "/>";
0563             if (adjust) {
0564                 adjust++; // for the "/"
0565             }
0566         } else {
0567             pre = '<' + text + '>';
0568             post = "</" + list[0] + '>';
0569         }
0570 
0571         QString marked;
0572         if (!post.isEmpty()) {
0573             marked = kv->selectionText();
0574         }
0575 
0576         KTextEditor::Document::EditingTransaction transaction(doc);
0577 
0578         if (!marked.isEmpty()) {
0579             kv->removeSelectionText();
0580         }
0581 
0582         // with the old selection now removed, curPos points to the start of pre
0583         KTextEditor::Cursor curPos = kv->cursorPosition();
0584         curPos.setColumn(curPos.column() + pre.length() - adjust);
0585 
0586         kv->insertText(pre + marked + post);
0587 
0588         kv->setCursorPosition(curPos);
0589     }
0590 }
0591 
0592 /**
0593  * Insert a closing tag for the nearest not-closed parent element.
0594  */
0595 void PluginKateXMLToolsCompletionModel::slotCloseElement()
0596 {
0597     if (!KTextEditor::Editor::instance()->application()->activeMainWindow()) {
0598         return;
0599     }
0600 
0601     KTextEditor::View *kv = KTextEditor::Editor::instance()->application()->activeMainWindow()->activeView();
0602     if (!kv) {
0603         qDebug() << "Warning: no KTextEditor::View";
0604         return;
0605     }
0606     QString parentElement = getParentElement(*kv, 0);
0607 
0608     // qDebug() << "parentElement: '" << parentElement << "'";
0609     QString closeTag = "</" + parentElement + '>';
0610     if (!parentElement.isEmpty()) {
0611         kv->insertText(closeTag);
0612     }
0613 }
0614 
0615 // modify the completion string before it gets inserted
0616 void PluginKateXMLToolsCompletionModel::executeCompletionItem(KTextEditor::View *view, const KTextEditor::Range &word, const QModelIndex &index) const
0617 {
0618     KTextEditor::Range toReplace = word;
0619     KTextEditor::Document *document = view->document();
0620 
0621     QString text = data(index.sibling(index.row(), Name), Qt::DisplayRole).toString();
0622 
0623     qDebug() << "executeCompletionItem text: " << text;
0624 
0625     int line, col;
0626     view->cursorPosition().position(line, col);
0627     QString lineStr = document->line(line);
0628     QString rightCh = lineStr.mid(col, 1);
0629 
0630     int posCorrection = 0; // where to move the cursor after completion ( >0 = move right )
0631     if (m_mode == entities) {
0632         text = text + ';';
0633     }
0634 
0635     else if (m_mode == attributes) {
0636         text = text + "=\"\"";
0637         posCorrection = -1;
0638         if (!rightCh.isEmpty() && rightCh != QLatin1String(">") && rightCh != QLatin1String("/") && rightCh != QLatin1String(" ")) {
0639             // TODO: other whitespaces
0640             // add space in front of the next attribute
0641             text = text + ' ';
0642             posCorrection--;
0643         }
0644     }
0645 
0646     else if (m_mode == attributevalues) {
0647         // TODO: support more than one line
0648         uint startAttValue = 0;
0649         uint endAttValue = 0;
0650 
0651         // find left quote:
0652         for (startAttValue = col; startAttValue > 0; startAttValue--) {
0653             QString ch = lineStr.mid(startAttValue - 1, 1);
0654             if (isQuote(ch)) {
0655                 break;
0656             }
0657         }
0658 
0659         // find right quote:
0660         for (endAttValue = col; endAttValue <= static_cast<uint>(lineStr.length()); endAttValue++) {
0661             QString ch = lineStr.mid(endAttValue - 1, 1);
0662             if (isQuote(ch)) {
0663                 break;
0664             }
0665         }
0666 
0667         // replace the current contents of the attribute
0668         if (startAttValue < endAttValue) {
0669             toReplace = KTextEditor::Range(line, startAttValue, line, endAttValue - 1);
0670         }
0671     }
0672 
0673     else if (m_mode == elements) {
0674         // anders: if the tag is marked EMPTY, insert in form <tagname/>
0675         QString str;
0676         bool isEmptyTag = m_docDtds[document]->allowedElements(text).contains(QLatin1String("__EMPTY"));
0677         if (isEmptyTag) {
0678             str = text + "/>";
0679         } else {
0680             str = text + "></" + text + '>';
0681         }
0682 
0683         // Place the cursor where it is most likely wanted:
0684         // always inside the tag if the tag is empty AND the DTD indicates that there are attribs)
0685         // outside for open tags, UNLESS there are mandatory attributes
0686         if (m_docDtds[document]->requiredAttributes(text).count() || (isEmptyTag && m_docDtds[document]->allowedAttributes(text).count())) {
0687             posCorrection = text.length() - str.length();
0688         } else if (!isEmptyTag) {
0689             posCorrection = text.length() - str.length() + 1;
0690         }
0691 
0692         text = str;
0693     }
0694 
0695     else if (m_mode == closingtag) {
0696         text += '>';
0697     }
0698 
0699     document->replaceText(toReplace, text);
0700 
0701     // move the cursor to desired position
0702     KTextEditor::Cursor curPos = view->cursorPosition();
0703     curPos.setColumn(curPos.column() + posCorrection);
0704     view->setCursorPosition(curPos);
0705 }
0706 
0707 // ========================================================================
0708 // Pseudo-XML stuff:
0709 
0710 /**
0711  * Check if cursor is inside a tag, that is
0712  * if "<" occurs before ">" occurs ( on the left side of the cursor ).
0713  * Return the tag name, return "" if we cursor is outside a tag.
0714  */
0715 QString PluginKateXMLToolsCompletionModel::insideTag(KTextEditor::View &kv)
0716 {
0717     int line, col;
0718     kv.cursorPosition().position(line, col);
0719     int y = line; // another variable because uint <-> int
0720 
0721     do {
0722         QString lineStr = kv.document()->line(y);
0723         for (uint x = col; x > 0; x--) {
0724             QString ch = lineStr.mid(x - 1, 1);
0725             if (ch == QLatin1String(">")) { // cursor is outside tag
0726                 return QString();
0727             }
0728 
0729             if (ch == QLatin1String("<")) {
0730                 QString tag;
0731                 // look for white space on the right to get the tag name
0732                 for (int z = x; z <= lineStr.length(); ++z) {
0733                     ch = lineStr.mid(z - 1, 1);
0734                     if (ch.at(0).isSpace() || ch == QLatin1String("/") || ch == QLatin1String(">")) {
0735                         return tag.right(tag.length() - 1);
0736                     }
0737 
0738                     if (z == lineStr.length()) {
0739                         tag += ch;
0740                         return tag.right(tag.length() - 1);
0741                     }
0742 
0743                     tag += ch;
0744                 }
0745             }
0746         }
0747         y--;
0748         col = kv.document()->line(y).length();
0749     } while (y >= 0);
0750 
0751     return QString();
0752 }
0753 
0754 /**
0755  * Check if cursor is inside an attribute value, that is
0756  * if '="' is on the left, and if it's nearer than "<" or ">".
0757  *
0758  * @Return the attribute name or "" if we're outside an attribute
0759  * value.
0760  *
0761  * Note: only call when insideTag() == true.
0762  * TODO: allow whitespace around "="
0763  */
0764 QString PluginKateXMLToolsCompletionModel::insideAttribute(KTextEditor::View &kv)
0765 {
0766     int line, col;
0767     kv.cursorPosition().position(line, col);
0768     int y = line; // another variable because uint <-> int
0769     uint x = 0;
0770     QString lineStr;
0771     QString ch;
0772 
0773     do {
0774         lineStr = kv.document()->line(y);
0775         for (x = col; x > 0; x--) {
0776             ch = lineStr.mid(x - 1, 1);
0777             QString chLeft = lineStr.mid(x - 2, 1);
0778             // TODO: allow whitespace
0779             if (isQuote(ch) && chLeft == QLatin1String("=")) {
0780                 break;
0781             } else if (isQuote(ch) && chLeft != QLatin1String("=")) {
0782                 return QString();
0783             } else if (ch == QLatin1String("<") || ch == QLatin1String(">")) {
0784                 return QString();
0785             }
0786         }
0787         y--;
0788         col = kv.document()->line(y).length();
0789     } while (!isQuote(ch));
0790 
0791     // look for next white space on the left to get the tag name
0792     QString attr;
0793     for (int z = x; z >= 0; z--) {
0794         ch = lineStr.mid(z - 1, 1);
0795 
0796         if (ch.at(0).isSpace()) {
0797             break;
0798         }
0799 
0800         if (z == 0) {
0801             // start of line == whitespace
0802             attr += ch;
0803             break;
0804         }
0805 
0806         attr = ch + attr;
0807     }
0808 
0809     return attr.left(attr.length() - 2);
0810 }
0811 
0812 /**
0813  * Find the parent element for the current cursor position. That is,
0814  * go left and find the first opening element that's not closed yet,
0815  * ignoring empty elements.
0816  * Examples: If cursor is at "X", the correct parent element is "p":
0817  * <p> <a x="xyz"> foo <i> test </i> bar </a> X
0818  * <p> <a x="xyz"> foo bar </a> X
0819  * <p> foo <img/> bar X
0820  * <p> foo bar X
0821  */
0822 QString PluginKateXMLToolsCompletionModel::getParentElement(KTextEditor::View &kv, int skipCharacters)
0823 {
0824     enum { parsingText, parsingElement, parsingElementBoundary, parsingNonElement, parsingAttributeDquote, parsingAttributeSquote, parsingIgnore } parseState;
0825     parseState = (skipCharacters > 0) ? parsingIgnore : parsingText;
0826 
0827     int nestingLevel = 0;
0828 
0829     int line, col;
0830     kv.cursorPosition().position(line, col);
0831     QString str = kv.document()->line(line);
0832 
0833     while (true) {
0834         // move left a character
0835         if (!col--) {
0836             do {
0837                 if (!line--) {
0838                     return QString(); // reached start of document
0839                 }
0840                 str = kv.document()->line(line);
0841                 col = str.length();
0842             } while (!col);
0843             --col;
0844         }
0845 
0846         ushort ch = str.at(col).unicode();
0847 
0848         switch (parseState) {
0849         case parsingIgnore:
0850             // ignore the specified number of characters
0851             parseState = (--skipCharacters > 0) ? parsingIgnore : parsingText;
0852             break;
0853 
0854         case parsingText:
0855             switch (ch) {
0856             case '<':
0857                 // hmm... we were actually inside an element
0858                 return QString();
0859 
0860             case '>':
0861                 // we just hit an element boundary
0862                 parseState = parsingElementBoundary;
0863                 break;
0864             }
0865             break;
0866 
0867         case parsingElement:
0868             switch (ch) {
0869             case '"': // attribute ( double quoted )
0870                 parseState = parsingAttributeDquote;
0871                 break;
0872 
0873             case '\'': // attribute ( single quoted )
0874                 parseState = parsingAttributeSquote;
0875                 break;
0876 
0877             case '/': // close tag
0878                 parseState = parsingNonElement;
0879                 ++nestingLevel;
0880                 break;
0881 
0882             case '<':
0883                 // we just hit the start of the element...
0884                 if (nestingLevel--) {
0885                     break;
0886                 }
0887 
0888                 QString tag = str.mid(col + 1);
0889                 for (uint pos = 0, len = tag.length(); pos < len; ++pos) {
0890                     ch = tag.at(pos).unicode();
0891                     if (ch == ' ' || ch == '\t' || ch == '>') {
0892                         tag.truncate(pos);
0893                         break;
0894                     }
0895                 }
0896                 return tag;
0897             }
0898             break;
0899 
0900         case parsingElementBoundary:
0901             switch (ch) {
0902             case '?': // processing instruction
0903             case '-': // comment
0904             case '/': // empty element
0905                 parseState = parsingNonElement;
0906                 break;
0907 
0908             case '"':
0909                 parseState = parsingAttributeDquote;
0910                 break;
0911 
0912             case '\'':
0913                 parseState = parsingAttributeSquote;
0914                 break;
0915 
0916             case '<': // empty tag ( bad XML )
0917                 parseState = parsingText;
0918                 break;
0919 
0920             default:
0921                 parseState = parsingElement;
0922             }
0923             break;
0924 
0925         case parsingAttributeDquote:
0926             if (ch == '"') {
0927                 parseState = parsingElement;
0928             }
0929             break;
0930 
0931         case parsingAttributeSquote:
0932             if (ch == '\'') {
0933                 parseState = parsingElement;
0934             }
0935             break;
0936 
0937         case parsingNonElement:
0938             if (ch == '<') {
0939                 parseState = parsingText;
0940             }
0941             break;
0942         }
0943     }
0944 }
0945 
0946 /**
0947  * Return true if the tag is neither a closing tag
0948  * nor an empty tag, nor a comment, nor processing instruction.
0949  */
0950 bool PluginKateXMLToolsCompletionModel::isOpeningTag(const QString &tag)
0951 {
0952     return (!isClosingTag(tag) && !isEmptyTag(tag) && !tag.startsWith(QLatin1String("<?")) && !tag.startsWith(QLatin1String("<!")));
0953 }
0954 
0955 /**
0956  * Return true if the tag is a closing tag. Return false
0957  * if the tag is an opening tag or an empty tag ( ! )
0958  */
0959 bool PluginKateXMLToolsCompletionModel::isClosingTag(const QString &tag)
0960 {
0961     return (tag.startsWith(QLatin1String("</")));
0962 }
0963 
0964 bool PluginKateXMLToolsCompletionModel::isEmptyTag(const QString &tag)
0965 {
0966     return (tag.right(2) == QLatin1String("/>"));
0967 }
0968 
0969 /**
0970  * Return true if ch is a single or double quote. Expects ch to be of length 1.
0971  */
0972 bool PluginKateXMLToolsCompletionModel::isQuote(const QString &ch)
0973 {
0974     return (ch == QLatin1String("\"") || ch == QLatin1String("'"));
0975 }
0976 
0977 // ========================================================================
0978 // Tools:
0979 
0980 /// Get string describing current mode
0981 QString PluginKateXMLToolsCompletionModel::currentModeToString() const
0982 {
0983     switch (m_mode) {
0984     case entities:
0985         return i18n("XML entities");
0986     case attributevalues:
0987         return i18n("XML attribute values");
0988     case attributes:
0989         return i18n("XML attributes");
0990     case elements:
0991     case closingtag:
0992         return i18n("XML elements");
0993     default:
0994         break;
0995     }
0996     return QString();
0997 }
0998 
0999 /** Sort a QStringList case-insensitively. Static. TODO: make it more simple. */
1000 QStringList PluginKateXMLToolsCompletionModel::sortQStringList(QStringList list)
1001 {
1002     // Sort list case-insensitive. This looks complicated but using a map
1003     // is even suggested by the Qt documentation.
1004     std::map<QString, QString> mapList;
1005     for (const auto &str : qAsConst(list)) {
1006         if (mapList.find(str.toLower()) != mapList.end()) {
1007             // do not override a previous value, e.g. "Auml" and "auml" are two different
1008             // entities, but they should be sorted next to each other.
1009             // TODO: currently it's undefined if e.g. "A" or "a" comes first, it depends on
1010             // the meta DTD ( really? it seems to work okay?!? )
1011             mapList[str.toLower() + '_'] = str;
1012         } else {
1013             mapList[str.toLower()] = str;
1014         }
1015     }
1016 
1017     list.clear();
1018 
1019     // Qt doc: "the items are alphabetically sorted [by key] when iterating over the map":
1020     for (const auto &[_, value] : mapList) {
1021         list.append(value);
1022     }
1023 
1024     return list;
1025 }
1026 
1027 // BEGIN InsertElement dialog
1028 InsertElement::InsertElement(const QStringList &completions, QWidget *parent)
1029     : QDialog(parent)
1030 {
1031     setWindowTitle(i18n("Insert XML Element"));
1032 
1033     QVBoxLayout *topLayout = new QVBoxLayout(this);
1034 
1035     // label
1036     QString text = i18n("Enter XML tag name and attributes (\"<\", \">\" and closing tag will be supplied):");
1037     QLabel *label = new QLabel(text, this);
1038     label->setWordWrap(true);
1039     // combo box
1040     m_cmbElements = new KHistoryComboBox(this);
1041     static_cast<KHistoryComboBox *>(m_cmbElements)->setHistoryItems(completions, true);
1042     connect(m_cmbElements->lineEdit(), &QLineEdit::textChanged, this, &InsertElement::slotHistoryTextChanged);
1043 
1044     // button box
1045     QDialogButtonBox *box = new QDialogButtonBox(this);
1046     box->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
1047     m_okButton = box->button(QDialogButtonBox::Ok);
1048     m_okButton->setDefault(true);
1049 
1050     connect(box, &QDialogButtonBox::accepted, this, &InsertElement::accept);
1051     connect(box, &QDialogButtonBox::rejected, this, &InsertElement::reject);
1052 
1053     // fill layout
1054     topLayout->addWidget(label);
1055     topLayout->addWidget(m_cmbElements);
1056     topLayout->addWidget(box);
1057 
1058     m_cmbElements->setFocus();
1059 
1060     // make sure the ok button is enabled/disabled correctly
1061     slotHistoryTextChanged(m_cmbElements->lineEdit()->text());
1062 }
1063 
1064 InsertElement::~InsertElement()
1065 {
1066 }
1067 
1068 void InsertElement::slotHistoryTextChanged(const QString &text)
1069 {
1070     m_okButton->setEnabled(!text.isEmpty());
1071 }
1072 
1073 QString InsertElement::text() const
1074 {
1075     return m_cmbElements->currentText();
1076 }
1077 // END InsertElement dialog
1078 
1079 #include "moc_plugin_katexmltools.cpp"
1080 #include "plugin_katexmltools.moc"
1081 
1082 // kate: space-indent on; indent-width 4; replace-tabs on; mixed-indent off;