1/***************************************************************************
2 pluginKatexmltools.cpp
3
4 List elements, attributes, attribute values and entities allowed by DTD.
5 Needs a DTD in XML format ( as produced by dtdparse ) for most features.
6
7 copyright : ( C ) 2001-2002 by Daniel Naber
8 email : daniel.naber@t-online.de
9
10 Copyright (C) 2005 by Anders Lund <anders@alweb.dk>
11
12 KDE SC 4 version (C) 2010 Tomas Trnka <tomastrnka@gmx.com>
13 ***************************************************************************/
14
15/***************************************************************************
16 This program is free software; you can redistribute it and/or
17 modify it under the terms of the GNU General Public License
18 as published by the Free Software Foundation; either version 2
19 of the License, or ( at your option ) any later version.
20
21 This program is distributed in the hope that it will be useful,
22 but WITHOUT ANY WARRANTY; without even the implied warranty of
23 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 GNU General Public License for more details.
25
26 You should have received a copy of the GNU General Public License
27 along with this program; if not, write to the Free Software
28 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
29 ***************************************************************************/
30
31/*
32README:
33The basic idea is this: certain keyEvents(), namely [<&" ], trigger a completion box.
34This is intended as a help for editing. There are some cases where the XML
35spec is not followed, e.g. one can add the same attribute twice to an element.
36Also see the user documentation. If backspace is pressed after a completion popup
37was closed, the popup will re-open. This way typos can be corrected and the popup
38will reappear, which is quite comfortable.
39
40FIXME:
41-( docbook ) <author lang="">: insert space between the quotes, press "de" and return -> only "d" inserted
42-The "Insert Element" dialog isn't case insensitive, but it should be
43-See the "fixme"'s in the code
44
45TODO:
46-check for mem leaks
47-add "Go to opening/parent tag"?
48-check doctype to get top-level element
49-can undo behaviour be improved?, e.g. the plugins internal deletions of text
50 don't have to be an extra step
51-don't offer entities if inside tag but outside attribute value
52
53-Support for more than one namespace at the same time ( e.g. XSLT + XSL-FO )?
54=>This could also be handled in the XSLT DTD fragment, as described in the XSLT 1.0 spec,
55 but then at <xsl:template match="/"><html> it will only show you HTML elements!
56=>So better "Assign meta DTD" and "Add meta DTD", the latter will expand the current meta DTD
57-Option to insert empty element in <empty/> form
58-Show expanded entities with QChar::QChar( int rc ) + unicode font
59-Don't ignore entities defined in the document's prologue
60-Only offer 'valid' elements, i.e. don't take the elements as a set but check
61 if the DTD is matched ( order, number of occurrences, ... )
62
63-Maybe only read the meta DTD file once, then store the resulting QMap on disk ( using QDataStream )?
64 We'll then have to compare timeOf_cacheFile <-> timeOf_metaDtd.
65-Try to use libxml
66*/
67
68#include "plugin_katexmltools.h"
69#include "plugin_katexmltools.moc"
70
71#include <assert.h>
72
73#include <qdatetime.h>
74#include <qdom.h>
75#include <qfile.h>
76#include <qlayout.h>
77#include <qpushbutton.h>
78#include <qregexp.h>
79#include <qstring.h>
80#include <qtimer.h>
81#include <QLabel>
82#include <QVBoxLayout>
83
84#include <kaction.h>
85#include <kactioncollection.h>
86#include <kapplication.h>
87#include <klineedit.h>
88#include <kdebug.h>
89#include <kfiledialog.h>
90#include <kglobal.h>
91#include <khistorycombobox.h>
92#include <kcomponentdata.h>
93#include <kio/job.h>
94#include <kio/jobuidelegate.h>
95#include <klocale.h>
96#include <kmessagebox.h>
97#include <kstandarddirs.h>
98#include <kpluginfactory.h>
99
100K_PLUGIN_FACTORY(PluginKateXMLToolsFactory, registerPlugin<PluginKateXMLTools>();)
101K_EXPORT_PLUGIN(PluginKateXMLToolsFactory("katexmltools"))
102
103using Kate::application;
104
105
106PluginKateXMLTools::PluginKateXMLTools( QObject* const parent, const QVariantList& )
107 : Kate::Plugin ( (Kate::Application *)parent )
108{
109}
110
111PluginKateXMLTools::~PluginKateXMLTools()
112{
113}
114
115Kate::PluginView *PluginKateXMLTools::createView(Kate::MainWindow *mainWindow)
116{
117 return new PluginKateXMLToolsView(mainWindow);
118}
119
120
121PluginKateXMLToolsView::PluginKateXMLToolsView(Kate::MainWindow* const win)
122 : Kate::PluginView ( win ), Kate::XMLGUIClient ( PluginKateXMLToolsFactory::componentData() ),
123 m_model ( this )
124{
125 //kDebug() << "PluginKateXMLTools constructor called";
126
127
128 KAction *actionInsert = new KAction ( i18n("&Insert Element..."), this );
129 actionInsert->setShortcut( Qt::CTRL+Qt::Key_Return );
130 connect( actionInsert, SIGNAL(triggered()), &m_model, SLOT(slotInsertElement()) );
131 actionCollection()->addAction( "xml_tool_insert_element", actionInsert );
132
133 KAction *actionClose = new KAction ( i18n("&Close Element"), this );
134 actionClose->setShortcut( Qt::CTRL+Qt::Key_Less );
135 connect( actionClose, SIGNAL(triggered()), &m_model, SLOT(slotCloseElement()) );
136 actionCollection()->addAction( "xml_tool_close_element", actionClose );
137
138 KAction *actionAssignDTD = new KAction ( i18n("Assign Meta &DTD..." ), this );
139 connect( actionAssignDTD, SIGNAL(triggered()), &m_model, SLOT(getDTD()) );
140 actionCollection()->addAction( "xml_tool_assign", actionAssignDTD );
141
142 win->guiFactory()->addClient( this );
143
144 connect( application()->documentManager(), SIGNAL(documentDeleted(KTextEditor::Document*)),
145 &m_model, SLOT(slotDocumentDeleted(KTextEditor::Document*)) );
146
147}
148
149PluginKateXMLToolsView::~PluginKateXMLToolsView()
150{
151 mainWindow()->guiFactory()->removeClient (this);
152
153 //kDebug() << "xml tools descructor 1...";
154 //TODO: unregister the model
155}
156
157PluginKateXMLToolsCompletionModel::PluginKateXMLToolsCompletionModel( QObject* const parent )
158 : CodeCompletionModel2 (parent)
159 , m_docToAssignTo(0)
160 , m_mode(none)
161 , m_correctPos(0)
162{
163}
164
165PluginKateXMLToolsCompletionModel::~PluginKateXMLToolsCompletionModel()
166{
167 qDeleteAll( m_dtds );
168 m_dtds.clear();
169}
170
171void PluginKateXMLToolsCompletionModel::slotDocumentDeleted( KTextEditor::Document *doc )
172{
173 // Remove the document from m_DTDs, and also delete the PseudoDTD
174 // if it becomes unused.
175 if ( m_docDtds.contains (doc) )
176 {
177 kDebug()<<"XMLTools:slotDocumentDeleted: documents: "<<m_docDtds.count()<<", DTDs: "<<m_dtds.count();
178 PseudoDTD *dtd = m_docDtds.take( doc );
179
180 if ( m_docDtds.key( dtd ) )
181 return;
182
183 QHash<QString, PseudoDTD *>::iterator it;
184 for ( it = m_dtds.begin() ; it != m_dtds.end() ; ++it )
185 {
186 if ( it.value() == dtd )
187 {
188 m_dtds.erase(it);
189 delete dtd;
190 return;
191 }
192 }
193 }
194}
195
196
197void PluginKateXMLToolsCompletionModel::completionInvoked(
198 KTextEditor::View *kv
199 , const KTextEditor::Range &range
200 , const InvocationType invocationType
201 )
202{
203 Q_UNUSED( range )
204 Q_UNUSED( invocationType )
205
206 kDebug() << "xml tools completionInvoked";
207
208 KTextEditor::Document *doc = kv->document();
209 if( ! m_docDtds[ doc ] )
210 // no meta DTD assigned yet
211 return;
212
213 // debug to test speed:
214 //QTime t; t.start();
215
216 beginResetModel();
217 m_allowed.clear();
218
219 // get char on the left of the cursor:
220 KTextEditor::Cursor curpos = kv->cursorPosition();
221 uint line = curpos.line(), col = curpos.column();
222
223 QString lineStr = kv->document()->line( line );
224 QString leftCh = lineStr.mid( col-1, 1 );
225 QString secondLeftCh = lineStr.mid( col-2, 1 );
226
227 if( leftCh == "&" )
228 {
229 kDebug() << "Getting entities";
230 m_allowed = m_docDtds[doc]->entities(QString());
231 m_mode = entities;
232 }
233 else if( leftCh == "<" )
234 {
235 kDebug() << "*outside tag -> get elements";
236 QString parentElement = getParentElement( *kv, 1 );
237 kDebug() << "parent: " << parentElement;
238 m_allowed = m_docDtds[doc]->allowedElements(parentElement );
239 m_mode = elements;
240 }
241 else if ( leftCh == "/" && secondLeftCh == "<" )
242 {
243 kDebug() << "*close parent element";
244 QString parentElement = getParentElement( *kv, 2 );
245
246 if ( ! parentElement.isEmpty() )
247 {
248 m_mode = closingtag;
249 m_allowed = QStringList( parentElement );
250 }
251 }
252 else if( leftCh == " " || (isQuote(leftCh) && secondLeftCh == "=") )
253 {
254 // TODO: check secondLeftChar, too?! then you don't need to trigger
255 // with space and we yet save CPU power
256 QString currentElement = insideTag( *kv );
257 QString currentAttribute;
258 if( ! currentElement.isEmpty() )
259 currentAttribute = insideAttribute( *kv );
260
261 kDebug() << "Tag: " << currentElement;
262 kDebug() << "Attr: " << currentAttribute;
263
264 if( ! currentElement.isEmpty() && ! currentAttribute.isEmpty() )
265 {
266 kDebug() << "*inside attribute -> get attribute values";
267 m_allowed = m_docDtds[doc]->attributeValues(currentElement, currentAttribute );
268 if( m_allowed.count() == 1 &&
269 (m_allowed[0] == "CDATA" || m_allowed[0] == "ID" || m_allowed[0] == "IDREF" ||
270 m_allowed[0] == "IDREFS" || m_allowed[0] == "ENTITY" || m_allowed[0] == "ENTITIES" ||
271 m_allowed[0] == "NMTOKEN" || m_allowed[0] == "NMTOKENS" || m_allowed[0] == "NAME") )
272 {
273 // these must not be taken literally, e.g. don't insert the string "CDATA"
274 m_allowed.clear();
275 }
276 else
277 {
278 m_mode = attributevalues;
279 }
280 }
281 else if( ! currentElement.isEmpty() )
282 {
283 kDebug() << "*inside tag -> get attributes";
284 m_allowed = m_docDtds[doc]->allowedAttributes(currentElement );
285 m_mode = attributes;
286 }
287 }
288
289 //kDebug() << "time elapsed (ms): " << t.elapsed();
290 kDebug() << "Allowed strings: " << m_allowed.count();
291
292 if( m_allowed.count() >= 1 && m_allowed[0] != "__EMPTY" )
293 {
294 m_allowed = sortQStringList( m_allowed );
295 }
296 setRowCount( m_allowed.count() );
297 endResetModel();
298}
299
300int PluginKateXMLToolsCompletionModel::columnCount(const QModelIndex&) const
301{
302 return 1;
303}
304
305int PluginKateXMLToolsCompletionModel::rowCount(const QModelIndex &parent) const
306{
307 if (!m_allowed.isEmpty()) // Is there smth to complete?
308 {
309 if (!parent.isValid()) // Return the only one group node for root
310 return 1;
311 if (parent.internalId() == groupNode) // Return available rows count for group level node
312 return m_allowed.size();
313 }
314 return 0;
315}
316
317QModelIndex PluginKateXMLToolsCompletionModel::parent(const QModelIndex& index) const
318{
319 if (!index.isValid()) // Is root/invalid index?
320 return QModelIndex(); // Nothing to return...
321 if (index.internalId() == groupNode) // Return a root node for group
322 return QModelIndex();
323 // Otherwise, this is a leaf level, so return the only group as a parent
324 return createIndex(0, 0, groupNode);
325}
326
327QModelIndex PluginKateXMLToolsCompletionModel::index(const int row, const int column, const QModelIndex &parent) const
328{
329 if (!parent.isValid())
330 {
331 // At 'top' level only 'header' present, so nothing else than row 0 can be here...
332 return row == 0 ? createIndex(row, column, groupNode) : QModelIndex();
333 }
334 if (parent.internalId() == groupNode) // Is this a group node?
335 {
336 if (0 <= row && row < m_allowed.size()) // Make sure to return only valid indices
337 return createIndex(row, column, 0); // Just return a leaf-level index
338 }
339 // Leaf node has no children... nothing to return
340 return QModelIndex();
341}
342
343QVariant PluginKateXMLToolsCompletionModel::data(const QModelIndex &index, int role) const
344{
345 if (!index.isValid()) // Nothing to do w/ invalid index
346 return QVariant();
347
348 if (index.internalId() == groupNode) // Return group level node data
349 {
350 switch (role)
351 {
352 case KTextEditor::CodeCompletionModel::GroupRole:
353 return QVariant(Qt::DisplayRole);
354 case Qt::DisplayRole:
355 return currentModeToString();
356 default:
357 break;
358 }
359 return QVariant(); // Nothing to return for other roles
360 }
361 switch (role)
362 {
363 case Qt::DisplayRole:
364 switch (index.column())
365 {
366 case KTextEditor::CodeCompletionModel::Name:
367 return m_allowed.at(index.row());
368 default:
369 break;
370 }
371 default:
372 break;
373 }
374 return QVariant();
375}
376
377
378bool PluginKateXMLToolsCompletionModel::shouldStartCompletion( KTextEditor::View *view,
379 const QString &insertedText,
380 bool userInsertion,
381 const KTextEditor::Cursor &position )
382{
383 Q_UNUSED( view )
384 Q_UNUSED( userInsertion )
385 Q_UNUSED( position )
386 const QString triggerChars = "&</ '\""; // these are subsequently handled by completionInvoked()
387
388 return triggerChars.contains( insertedText.right(1) );
389}
390
391
392/**
393 * Load the meta DTD. In case of success set the 'ready'
394 * flag to true, to show that we're is ready to give hints about the DTD.
395 */
396void PluginKateXMLToolsCompletionModel::getDTD()
397{
398 if ( !application()->activeMainWindow() )
399 return;
400
401 KTextEditor::View *kv = application()->activeMainWindow()->activeView();
402 if( ! kv )
403 {
404 kDebug() << "Warning: no KTextEditor::View";
405 return;
406 }
407
408 // ### replace this with something more sane
409 // Start where the supplied XML-DTDs are fed by default unless
410 // user changed directory last time:
411
412 QString defaultDir = KGlobal::dirs()->findResourceDir("data", "katexmltools/" ) + "katexmltools/";
413 if( m_urlString.isNull() ) {
414 m_urlString = defaultDir;
415 }
416 KUrl url;
417
418 // Guess the meta DTD by looking at the doctype's public identifier.
419 // XML allows comments etc. before the doctype, so look further than
420 // just the first line.
421 // Example syntax:
422 // <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd">
423 uint checkMaxLines = 200;
424 QString documentStart = kv->document()->text( KTextEditor::Range(0, 0, checkMaxLines+1, 0) );
425 QRegExp re( "<!DOCTYPE\\s+(.*)\\s+PUBLIC\\s+[\"'](.*)[\"']", Qt::CaseInsensitive );
426 re.setMinimal( true );
427 int matchPos = re.indexIn( documentStart );
428 QString filename;
429 QString doctype;
430 QString topElement;
431
432 if( matchPos != -1 ) {
433 topElement = re.cap( 1 );
434 doctype = re.cap( 2 );
435 kDebug() << "Top element: " << topElement;
436 kDebug() << "Doctype match: " << doctype;
437 // XHTML:
438 if( doctype == "-//W3C//DTD XHTML 1.0 Transitional//EN" )
439 filename = "xhtml1-transitional.dtd.xml";
440 else if( doctype == "-//W3C//DTD XHTML 1.0 Strict//EN" )
441 filename = "xhtml1-strict.dtd.xml";
442 else if( doctype == "-//W3C//DTD XHTML 1.0 Frameset//EN" )
443 filename = "xhtml1-frameset.dtd.xml";
444 // HTML 4.0:
445 else if ( doctype == "-//W3C//DTD HTML 4.01 Transitional//EN" )
446 filename = "html4-loose.dtd.xml";
447 else if ( doctype == "-//W3C//DTD HTML 4.01//EN" )
448 filename = "html4-strict.dtd.xml";
449 // KDE Docbook:
450 else if ( doctype == "-//KDE//DTD DocBook XML V4.1.2-Based Variant V1.1//EN" )
451 filename = "kde-docbook.dtd.xml";
452 }
453 else if( documentStart.indexOf("<xsl:stylesheet" ) != -1 &&
454 documentStart.indexOf( "xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\"") != -1 )
455 {
456 /* XSLT doesn't have a doctype/DTD. We look for an xsl:stylesheet tag instead.
457 Example:
458 <xsl:stylesheet version="1.0"
459 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
460 xmlns="http://www.w3.org/TR/xhtml1/strict">
461 */
462 filename = "xslt-1.0.dtd.xml";
463 doctype = "XSLT 1.0";
464 }
465 else
466 kDebug() << "No doctype found";
467
468 if( filename.isEmpty() )
469 {
470 // no meta dtd found for this file
471 url = KFileDialog::getOpenUrl(m_urlString, "*.xml",
472 0, i18n( "Assign Meta DTD in XML Format") );
473 }
474 else
475 {
476 url.setFileName( defaultDir + filename );
477 KMessageBox::information(0, i18n("The current file has been identified "
478 "as a document of type \"%1\". The meta DTD for this document type "
479 "will now be loaded.", doctype ),
480 i18n( "Loading XML Meta DTD" ),
481 QString::fromLatin1( "DTDAssigned") );
482 }
483
484 if( url.isEmpty() )
485 return;
486
487 m_urlString = url.url(); // remember directory for next time
488
489 if ( m_dtds[ m_urlString ] )
490 assignDTD( m_dtds[ m_urlString ], kv->document() );
491 else
492 {
493 m_dtdString.clear();
494 m_docToAssignTo = kv->document();
495
496 KApplication::setOverrideCursor( Qt::WaitCursor );
497 KIO::Job *job = KIO::get( url );
498 connect( job, SIGNAL(result(KJob*)), this, SLOT(slotFinished(KJob*)) );
499 connect( job, SIGNAL(data(KIO::Job*,QByteArray)),
500 this, SLOT(slotData(KIO::Job*,QByteArray)) );
501 }
502 kDebug()<<"XMLTools::getDTD: Documents: "<<m_docDtds.count()<<", DTDs: "<<m_dtds.count();
503}
504
505void PluginKateXMLToolsCompletionModel::slotFinished( KJob *job )
506{
507 if( job->error() )
508 {
509 //kDebug() << "XML Plugin error: DTD in XML format (" << filename << " ) could not be loaded";
510 static_cast<KIO::Job*>(job)->ui()->showErrorMessage();
511 }
512 else if ( static_cast<KIO::TransferJob *>(job)->isErrorPage() )
513 {
514 // catch failed loading loading via http:
515 KMessageBox::error(0, i18n("The file '%1' could not be opened. "
516 "The server returned an error.", m_urlString ),
517 i18n( "XML Plugin Error") );
518 }
519 else
520 {
521 PseudoDTD *dtd = new PseudoDTD();
522 dtd->analyzeDTD( m_urlString, m_dtdString );
523
524 m_dtds.insert( m_urlString, dtd );
525 assignDTD( dtd, m_docToAssignTo );
526
527 // clean up a bit
528 m_docToAssignTo = 0;
529 m_dtdString.clear();
530 }
531 QApplication::restoreOverrideCursor();
532}
533
534void PluginKateXMLToolsCompletionModel::slotData( KIO::Job *, const QByteArray &data )
535{
536 m_dtdString += QString( data );
537}
538
539void PluginKateXMLToolsCompletionModel::assignDTD( PseudoDTD *dtd, KTextEditor::Document *doc )
540{
541 m_docDtds.insert( doc, dtd );
542
543 //TODO:perhaps foreach views()?
544 KTextEditor::CodeCompletionInterface *cci = qobject_cast<KTextEditor::CodeCompletionInterface *>(doc->activeView());
545
546 if( cci ) {
547 cci->registerCompletionModel( this );
548 cci->setAutomaticInvocationEnabled( true );
549 kDebug() << "PluginKateXMLToolsView: completion model registered";
550 }
551 else {
552 kWarning() << "PluginKateXMLToolsView: completion interface unavailable";
553 }
554}
555
556/**
557 * Offer a line edit with completion for possible elements at cursor position and insert the
558 * tag one chosen/entered by the user, plus its closing tag. If there's a text selection,
559 * add the markup around it.
560 */
561void PluginKateXMLToolsCompletionModel::slotInsertElement()
562{
563 if ( !application()->activeMainWindow() )
564 return;
565
566 KTextEditor::View *kv = application()->activeMainWindow()->activeView();
567 if( ! kv )
568 {
569 kDebug() << "Warning: no KTextEditor::View";
570 return;
571 }
572
573 KTextEditor::Document *doc = kv->document();
574 PseudoDTD *dtd = m_docDtds[doc];
575 QString parentElement = getParentElement( *kv, 0 );
576 QStringList allowed;
577
578 if( dtd )
579 allowed = dtd->allowedElements(parentElement );
580
581 InsertElement *dialog = new InsertElement(
582 ( QWidget *)application()->activeMainWindow()->activeView(), "insertXml" );
583 QString text = dialog->showDialog( allowed );
584 delete dialog;
585
586 if( !text.isEmpty() )
587 {
588 QStringList list = text.split( QChar(' ') );
589 QString pre;
590 QString post;
591 // anders: use <tagname/> if the tag is required to be empty.
592 // In that case maybe we should not remove the selection? or overwrite it?
593 int adjust = 0; // how much to move cursor.
594 // if we know that we have attributes, it goes
595 // just after the tag name, otherwise between tags.
596 if ( dtd && dtd->allowedAttributes(list[0]).count() )
597 adjust++; // the ">"
598
599 if ( dtd && dtd->allowedElements(list[0]).contains("__EMPTY") )
600 {
601 pre = '<' + text + "/>";
602 if ( adjust )
603 adjust++; // for the "/"
604 }
605 else
606 {
607 pre = '<' + text + '>';
608 post ="</" + list[0] + '>';
609 }
610
611 QString marked;
612 if ( ! post.isEmpty() )
613 marked = kv->selectionText();
614
615 doc->startEditing();
616
617 if( ! marked.isEmpty() )
618 kv->removeSelectionText();
619
620 // with the old selection now removed, curPos points to the start of pre
621 KTextEditor::Cursor curPos = kv->cursorPosition();
622 curPos.setColumn( curPos.column() + pre.length() - adjust );
623
624 kv->insertText( pre + marked + post );
625
626 kv->setCursorPosition( curPos );
627
628 doc->endEditing();
629 }
630}
631
632/**
633 * Insert a closing tag for the nearest not-closed parent element.
634 */
635void PluginKateXMLToolsCompletionModel::slotCloseElement()
636{
637 if ( !application()->activeMainWindow() )
638 return;
639
640 KTextEditor::View *kv = application()->activeMainWindow()->activeView();
641 if( ! kv )
642 {
643 kDebug() << "Warning: no KTextEditor::View";
644 return;
645 }
646 QString parentElement = getParentElement( *kv, 0 );
647
648 //kDebug() << "parentElement: '" << parentElement << "'";
649 QString closeTag = "</" + parentElement + '>';
650 if( ! parentElement.isEmpty() )
651 kv->insertText( closeTag );
652}
653
654
655// modify the completion string before it gets inserted
656void PluginKateXMLToolsCompletionModel::executeCompletionItem2( KTextEditor::Document *document,
657 const KTextEditor::Range &word,
658 const QModelIndex &index ) const
659{
660 KTextEditor::Range toReplace = word;
661
662 QString text = data( index.sibling( index.row(), Name ), Qt::DisplayRole ).toString();
663
664 kDebug() << "executeCompletionItem text: " << text;
665
666 KTextEditor::View *kv = document->activeView();
667 if( ! kv )
668 {
669 kDebug() << "Warning (filterInsertString() ): no KTextEditor::View";
670 return;
671 }
672
673 int line, col;
674 kv->cursorPosition().position( line, col );
675 QString lineStr = document->line( line );
676 QString leftCh = lineStr.mid( col-1, 1 );
677 QString rightCh = lineStr.mid( col, 1 );
678
679 int posCorrection = 0; // where to move the cursor after completion ( >0 = move right )
680 if( m_mode == entities )
681 {
682 text = text + ';';
683 }
684
685 else if( m_mode == attributes )
686 {
687 text = text + "=\"\"";
688 posCorrection = -1;
689 if( !rightCh.isEmpty() && rightCh != ">" && rightCh != "/" && rightCh != " " )
690 { // TODO: other whitespaces
691 // add space in front of the next attribute
692 text = text + ' ';
693 posCorrection--;
694 }
695 }
696
697 else if( m_mode == attributevalues )
698 {
699 // TODO: support more than one line
700 uint startAttValue = 0;
701 uint endAttValue = 0;
702
703 // find left quote:
704 for( startAttValue = col; startAttValue > 0; startAttValue-- )
705 {
706 QString ch = lineStr.mid( startAttValue-1, 1 );
707 if( isQuote(ch) )
708 break;
709 }
710
711 // find right quote:
712 for( endAttValue = col; endAttValue <= (uint) lineStr.length(); endAttValue++ )
713 {
714 QString ch = lineStr.mid( endAttValue-1, 1 );
715 if( isQuote(ch) )
716 break;
717 }
718
719 // replace the current contents of the attribute
720 if( startAttValue < endAttValue )
721 toReplace = KTextEditor::Range( line, startAttValue, line, endAttValue-1 );
722 }
723
724 else if( m_mode == elements )
725 {
726 // anders: if the tag is marked EMPTY, insert in form <tagname/>
727 QString str;
728 bool isEmptyTag =m_docDtds[document]->allowedElements(text).contains( "__EMPTY" );
729 if ( isEmptyTag )
730 str = text + "/>";
731 else
732 str = text + "></" + text + '>';
733
734 // Place the cursor where it is most likely wanted:
735 // always inside the tag if the tag is empty AND the DTD indicates that there are attribs)
736 // outside for open tags, UNLESS there are mandatory attributes
737 if ( m_docDtds[document]->requiredAttributes(text).count()
738 || ( isEmptyTag && m_docDtds[document]->allowedAttributes(text).count() ) )
739 posCorrection = text.length() - str.length();
740 else if ( ! isEmptyTag )
741 posCorrection = text.length() - str.length() + 1;
742
743 text = str;
744 }
745
746 else if( m_mode == closingtag )
747 {
748 text += '>';
749 }
750
751 document->replaceText( toReplace, text );
752
753 // move the cursor to desired position
754 KTextEditor::Cursor curPos = kv->cursorPosition();
755 curPos.setColumn( curPos.column() + posCorrection );
756 kv->setCursorPosition( curPos );
757}
758
759
760// ========================================================================
761// Pseudo-XML stuff:
762
763/**
764 * Check if cursor is inside a tag, that is
765 * if "<" occurs before ">" occurs ( on the left side of the cursor ).
766 * Return the tag name, return "" if we cursor is outside a tag.
767 */
768QString PluginKateXMLToolsCompletionModel::insideTag( KTextEditor::View &kv )
769{
770 int line, col;
771 kv.cursorPosition().position( line, col );
772 int y = line; // another variable because uint <-> int
773
774 do {
775 QString lineStr = kv.document()->line(y );
776 for( uint x = col; x > 0; x-- )
777 {
778 QString ch = lineStr.mid( x-1, 1 );
779 if( ch == ">" ) // cursor is outside tag
780 return QString();
781
782 if( ch == "<" )
783 {
784 QString tag;
785 // look for white space on the right to get the tag name
786 for( int z = x; z <= lineStr.length() ; ++z )
787 {
788 ch = lineStr.mid( z-1, 1 );
789 if( ch.at(0).isSpace() || ch == "/" || ch == ">" )
790 return tag.right( tag.length()-1 );
791
792 if( z == lineStr.length() )
793 {
794 tag += ch;
795 return tag.right( tag.length()-1 );
796 }
797
798 tag += ch;
799 }
800 }
801 }
802 y--;
803 col = kv.document()->line(y).length();
804 } while( y >= 0 );
805
806 return QString();
807}
808
809/**
810 * Check if cursor is inside an attribute value, that is
811 * if '="' is on the left, and if it's nearer than "<" or ">".
812 *
813 * @Return the attribute name or "" if we're outside an attribute
814 * value.
815 *
816 * Note: only call when insideTag() == true.
817 * TODO: allow whitespace around "="
818 */
819QString PluginKateXMLToolsCompletionModel::insideAttribute( KTextEditor::View &kv )
820{
821 int line, col;
822 kv.cursorPosition().position( line, col );
823 int y = line; // another variable because uint <-> int
824 uint x = 0;
825 QString lineStr;
826 QString ch;
827
828 do {
829 lineStr = kv.document()->line(y );
830 for( x = col; x > 0; x-- )
831 {
832 ch = lineStr.mid( x-1, 1 );
833 QString chLeft = lineStr.mid( x-2, 1 );
834 // TODO: allow whitespace
835 if( isQuote(ch) && chLeft == "=" )
836 break;
837 else if( isQuote(ch) && chLeft != "=" )
838 return QString();
839 else if( ch == "<" || ch == ">" )
840 return QString();
841 }
842 y--;
843 col = kv.document()->line(y).length();
844 } while( !isQuote(ch) );
845
846 // look for next white space on the left to get the tag name
847 QString attr;
848 for( int z = x; z >= 0; z-- )
849 {
850 ch = lineStr.mid( z-1, 1 );
851
852 if( ch.at(0).isSpace() )
853 break;
854
855 if( z == 0 )
856 { // start of line == whitespace
857 attr += ch;
858 break;
859 }
860
861 attr = ch + attr;
862 }
863
864 return attr.left( attr.length()-2 );
865}
866
867/**
868 * Find the parent element for the current cursor position. That is,
869 * go left and find the first opening element that's not closed yet,
870 * ignoring empty elements.
871 * Examples: If cursor is at "X", the correct parent element is "p":
872 * <p> <a x="xyz"> foo <i> test </i> bar </a> X
873 * <p> <a x="xyz"> foo bar </a> X
874 * <p> foo <img/> bar X
875 * <p> foo bar X
876 */
877QString PluginKateXMLToolsCompletionModel::getParentElement( KTextEditor::View &kv, int skipCharacters )
878{
879 enum {
880 parsingText,
881 parsingElement,
882 parsingElementBoundary,
883 parsingNonElement,
884 parsingAttributeDquote,
885 parsingAttributeSquote,
886 parsingIgnore
887 } parseState;
888 parseState = (skipCharacters > 0) ? parsingIgnore : parsingText;
889
890 int nestingLevel = 0;
891
892 int line, col;
893 kv.cursorPosition().position( line, col );
894 QString str = kv.document()->line(line );
895
896 while( true )
897 {
898 // move left a character
899 if( !col-- )
900 {
901 do
902 {
903 if( !line-- ) return QString(); // reached start of document
904 str = kv.document()->line(line );
905 col = str.length();
906 } while( !col );
907 --col;
908 }
909
910 ushort ch = str.at( col).unicode();
911
912 switch( parseState )
913 {
914 case parsingIgnore:
915 // ignore the specified number of characters
916 parseState = ( --skipCharacters > 0 ) ? parsingIgnore : parsingText;
917 break;
918
919 case parsingText:
920 switch( ch )
921 {
922 case '<':
923 // hmm... we were actually inside an element
924 return QString();
925
926 case '>':
927 // we just hit an element boundary
928 parseState = parsingElementBoundary;
929 break;
930 }
931 break;
932
933 case parsingElement:
934 switch( ch )
935 {
936 case '"': // attribute ( double quoted )
937 parseState = parsingAttributeDquote;
938 break;
939
940 case '\'': // attribute ( single quoted )
941 parseState = parsingAttributeSquote;
942 break;
943
944 case '/': // close tag
945 parseState = parsingNonElement;
946 ++nestingLevel;
947 break;
948
949 case '<':
950 // we just hit the start of the element...
951 if( nestingLevel-- ) break;
952
953 QString tag = str.mid( col + 1 );
954 for( uint pos = 0, len = tag.length(); pos < len; ++pos ) {
955 ch = tag.at( pos).unicode();
956 if( ch == ' ' || ch == '\t' || ch == '>' ) {
957 tag.truncate( pos );
958 break;
959 }
960 }
961 return tag;
962 }
963 break;
964
965 case parsingElementBoundary:
966 switch( ch )
967 {
968 case '?': // processing instruction
969 case '-': // comment
970 case '/': // empty element
971 parseState = parsingNonElement;
972 break;
973
974 case '"':
975 parseState = parsingAttributeDquote;
976 break;
977
978 case '\'':
979 parseState = parsingAttributeSquote;
980 break;
981
982 case '<': // empty tag ( bad XML )
983 parseState = parsingText;
984 break;
985
986 default:
987 parseState = parsingElement;
988 }
989 break;
990
991 case parsingAttributeDquote:
992 if( ch == '"' ) parseState = parsingElement;
993 break;
994
995 case parsingAttributeSquote:
996 if( ch == '\'' ) parseState = parsingElement;
997 break;
998
999 case parsingNonElement:
1000 if( ch == '<' ) parseState = parsingText;
1001 break;
1002 }
1003 }
1004}
1005
1006/**
1007 * Return true if the tag is neither a closing tag
1008 * nor an empty tag, nor a comment, nor processing instruction.
1009 */
1010bool PluginKateXMLToolsCompletionModel::isOpeningTag( const QString& tag )
1011{
1012 return ( !isClosingTag(tag) && !isEmptyTag(tag ) &&
1013 !tag.startsWith( "<?") && !tag.startsWith("<!") );
1014}
1015
1016/**
1017 * Return true if the tag is a closing tag. Return false
1018 * if the tag is an opening tag or an empty tag ( ! )
1019 */
1020bool PluginKateXMLToolsCompletionModel::isClosingTag( const QString& tag )
1021{
1022 return ( tag.startsWith("</") );
1023}
1024
1025bool PluginKateXMLToolsCompletionModel::isEmptyTag( const QString& tag )
1026{
1027 return ( tag.right(2) == "/>" );
1028}
1029
1030/**
1031 * Return true if ch is a single or double quote. Expects ch to be of length 1.
1032 */
1033bool PluginKateXMLToolsCompletionModel::isQuote( const QString& ch )
1034{
1035 return ( ch == "\"" || ch == "'" );
1036}
1037
1038
1039// ========================================================================
1040// Tools:
1041
1042/// Get string describing current mode
1043QString PluginKateXMLToolsCompletionModel::currentModeToString() const
1044{
1045 switch (m_mode)
1046 {
1047 case entities:
1048 return i18n("XML entities");
1049 case attributevalues:
1050 return i18n("XML attribute values");
1051 case attributes:
1052 return i18n("XML attributes");
1053 case elements:
1054 case closingtag:
1055 return i18n("XML elements");
1056 default:
1057 break;
1058 }
1059 return QString();
1060}
1061
1062/** Sort a QStringList case-insensitively. Static. TODO: make it more simple. */
1063QStringList PluginKateXMLToolsCompletionModel::sortQStringList( QStringList list ) {
1064 // Sort list case-insensitive. This looks complicated but using a QMap
1065 // is even suggested by the Qt documentation.
1066 QMap<QString,QString> mapList;
1067 for ( QStringList::Iterator it = list.begin(); it != list.end(); ++it )
1068 {
1069 QString str = *it;
1070 if( mapList.contains(str.toLower()) )
1071 {
1072 // do not override a previous value, e.g. "Auml" and "auml" are two different
1073 // entities, but they should be sorted next to each other.
1074 // TODO: currently it's undefined if e.g. "A" or "a" comes first, it depends on
1075 // the meta DTD ( really? it seems to work okay?!? )
1076 mapList[str.toLower()+'_'] = str;
1077 }
1078 else
1079 mapList[str.toLower()] = str;
1080 }
1081
1082 list.clear();
1083 QMap<QString,QString>::Iterator it;
1084
1085 // Qt doc: "the items are alphabetically sorted [by key] when iterating over the map":
1086 for( it = mapList.begin(); it != mapList.end(); ++it )
1087 list.append( it.value() );
1088
1089 return list;
1090}
1091
1092//BEGIN InsertElement dialog
1093InsertElement::InsertElement( QWidget* const parent, const char *name )
1094 :KDialog( parent)
1095{
1096 Q_UNUSED( name )
1097
1098 setCaption(i18n("Insert XML Element" ));
1099 setButtons(KDialog::Ok|KDialog::Cancel);
1100 setDefaultButton(KDialog::Ok);
1101 setModal(true);
1102}
1103
1104InsertElement::~InsertElement()
1105{
1106}
1107
1108void InsertElement::slotHistoryTextChanged( const QString& text )
1109{
1110 enableButtonOk( !text.isEmpty() );
1111}
1112
1113QString InsertElement::showDialog( QStringList &completions )
1114{
1115 QWidget *page = new QWidget( this );
1116 setMainWidget( page );
1117 QVBoxLayout *topLayout = new QVBoxLayout( page );
1118
1119 KHistoryComboBox *combo = new KHistoryComboBox( page );
1120 combo->setHistoryItems( completions, true );
1121 connect( combo->lineEdit(), SIGNAL(textChanged(QString)),
1122 this, SLOT(slotHistoryTextChanged(QString)) );
1123 QString text = i18n( "Enter XML tag name and attributes (\"<\", \">\" and closing tag will be supplied):" );
1124 QLabel *label = new QLabel( text, page );
1125
1126 topLayout->addWidget( label );
1127 topLayout->addWidget( combo );
1128
1129 combo->setFocus();
1130 slotHistoryTextChanged( combo->lineEdit()->text() );
1131 if( exec() )
1132 return combo->currentText();
1133
1134 return QString();
1135}
1136//END InsertElement dialog
1137// kate: space-indent on; indent-width 2; replace-tabs on; mixed-indent off;
1138