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 | /* |
32 | README: |
33 | The basic idea is this: certain keyEvents(), namely [<&" ], trigger a completion box. |
34 | This is intended as a help for editing. There are some cases where the XML |
35 | spec is not followed, e.g. one can add the same attribute twice to an element. |
36 | Also see the user documentation. If backspace is pressed after a completion popup |
37 | was closed, the popup will re-open. This way typos can be corrected and the popup |
38 | will reappear, which is quite comfortable. |
39 | |
40 | FIXME: |
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 | |
45 | TODO: |
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 | |
100 | K_PLUGIN_FACTORY(PluginKateXMLToolsFactory, registerPlugin<PluginKateXMLTools>();) |
101 | K_EXPORT_PLUGIN(PluginKateXMLToolsFactory("katexmltools" )) |
102 | |
103 | using Kate::application; |
104 | |
105 | |
106 | PluginKateXMLTools::PluginKateXMLTools( QObject* const parent, const QVariantList& ) |
107 | : Kate::Plugin ( (Kate::Application *)parent ) |
108 | { |
109 | } |
110 | |
111 | PluginKateXMLTools::~PluginKateXMLTools() |
112 | { |
113 | } |
114 | |
115 | Kate::PluginView *PluginKateXMLTools::createView(Kate::MainWindow *mainWindow) |
116 | { |
117 | return new PluginKateXMLToolsView(mainWindow); |
118 | } |
119 | |
120 | |
121 | PluginKateXMLToolsView::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 | |
149 | PluginKateXMLToolsView::~PluginKateXMLToolsView() |
150 | { |
151 | mainWindow()->guiFactory()->removeClient (this); |
152 | |
153 | //kDebug() << "xml tools descructor 1..."; |
154 | //TODO: unregister the model |
155 | } |
156 | |
157 | PluginKateXMLToolsCompletionModel::PluginKateXMLToolsCompletionModel( QObject* const parent ) |
158 | : CodeCompletionModel2 (parent) |
159 | , m_docToAssignTo(0) |
160 | , m_mode(none) |
161 | , m_correctPos(0) |
162 | { |
163 | } |
164 | |
165 | PluginKateXMLToolsCompletionModel::~PluginKateXMLToolsCompletionModel() |
166 | { |
167 | qDeleteAll( m_dtds ); |
168 | m_dtds.clear(); |
169 | } |
170 | |
171 | void 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 | |
197 | void 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 | |
300 | int PluginKateXMLToolsCompletionModel::columnCount(const QModelIndex&) const |
301 | { |
302 | return 1; |
303 | } |
304 | |
305 | int 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 | |
317 | QModelIndex 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 | |
327 | QModelIndex 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 | |
343 | QVariant 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 | |
378 | bool 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 | */ |
396 | void 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 | |
505 | void 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 | |
534 | void PluginKateXMLToolsCompletionModel::slotData( KIO::Job *, const QByteArray &data ) |
535 | { |
536 | m_dtdString += QString( data ); |
537 | } |
538 | |
539 | void 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 | */ |
561 | void 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 | */ |
635 | void 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 |
656 | void 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 | */ |
768 | QString 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 | */ |
819 | QString 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 | */ |
877 | QString 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 | */ |
1010 | bool 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 | */ |
1020 | bool PluginKateXMLToolsCompletionModel::isClosingTag( const QString& tag ) |
1021 | { |
1022 | return ( tag.startsWith("</" ) ); |
1023 | } |
1024 | |
1025 | bool 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 | */ |
1033 | bool PluginKateXMLToolsCompletionModel::isQuote( const QString& ch ) |
1034 | { |
1035 | return ( ch == "\"" || ch == "'" ); |
1036 | } |
1037 | |
1038 | |
1039 | // ======================================================================== |
1040 | // Tools: |
1041 | |
1042 | /// Get string describing current mode |
1043 | QString 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. */ |
1063 | QStringList 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 |
1093 | InsertElement::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 | |
1104 | InsertElement::~InsertElement() |
1105 | { |
1106 | } |
1107 | |
1108 | void InsertElement::slotHistoryTextChanged( const QString& text ) |
1109 | { |
1110 | enableButtonOk( !text.isEmpty() ); |
1111 | } |
1112 | |
1113 | QString 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 | |