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;